commit bb43aed936d8d815d585e385398838315c180b40 Author: MOHAN Date: Fri Mar 20 14:54:08 2026 +0530 Initial GIT Veriflo - Backend diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a989f89 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +PORT=3000 +VERTIFLO_DASHBOARD_ORIGIN="http://localhost:5174" +VERTIFLO_ENABLE_WHATSAPP_CLIENT=true +VERTIFLO_MESSAGE_TEMPLATE_MAX_CHARS=320 +VERTIFLO_SANDBOX_MONTHLY_MESSAGE_LIMIT=500 +VERTIFLO_LIVE_MONTHLY_MESSAGE_LIMIT=100 +VERTIFLO_SANDBOX_INTEGRATION_QUOTA_UNITS=1 +VERTIFLO_UPI_ID="your-upi-id@bank" +VERTIFLO_UPI_NAME="Veriflo" +VERTIFLO_SUPERADMIN_EMAIL="superadmin@veriflo.local" +VERTIFLO_SUPERADMIN_PASSWORD="SuperAdmin@123" +VERTIFLO_SUPERADMIN_NAME="Super Admin" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51dc2da --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.wwebjs_auth/ +.wwebjs_cache/ +.env diff --git a/database.sqlite b/database.sqlite new file mode 100644 index 0000000..850ef29 Binary files /dev/null and b/database.sqlite differ diff --git a/database.sqlite-shm b/database.sqlite-shm new file mode 100644 index 0000000..1e87772 Binary files /dev/null and b/database.sqlite-shm differ diff --git a/database.sqlite-wal b/database.sqlite-wal new file mode 100644 index 0000000..68eb91e Binary files /dev/null and b/database.sqlite-wal differ diff --git a/index.js b/index.js new file mode 100644 index 0000000..6daccbd --- /dev/null +++ b/index.js @@ -0,0 +1,144 @@ +const express = require("express"); +const cors = require("cors"); +const helmet = require("helmet"); +const rateLimit = require("express-rate-limit"); +const { Client, LocalAuth } = require("whatsapp-web.js"); +const qrcode = require("qrcode-terminal"); +const { loadEnvFile } = require("./src/utils/env"); +const { initSchema } = require("./src/db"); +const { requestLogger } = require("./src/utils/logger"); + +loadEnvFile(); + +const app = express(); +const PORT = process.env.PORT || 3000; +const dashboardOrigin = process.env.VERTIFLO_DASHBOARD_ORIGIN || 'http://localhost:5174'; +const whatsappEnabled = process.env.VERTIFLO_ENABLE_WHATSAPP_CLIENT !== 'false'; + +// Initialize SQLite Schema +initSchema(); + +// --- Global Security & Middleware --- +app.use(helmet()); +app.use(cors({ + origin: [dashboardOrigin, 'http://localhost:5173'], + credentials: false, +})); +app.use(express.json()); + +// Request logging middleware - logs all requests with timestamps to file +app.use(requestLogger); + +// Import Routers +const authRoutes = require('./src/routes/auth'); +const userRoutes = require('./src/routes/user'); +const otpRoutes = require('./src/routes/otp'); +const analyticsRoutes = require('./src/routes/analytics'); +const adminRoutes = require('./src/routes/admin'); + +// Trust proxy if running behind Nginx/Vercel to get real IPs for analytics +app.set('trust proxy', 1); + +// Global Rate Limiter — 5000 reqs / 15 mins per IP (development-friendly, still prevents abuse) +const globalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5000, + standardHeaders: true, + legacyHeaders: false, + message: { error: 'Too many requests from this IP. Please try again later.' }, + skip: (req) => req.ip === '127.0.0.1' || req.ip === '::1', // Skip rate limiting for localhost +}); +app.use(globalLimiter); + +// --- WhatsApp Client Wrapper --- +// We attach the client and its state to `app.locals` so routes can access it +app.locals.waReady = false; +app.locals.waInitError = null; +app.locals.waClient = null; + +if (whatsappEnabled) { + const client = new Client({ + authStrategy: new LocalAuth(), + puppeteer: { + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-features=site-per-process', + '--ignore-certificate-errors', + '--ignore-certificate-errors-spki-list' + ] + } + }); + + client.on("qr", (qr) => { + console.log("Scan this QR code with WhatsApp:"); + qrcode.generate(qr, { small: true }); + }); + + client.on("ready", () => { + app.locals.waReady = true; + app.locals.waInitError = null; + console.log("WhatsApp client is ready!"); + }); + + client.on("authenticated", () => { + console.log("Authenticated successfully."); + }); + + client.on("auth_failure", (msg) => { + app.locals.waInitError = typeof msg === 'string' ? msg : 'WhatsApp authentication failed.'; + console.error("Authentication failed:", msg); + }); + + client.on("disconnected", (reason) => { + app.locals.waReady = false; + app.locals.waInitError = typeof reason === 'string' ? reason : 'WhatsApp client disconnected.'; + console.log("Client disconnected:", reason); + }); + + client.initialize().catch((error) => { + app.locals.waReady = false; + app.locals.waInitError = error.message; + console.error("WhatsApp client failed to initialize:", error.message); + }); + + app.locals.waClient = client; +} else { + app.locals.waInitError = 'WhatsApp client disabled by configuration.'; + console.log('WhatsApp client initialization skipped because VERTIFLO_ENABLE_WHATSAPP_CLIENT=false'); +} + +// --- Express Routes --- + +app.get("/status", (req, res) => { + res.json({ + connected: app.locals.waReady, + whatsapp_enabled: whatsappEnabled, + whatsapp_error: app.locals.waInitError, + timestamp: new Date(), + port: PORT, + dashboard_origin: dashboardOrigin, + }); +}); + +// --- Register Internal Dashboard APIs --- +app.use('/api/auth', authRoutes); +app.use('/api/user', userRoutes); +app.use('/api/user/analytics', analyticsRoutes); +app.use('/api/admin', adminRoutes); + +// --- Register Public Facing Edge SDK APIs --- +app.use('/v1/otp', otpRoutes); + +// Legacy /send route temporarily preserved for reference, +// but it will be replaced by the secured /v1/otp/send SDK endpoint later +app.post("/send", async (req, res) => { + if (!app.locals.waReady) return res.status(503).json({ error: "WhatsApp not ready" }); + res.status(410).json({ error: "Use /v1/otp/send instead" }); +}); + +// --- Server Startup --- +app.listen(PORT, () => { + console.log(`Veriflo API server running on http://localhost:${PORT}`); +}); diff --git a/logs/requests-2026-03-12.log b/logs/requests-2026-03-12.log new file mode 100644 index 0000000..08ddf6d --- /dev/null +++ b/logs/requests-2026-03-12.log @@ -0,0 +1,42 @@ +[2026-03-12T20:57:48.932Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 29ms +[2026-03-12T20:57:48.959Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T20:58:11.121Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T20:58:11.133Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T20:58:13.074Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T20:58:13.078Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-12T20:58:13.091Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T20:58:13.098Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T20:58:13.722Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 6ms +[2026-03-12T20:58:14.425Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 185ms +[2026-03-12T20:58:14.517Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T20:58:14.671Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 59ms +[2026-03-12T20:58:21.921Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 8ms +[2026-03-12T20:58:21.997Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 13ms +[2026-03-12T20:58:37.736Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T20:58:37.888Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-12T20:58:38.746Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 4ms +[2026-03-12T20:58:38.758Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 6ms +[2026-03-12T20:58:38.776Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T20:58:38.783Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-12T20:58:40.231Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T20:58:40.244Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T20:59:42.915Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 160ms +[2026-03-12T20:59:43.083Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T20:59:50.093Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 123ms +[2026-03-12T20:59:50.237Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T21:00:07.223Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T21:00:07.245Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T21:00:07.757Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T21:00:07.803Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T21:00:09.572Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 4ms +[2026-03-12T21:00:09.702Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-12T21:00:10.197Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 40ms +[2026-03-12T21:00:10.679Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 342ms +[2026-03-12T21:00:10.784Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 102ms +[2026-03-12T21:00:10.915Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-12T21:00:10.917Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-12T21:00:11.054Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 104ms +[2026-03-12T21:00:11.337Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-12T21:00:11.349Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-12T21:00:12.382Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-12T21:00:12.516Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 13ms diff --git a/logs/requests-2026-03-13.log b/logs/requests-2026-03-13.log new file mode 100644 index 0000000..d3f76fa --- /dev/null +++ b/logs/requests-2026-03-13.log @@ -0,0 +1,3 @@ +[2026-03-13T07:50:53.259Z] POST /signup/send-otp - | IP: ::1 | Status: 503 | Duration: 408ms +[2026-03-13T07:51:52.997Z] POST /signup/send-otp - | IP: ::1 | Status: 200 | Duration: 515ms +[2026-03-13T07:52:15.833Z] POST /signup/send-otp - | IP: ::1 | Status: 200 | Duration: 576ms diff --git a/logs/requests-2026-03-15.log b/logs/requests-2026-03-15.log new file mode 100644 index 0000000..0e7e550 --- /dev/null +++ b/logs/requests-2026-03-15.log @@ -0,0 +1,4 @@ +[2026-03-15T03:59:10.909Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 26ms +[2026-03-15T03:59:10.927Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 4ms +[2026-03-15T03:59:10.938Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 4ms +[2026-03-15T03:59:10.945Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 3ms diff --git a/logs/requests-2026-03-19.log b/logs/requests-2026-03-19.log new file mode 100644 index 0000000..8d027bf --- /dev/null +++ b/logs/requests-2026-03-19.log @@ -0,0 +1,663 @@ +[2026-03-19T18:59:19.004Z] POST /login - | IP: ::1 | Status: 401 | Duration: 64ms +[2026-03-19T18:59:20.367Z] POST /login - | IP: ::1 | Status: 401 | Duration: 56ms +[2026-03-19T18:59:20.838Z] POST /login - | IP: ::1 | Status: 401 | Duration: 60ms +[2026-03-19T18:59:21.100Z] POST /login - | IP: ::1 | Status: 401 | Duration: 55ms +[2026-03-19T18:59:21.351Z] POST /login - | IP: ::1 | Status: 401 | Duration: 58ms +[2026-03-19T18:59:24.567Z] POST /login - | IP: ::1 | Status: 401 | Duration: 58ms +[2026-03-19T18:59:25.148Z] POST /login - | IP: ::1 | Status: 401 | Duration: 61ms +[2026-03-19T18:59:25.268Z] POST /login - | IP: ::1 | Status: 401 | Duration: 58ms +[2026-03-19T18:59:25.393Z] POST /login - | IP: ::1 | Status: 401 | Duration: 62ms +[2026-03-19T18:59:32.399Z] POST /signup/send-otp - | IP: ::1 | Status: 200 | Duration: 1637ms +[2026-03-19T19:00:04.337Z] POST /signup/verify - | IP: ::1 | Status: 409 | Duration: 19ms +[2026-03-19T19:00:05.233Z] POST /signup/verify - | IP: ::1 | Status: 409 | Duration: 18ms +[2026-03-19T19:00:05.350Z] POST /signup/verify - | IP: ::1 | Status: 409 | Duration: 17ms +[2026-03-19T19:00:05.559Z] POST /signup/verify - | IP: ::1 | Status: 409 | Duration: 14ms +[2026-03-19T19:00:09.789Z] POST /signup/verify - | IP: ::1 | Status: 201 | Duration: 91ms +[2026-03-19T19:00:09.924Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 7ms +[2026-03-19T19:00:09.926Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:00:09.932Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:00:09.933Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:01:36.135Z] POST /login - | IP: ::1 | Status: 200 | Duration: 71ms +[2026-03-19T19:01:36.156Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:01:36.158Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:01:36.163Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:01:36.165Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:03:09.319Z] GET /api-keys - | IP: ::1 | Status: 200 | Duration: 8ms +[2026-03-19T19:03:09.327Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:03:47.820Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:03:47.822Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:06:30.473Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 5ms +[2026-03-19T19:06:30.481Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:09:51.454Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 2ms +[2026-03-19T19:09:52.207Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 1ms +[2026-03-19T19:09:52.391Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 0ms +[2026-03-19T19:09:52.593Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 0ms +[2026-03-19T19:09:52.747Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 1ms +[2026-03-19T19:09:52.968Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 0ms +[2026-03-19T19:09:53.275Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 0ms +[2026-03-19T19:09:53.571Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 1ms +[2026-03-19T19:09:53.790Z] POST /test-webhook - | IP: ::1 | Status: 400 | Duration: 0ms +[2026-03-19T19:10:01.901Z] PUT /settings - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:10:01.907Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:10:03.106Z] PUT /settings - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:10:03.116Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:12.949Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 8ms +[2026-03-19T19:11:12.958Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:12.960Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:12.968Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:11:15.437Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:15.444Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:30.862Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 3ms +[2026-03-19T19:11:30.868Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 200 | Duration: 3ms +[2026-03-19T19:11:30.874Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:11:30.880Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T19:11:32.238Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:32.242Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:33.521Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:33.525Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:33.527Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:33.531Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:11:34.107Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:11:34.113Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:13:11.844Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:13:11.846Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:13:11.849Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:13:11.851Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:13:14.174Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:13:14.180Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:13:14.182Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:13:14.186Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:13:47.267Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:13:47.273Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:14:00.012Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:14:00.014Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:14:00.019Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:14:00.025Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T19:14:52.638Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:14:52.644Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:16:21.974Z] POST /trial/apply - | IP: ::1 | Status: 200 | Duration: 4ms +[2026-03-19T19:16:21.984Z] GET /profile - | IP: ::1 | Status: 500 | Duration: 2ms +[2026-03-19T19:16:25.626Z] POST /trial/apply - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:16:25.633Z] GET /profile - | IP: ::1 | Status: 500 | Duration: 0ms +[2026-03-19T19:16:26.327Z] POST /trial/apply - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:16:26.333Z] GET /profile - | IP: ::1 | Status: 500 | Duration: 1ms +[2026-03-19T19:16:27.068Z] POST /trial/apply - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:16:27.078Z] GET /profile - | IP: ::1 | Status: 500 | Duration: 1ms +[2026-03-19T19:16:27.295Z] POST /trial/apply - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:16:27.311Z] GET /profile - | IP: ::1 | Status: 500 | Duration: 1ms +[2026-03-19T19:16:27.616Z] POST /trial/apply - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:16:27.625Z] GET /profile - | IP: ::1 | Status: 500 | Duration: 1ms +[2026-03-19T19:16:30.002Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 3ms +[2026-03-19T19:16:30.010Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:16:30.013Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:16:30.017Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:16:31.520Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-19T19:16:31.530Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:17:57.244Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 5ms +[2026-03-19T19:17:57.250Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:17:57.252Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:17:57.256Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:18:27.681Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:18:27.685Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:18:29.536Z] POST /trial/activate - | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-19T19:18:29.549Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:22:28.666Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 7ms +[2026-03-19T19:22:28.677Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:22:51.599Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-19T19:22:51.601Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:22:51.606Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:51.608Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:52.309Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:52.313Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:22:53.694Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:53.701Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:53.705Z] GET /api-keys - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:22:53.711Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:54.403Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:54.408Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:55.269Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:55.271Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:55.276Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:55.278Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:57.554Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:22:57.559Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:57.562Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:57.568Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:58.238Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:22:58.244Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:58.908Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:58.909Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:58.913Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:58.914Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:22:59.533Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:22:59.538Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:23:02.100Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:23:02.105Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:23:42.202Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:23:42.208Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:23:53.572Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:23:53.579Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:23:53.582Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:23:53.587Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:24:07.552Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:24:07.557Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:24:18.611Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:24:18.620Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:24:34.398Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:24:34.400Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:24:34.406Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:24:34.412Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T19:24:36.175Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:24:36.184Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:25:46.355Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:25:46.357Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:25:46.359Z] GET /status - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:25:46.360Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:25:46.362Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:25:47.879Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:25:47.885Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:26:35.524Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:26:35.529Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:26:35.532Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:26:35.538Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:26:55.775Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:26:55.785Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:05.367Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:05.372Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:17.582Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:17.584Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:17.589Z] GET /logs {"limit":"50"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:17.592Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:27:18.780Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:18.786Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:27.807Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:27.813Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:27.816Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:27.821Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:31.840Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:31.848Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:27:38.793Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:39:24.803Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 4ms +[2026-03-19T19:40:50.808Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T19:44:14.602Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-19T19:44:14.608Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:14.611Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:14.617Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:19.724Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:44:19.728Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:32.693Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:44:32.695Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:32.699Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:32.701Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:36.244Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:44:36.246Z] GET /billing/payments - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:44:36.251Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:44:36.256Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:36.878Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 4ms +[2026-03-19T19:46:36.885Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:40.140Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:40.142Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:40.148Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:40.150Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:40.771Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:40.777Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:41.573Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:41.575Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:41.581Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:46:41.583Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:46:42.305Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:46:42.310Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:46:49.997Z] POST /test-otp - | IP: ::1 | Status: 503 | Duration: 1ms +[2026-03-19T19:46:52.222Z] POST /test-otp - | IP: ::1 | Status: 503 | Duration: 1ms +[2026-03-19T19:46:52.359Z] POST /test-otp - | IP: ::1 | Status: 503 | Duration: 1ms +[2026-03-19T19:46:52.540Z] POST /test-otp - | IP: ::1 | Status: 503 | Duration: 1ms +[2026-03-19T19:47:25.951Z] POST /test-otp - | IP: ::1 | Status: 200 | Duration: 877ms +[2026-03-19T19:47:33.805Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 4ms +[2026-03-19T19:47:33.807Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:47:33.809Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:47:33.811Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:47:33.813Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:47:33.816Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:47:33.817Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:47:33.819Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:48:45.818Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:45.824Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:46.366Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:46.367Z] GET /billing/config - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:48:46.368Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:46.370Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:46.371Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:46.372Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:47.119Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:47.121Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:48:47.124Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:47.127Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:49.137Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:49.143Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:50.939Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:50.941Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:50.942Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:50.946Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:50.948Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:50.951Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:54.672Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:48:54.675Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:54.679Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:54.682Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:55.279Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:55.285Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:48:58.636Z] PUT /settings - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:48:58.651Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:48:58.654Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:48:58.659Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:48:58.664Z] GET /status - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:49:09.900Z] POST /test-otp - | IP: ::1 | Status: 200 | Duration: 1195ms +[2026-03-19T19:49:17.406Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:49:17.408Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:17.409Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:17.415Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:17.417Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:17.421Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:19.070Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:19.071Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:19.073Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:19.075Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:19.077Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:19.080Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:36.204Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:49:36.207Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:49:36.214Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:49:36.219Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:40.933Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:40.938Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:42.838Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:42.845Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:49:42.847Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:42.853Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:43.791Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:43.793Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:43.798Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:43.800Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:44.437Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:44.440Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:49:51.296Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:49:51.303Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:50:34.068Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:50:34.070Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:50:34.073Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:50:34.075Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:50:36.289Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:50:36.293Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:50:36.297Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:50:36.301Z] GET /status - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:50:36.308Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T19:50:36.312Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:50:36.318Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T19:50:36.321Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:50:36.330Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 4ms +[2026-03-19T19:50:36.338Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:51:16.352Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:51:16.357Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:51:17.243Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:17.251Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:17.256Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:17.264Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:51:18.603Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:18.609Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:19.806Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:19.814Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:19.818Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:51:19.825Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:20.336Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:51:20.341Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:24.988Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:24.989Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:24.990Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:51:24.995Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:51:24.997Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:25.000Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:51:51.084Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:51.088Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:53.531Z] PUT /settings - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:51:53.540Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:51:53.541Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:51:53.543Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:51:53.545Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:56.615Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:56.616Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:51:56.618Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:51:56.623Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:51:56.625Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:51:56.628Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:52:23.966Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:52:23.969Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:52:23.974Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:52:23.977Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:52:39.104Z] POST /test-otp - | IP: ::1 | Status: 200 | Duration: 891ms +[2026-03-19T19:53:03.950Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:53:03.952Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:03.953Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:03.956Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:03.957Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:03.960Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:06.871Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:53:06.873Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:53:06.877Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:06.879Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:07.787Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:07.789Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:07.791Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:07.794Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:07.796Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:07.800Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:08.437Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:08.444Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:11.373Z] PUT /settings - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:53:11.382Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:53:11.383Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:53:11.386Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-19T19:53:11.388Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:12.934Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:12.935Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:12.937Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:12.941Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:12.943Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:12.945Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:13.789Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:13.795Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:16.976Z] POST /test-otp - | IP: ::1 | Status: 403 | Duration: 1ms +[2026-03-19T19:53:29.302Z] POST /test-otp - | IP: ::1 | Status: 200 | Duration: 1824ms +[2026-03-19T19:53:32.150Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:53:32.152Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:53:32.158Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:32.162Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:32.808Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-19T19:53:32.810Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:32.814Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:32.817Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:32.820Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:32.821Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:34.869Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:34.870Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:34.875Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:34.877Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:52.083Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:52.085Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:52.086Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:52.089Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:52.091Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:52.092Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:53.715Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:53.719Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:54.471Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:53:54.473Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:54.479Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:53:54.481Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:55.186Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:55.191Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:53:55.883Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:53:55.887Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:46.141Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:46.142Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:54:46.143Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:54:46.148Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:46.150Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:54:46.151Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:54:46.867Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:46.872Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:49.706Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:54:49.713Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:49.716Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:49.723Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:54:50.704Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:54:50.711Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T19:55:14.720Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:14.725Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:55:15.398Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:15.403Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:15.408Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:55:15.414Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:55:16.177Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:55:16.188Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:35.167Z] GET /activity {"page":"1","page_size":"25"} | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:55:40.384Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:40.388Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:55:46.262Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:46.270Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:55:46.276Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:46.284Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:55:46.870Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:55:46.872Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:46.878Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:55:46.880Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:55:56.053Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:56.057Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:56.059Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:55:56.064Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:55:56.068Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:56.070Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:59.099Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:55:59.110Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:07.663Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:07.669Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:11.347Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:11.348Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:11.349Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:11.353Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:11.355Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:11.357Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:11.898Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:11.899Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:11.903Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:11.905Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:14.367Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:14.372Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:43.245Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:43.250Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:43.252Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:43.258Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:44.399Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:44.405Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:45.030Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:45.034Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:45.038Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:45.045Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:46.195Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:46.199Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:47.213Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:47.215Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:47.221Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:56:47.224Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:47.813Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:47.814Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:47.815Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:47.818Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:47.821Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:47.824Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:48.906Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:48.911Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:50.098Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:50.100Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:50.104Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:50.108Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:56:50.647Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:50.654Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:50.658Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:50.664Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:51.230Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:51.234Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:51.900Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:51.906Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:51.908Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:51.914Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:52.550Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:52.555Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:53.996Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:53.997Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:54.002Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:56:54.004Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:54.632Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:54.634Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:54.635Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:54.639Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:56:54.641Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:56:54.643Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.645Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.647Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.649Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.651Z] GET /status - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T19:59:29.654Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.656Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.659Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.661Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.662Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T19:59:29.664Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:29.667Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:59:29.677Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T19:59:31.241Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:31.246Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:59:35.240Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:59:35.243Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:35.245Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:35.248Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:35.250Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:35.252Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:47.595Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T19:59:47.602Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:47.605Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:47.610Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:48.180Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T19:59:48.187Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:17:26.076Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 3ms +[2026-03-19T20:17:26.082Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:17:26.085Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:17:26.091Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:17:29.050Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:17:29.055Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:10.923Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:18:10.927Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:18:10.932Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:18:10.936Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:13.293Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:13.295Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:13.296Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:13.300Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:13.305Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:13.307Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:22.338Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:22.341Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:22.347Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:22.351Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:27.582Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:27.584Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:27.585Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:27.589Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:27.593Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:27.595Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:29.141Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:29.145Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:37.849Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:37.850Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:37.852Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:37.854Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:37.856Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:37.857Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:38.478Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:38.479Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:38.486Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:38.488Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:39.277Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:39.283Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:40.194Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:40.199Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:40.201Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:18:40.209Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:18:46.707Z] POST /api-key - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:18:46.711Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:18:46.718Z] GET /api-keys - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:20:09.200Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:20:09.202Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:09.207Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:20:09.208Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:09.790Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:09.794Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:29.264Z] PUT /settings - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:20:29.274Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:20:29.276Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:20:29.277Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:29.278Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T20:20:29.280Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:29.479Z] PUT /settings - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:20:29.491Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:29.494Z] GET /summary - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:20:29.495Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T20:20:29.498Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:20:29.501Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:20:32.621Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:32.623Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:32.629Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:32.631Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:34.153Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:34.155Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:34.158Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:34.160Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:34.163Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:34.166Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:37.401Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:37.405Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:39.873Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:39.875Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:39.878Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:39.880Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:41.816Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:41.818Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:41.820Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:41.824Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:41.826Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:41.827Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:42.432Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:42.438Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:43.052Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:20:43.058Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:20:43.060Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:20:43.068Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:01.427Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:01.435Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:01.438Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T20:21:01.440Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:01.442Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:01.447Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 3ms +[2026-03-19T20:21:01.450Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:01.452Z] GET /status - | IP: ::1 | Status: 200 | Duration: 0ms +[2026-03-19T20:21:01.454Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:01.457Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:21:01.459Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:01.466Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:04.275Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:04.282Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:04.875Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:04.880Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:04.882Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:04.889Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:20.178Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:20.187Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:20.818Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:20.827Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:20.833Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:20.841Z] GET /api-keys - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:21.466Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:21.469Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:21.473Z] GET /logs {"page":"1","page_size":"20"} | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:21.475Z] GET /summary - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:48.025Z] GET /profile - | IP: ::1 | Status: 200 | Duration: 4ms +[2026-03-19T20:21:48.032Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:49.002Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 2ms +[2026-03-19T20:21:49.005Z] GET /billing/payments - | IP: ::1 | Status: 200 | Duration: 2ms +[2026-03-19T20:21:49.008Z] GET /billing/config - | IP: ::1 | Status: 200 | Duration: 1ms +[2026-03-19T20:21:49.010Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:49.013Z] GET /billing/payments - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:49.015Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:21:49.583Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:21:49.585Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms +[2026-03-19T20:21:49.590Z] GET /profile - | IP: ::1 | Status: 304 | Duration: 1ms +[2026-03-19T20:21:49.592Z] GET /billing/config - | IP: ::1 | Status: 304 | Duration: 0ms diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1802f7f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3294 @@ +{ + "name": "whats-otp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "whats-otp", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcrypt": "^6.0.0", + "better-sqlite3": "^12.6.2", + "cors": "^2.8.6", + "express": "^5.2.1", + "express-rate-limit": "^8.3.0", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", + "puppeteer": "^24.38.0", + "qrcode-terminal": "^0.12.0", + "whatsapp-web.js": "^1.34.6" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@pedroslopez/moduleraid": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@pedroslopez/moduleraid/-/moduleraid-5.0.2.tgz", + "integrity": "sha512-wtnBAETBVYZ9GvcbgdswRVSLkFkYAGv1KzwBBTeRXvGT9sb9cPllOgFFWXCn9PyARQ0H+Ijz6mmoRrGateUDxQ==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "optional": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "optional": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT", + "optional": true + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", + "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/better-sqlite3": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT", + "optional": true + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "optional": true, + "engines": { + "node": ">=0.2.0" + } + }, + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "optional": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT", + "optional": true + }, + "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/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "optional": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, + "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/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "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/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "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.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fluent-ffmpeg/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "ISC", + "optional": true + }, + "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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "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==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "optional": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "optional": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC", + "optional": true + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT", + "optional": true + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-webpmux": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/node-webpmux/-/node-webpmux-3.1.7.tgz", + "integrity": "sha512-ySkL4lBCto86OyQ0blAGzylWSECcn5I0lM3bYEhe75T8Zxt/BFUMHa8ktUguR7zwXNdS/Hms31VfSsYKN1383g==", + "license": "ISC" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "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==", + "license": "ISC" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT", + "optional": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.38.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.38.0.tgz", + "integrity": "sha512-abnJOBVoL9PQTLKSbYGm9mjNFyIPaTVj77J/6cS370dIQtcZMpx8wyZoAuBzR71Aoon6yvI71NEVFUsl3JU82g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.38.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.38.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.38.0.tgz", + "integrity": "sha512-zB3S/tksIhgi2gZRndUe07AudBz5SXOB7hqG0kEa9/YXWrGwlVlYm3tZtwKgfRftBzbmLQl5iwHkQQl04n/mWw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT", + "optional": true + }, + "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.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "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/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "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/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT", + "optional": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "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/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "optional": true, + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "optional": true + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatsapp-web.js": { + "version": "1.34.6", + "resolved": "https://registry.npmjs.org/whatsapp-web.js/-/whatsapp-web.js-1.34.6.tgz", + "integrity": "sha512-+zgLBqARcVfuCG7b80c7Gkt+4Yh8w+oDWx7lL2gTA6nlaykHBne7NwJ5yGe2r7O9IYraIzs6HiCzNGKfu9AUBg==", + "license": "Apache-2.0", + "dependencies": { + "@pedroslopez/moduleraid": "^5.0.2", + "fluent-ffmpeg": "2.1.3", + "mime": "^3.0.0", + "node-fetch": "^2.6.9", + "node-webpmux": "3.1.7", + "puppeteer": "^24.31.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "archiver": "^5.3.1", + "fs-extra": "^10.1.0", + "unzipper": "^0.10.11" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "optional": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7dfccce --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "veriflo-backend", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "commonjs", + "scripts": { + "start": "node index.js", + "dev": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "engines": { + "node": ">=20 <24" + }, + "dependencies": { + "bcrypt": "^6.0.0", + "better-sqlite3": "^12.6.2", + "cors": "^2.8.6", + "express": "^5.2.1", + "express-rate-limit": "^8.3.0", + "helmet": "^8.1.0", + "jsonwebtoken": "^9.0.3", + "puppeteer": "^24.38.0", + "qrcode-terminal": "^0.12.0", + "whatsapp-web.js": "^1.34.6" + } +} \ No newline at end of file diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..8b251f9 --- /dev/null +++ b/src/db.js @@ -0,0 +1,249 @@ +const Database = require('better-sqlite3'); +const path = require('path'); +const bcrypt = require('bcrypt'); + +// Initialize DB (creates database.sqlite at project root if it doesn't exist) +const dbPath = path.join(__dirname, '..', 'database.sqlite'); +const db = new Database(dbPath, { + // verbose: console.log +}); + +// Enable WAL mode for better concurrency and performance +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +// Initialize Schema +function initSchema() { + console.log('Initializing SQLite Schema...'); + + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + company TEXT, + email TEXT UNIQUE NOT NULL, + phone TEXT UNIQUE, + password_hash TEXT NOT NULL, + role TEXT DEFAULT 'user', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT NOT NULL DEFAULT 'Default Key', + key_hash TEXT NOT NULL, + prefix TEXT NOT NULL, + mode TEXT DEFAULT 'sandbox', + last_used_ip TEXT, + last_used_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS settings ( + user_id INTEGER PRIMARY KEY, + sender_name TEXT DEFAULT '', + greeting TEXT DEFAULT '', + message_template TEXT DEFAULT '', + otp_length INTEGER DEFAULT 6, + expiry_seconds INTEGER DEFAULT 300, + environment_mode TEXT DEFAULT 'sandbox', + trial_status TEXT DEFAULT 'not_applied', + trial_applied_at DATETIME, + trial_activated_at DATETIME, + trial_bonus_credits REAL DEFAULT 0, + live_bonus_credits REAL DEFAULT 0, + webhook_url TEXT DEFAULT '', + return_otp_in_response BOOLEAN DEFAULT FALSE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS otp_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id TEXT UNIQUE NOT NULL, + user_id INTEGER NOT NULL, + api_key_id INTEGER, + phone TEXT NOT NULL, + otp_hash TEXT NOT NULL, + status TEXT DEFAULT 'pending', + expires_at DATETIME NOT NULL, + ip_address TEXT, + user_agent TEXT, + delivery_latency_ms INTEGER, + quota_units REAL DEFAULT 1, + environment_mode TEXT DEFAULT 'sandbox', + quota_mode TEXT DEFAULT 'sandbox', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (api_key_id) REFERENCES api_keys (id) ON DELETE SET NULL + ); + + CREATE TABLE IF NOT EXISTS signup_otps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id TEXT UNIQUE NOT NULL, + phone TEXT NOT NULL, + otp_hash TEXT NOT NULL, + name TEXT, + company TEXT, + email TEXT, + password_hash TEXT, + status TEXT DEFAULT 'sent', + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS password_reset_otps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_id TEXT UNIQUE NOT NULL, + user_id INTEGER NOT NULL, + phone TEXT NOT NULL, + email TEXT NOT NULL, + otp_hash TEXT NOT NULL, + status TEXT DEFAULT 'sent', + expires_at DATETIME NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS activity_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + type TEXT NOT NULL, + title TEXT NOT NULL, + meta TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS payment_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + request_ref TEXT UNIQUE NOT NULL, + user_id INTEGER NOT NULL, + package_name TEXT, + credits INTEGER NOT NULL, + amount_inr REAL NOT NULL, + utr TEXT, + upi_link TEXT, + status TEXT DEFAULT 'pending', + admin_note TEXT, + approved_by INTEGER, + approved_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (approved_by) REFERENCES users (id) ON DELETE SET NULL + ); + + CREATE TABLE IF NOT EXISTS payment_config ( + id INTEGER PRIMARY KEY CHECK (id = 1), + payments_enabled INTEGER DEFAULT 1, + upi_id TEXT DEFAULT '', + upi_name TEXT DEFAULT 'Veriflo', + payment_note TEXT DEFAULT '', + updated_by INTEGER, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (updated_by) REFERENCES users (id) ON DELETE SET NULL + ); + `); + + const userColumns = db.prepare("PRAGMA table_info(users)").all(); + const hasPhoneColumn = userColumns.some((column) => column.name === 'phone'); + if (!hasPhoneColumn) { + db.exec('ALTER TABLE users ADD COLUMN phone TEXT'); + } + + const settingsColumns = db.prepare("PRAGMA table_info(settings)").all(); + const hasEnvironmentModeColumn = settingsColumns.some((column) => column.name === 'environment_mode'); + if (!hasEnvironmentModeColumn) { + db.exec("ALTER TABLE settings ADD COLUMN environment_mode TEXT DEFAULT 'sandbox'"); + } + if (!settingsColumns.some((column) => column.name === 'message_template')) { + db.exec("ALTER TABLE settings ADD COLUMN message_template TEXT DEFAULT ''"); + } + if (!settingsColumns.some((column) => column.name === 'trial_status')) { + db.exec("ALTER TABLE settings ADD COLUMN trial_status TEXT DEFAULT 'not_applied'"); + } + if (!settingsColumns.some((column) => column.name === 'trial_applied_at')) { + db.exec("ALTER TABLE settings ADD COLUMN trial_applied_at DATETIME"); + } + if (!settingsColumns.some((column) => column.name === 'trial_activated_at')) { + db.exec("ALTER TABLE settings ADD COLUMN trial_activated_at DATETIME"); + } + if (!settingsColumns.some((column) => column.name === 'trial_bonus_credits')) { + db.exec("ALTER TABLE settings ADD COLUMN trial_bonus_credits REAL DEFAULT 0"); + } + if (!settingsColumns.some((column) => column.name === 'live_bonus_credits')) { + db.exec("ALTER TABLE settings ADD COLUMN live_bonus_credits REAL DEFAULT 0"); + } + + const apiKeyColumns = db.prepare("PRAGMA table_info(api_keys)").all(); + if (!apiKeyColumns.some((column) => column.name === 'name')) { + db.exec("ALTER TABLE api_keys ADD COLUMN name TEXT NOT NULL DEFAULT 'Default Key'"); + } + if (!apiKeyColumns.some((column) => column.name === 'last_used_at')) { + db.exec("ALTER TABLE api_keys ADD COLUMN last_used_at DATETIME"); + } + if (!apiKeyColumns.some((column) => column.name === 'mode')) { + db.exec("ALTER TABLE api_keys ADD COLUMN mode TEXT DEFAULT 'sandbox'"); + } + + const otpLogColumns = db.prepare("PRAGMA table_info(otp_logs)").all(); + if (!otpLogColumns.some((column) => column.name === 'api_key_id')) { + db.exec("ALTER TABLE otp_logs ADD COLUMN api_key_id INTEGER"); + } if (!otpLogColumns.some((column) => column.name === 'failed_attempts')) { + db.exec('ALTER TABLE otp_logs ADD COLUMN failed_attempts INTEGER DEFAULT 0'); + } + if (!otpLogColumns.some((column) => column.name === 'resend_of')) { + db.exec('ALTER TABLE otp_logs ADD COLUMN resend_of TEXT'); + } + if (!otpLogColumns.some((column) => column.name === 'quota_units')) { + db.exec('ALTER TABLE otp_logs ADD COLUMN quota_units REAL DEFAULT 1'); + } + if (!otpLogColumns.some((column) => column.name === 'environment_mode')) { + db.exec("ALTER TABLE otp_logs ADD COLUMN environment_mode TEXT DEFAULT 'sandbox'"); + } + if (!otpLogColumns.some((column) => column.name === 'quota_mode')) { + db.exec("ALTER TABLE otp_logs ADD COLUMN quota_mode TEXT DEFAULT 'sandbox'"); + db.exec("UPDATE otp_logs SET quota_mode = COALESCE(environment_mode, 'sandbox') WHERE quota_mode IS NULL OR quota_mode = ''"); + } + db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_phone_unique ON users(phone)'); + db.exec('CREATE INDEX IF NOT EXISTS idx_payment_requests_user ON payment_requests(user_id)'); + db.exec('CREATE INDEX IF NOT EXISTS idx_payment_requests_status ON payment_requests(status)'); + + const existingPaymentConfig = db.prepare('SELECT id FROM payment_config WHERE id = 1').get(); + if (!existingPaymentConfig) { + db.prepare(` + INSERT INTO payment_config (id, payments_enabled, upi_id, upi_name, payment_note) + VALUES (1, 1, ?, ?, ?) + `).run( + (process.env.VERTIFLO_UPI_ID || '').trim(), + process.env.VERTIFLO_UPI_NAME || 'Veriflo', + 'Pay via UPI and submit UTR for admin approval.' + ); + } + + const superAdminEmail = (process.env.VERTIFLO_SUPERADMIN_EMAIL || 'superadmin@veriflo.local').trim().toLowerCase(); + const superAdminPassword = process.env.VERTIFLO_SUPERADMIN_PASSWORD || 'SuperAdmin@123'; + const superAdminName = process.env.VERTIFLO_SUPERADMIN_NAME || 'Super Admin'; + const existingAdmin = db.prepare('SELECT id FROM users WHERE email = ?').get(superAdminEmail); + if (!existingAdmin) { + const hash = bcrypt.hashSync(superAdminPassword, 10); + const insert = db.prepare(` + INSERT INTO users (name, company, email, password_hash, role) + VALUES (?, ?, ?, ?, 'admin') + `).run(superAdminName, 'Veriflo', superAdminEmail, hash); + db.prepare(` + INSERT INTO settings (user_id, sender_name, greeting, message_template, trial_status, trial_activated_at) + VALUES (?, ?, ?, ?, 'active', CURRENT_TIMESTAMP) + `).run(insert.lastInsertRowid, 'Veriflo', 'Hello!', 'Your OTP is {otp}.'); + console.log(`Seeded super admin account: ${superAdminEmail}`); + } + + console.log('SQLite Schema Ready.'); +} + +module.exports = { + db, + initSchema +}; diff --git a/src/middlewares/apiKey.js b/src/middlewares/apiKey.js new file mode 100644 index 0000000..227babd --- /dev/null +++ b/src/middlewares/apiKey.js @@ -0,0 +1,68 @@ +const crypto = require('crypto'); +const { db } = require('../db'); + +function requireApiKey(req, res, next) { + // Accept key from: Authorization header, request body, or query string + let plainApiKey = null; + + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + plainApiKey = authHeader.split(' ')[1]; + } else if (req.body?.api_key) { + plainApiKey = String(req.body.api_key); + } else if (req.query?.api_key) { + plainApiKey = String(req.query.api_key); + } + + if (!plainApiKey) { + return res.status(401).json({ error: 'Unauthorized: API key required. Pass it via Authorization header (Bearer ), request body (api_key), or query param (?api_key=).' }); + } + + try { + // Hash the incoming key to compare with the database + const keyHash = crypto.createHash('sha256').update(plainApiKey).digest('hex'); + + // Find the associated user and key record + const record = db.prepare(` + SELECT ak.id as key_id, ak.name as key_name, ak.mode as key_mode, u.id as user_id, u.company, u.name + FROM api_keys ak + JOIN users u ON ak.user_id = u.id + WHERE ak.key_hash = ? + `).get(keyHash); + + if (!record) { + return res.status(401).json({ error: 'Unauthorized: Invalid API Key' }); + } + + // Advanced request analytics capture + const requestIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; + const userAgent = req.headers['user-agent'] || 'Unknown'; + + // Update last_used_ip on the API key non-blockingly + try { + db.prepare('UPDATE api_keys SET last_used_ip = ?, last_used_at = CURRENT_TIMESTAMP WHERE id = ?').run(requestIp, record.key_id); + } catch (e) { + console.error("Failed to update last_used_ip", e); + } + + // Attach verified user context to the request for the controller + req.apiKeyUser = { + id: record.user_id, + key_id: record.key_id, + key_name: record.key_name, + key_mode: record.key_mode || 'sandbox', + company: record.company, + name: record.name, + ip: requestIp, + ua: userAgent + }; + + next(); + + } catch (err) { + console.error('API Key Verification Error:', err); + return res.status(500).json({ error: 'Internal Server Error during authentication' }); + } +} + +module.exports = { requireApiKey }; diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js new file mode 100644 index 0000000..4bfd0de --- /dev/null +++ b/src/middlewares/auth.js @@ -0,0 +1,35 @@ +const jwt = require('jsonwebtoken'); + +// Hardcoded for development, should be env var in production +const JWT_SECRET = process.env.JWT_SECRET || 'vertiflo_super_secret_key_2026'; + +function generateToken(user, expiresIn = '7d') { + return jwt.sign( + { id: user.id, email: user.email, role: user.role }, + JWT_SECRET, + { expiresIn } + ); +} + +function verifyToken(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Unauthorized: No token provided' }); + } + + const token = authHeader.split(' ')[1]; + + try { + const decoded = jwt.verify(token, JWT_SECRET); + req.user = decoded; // Attach user payload to request + next(); + } catch (err) { + return res.status(401).json({ error: 'Unauthorized: Invalid or expired token' }); + } +} + +module.exports = { + JWT_SECRET, + generateToken, + verifyToken +}; diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000..623196c --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,400 @@ +const express = require('express'); +const { db } = require('../db'); +const { verifyToken } = require('../middlewares/auth'); +const { getSandboxMonthlyMessageLimit } = require('../utils/sandbox'); + +const router = express.Router(); + +// Super Admin Protection Middleware +function requireSuperAdmin(req, res, next) { + if (req.user.role !== 'admin') { + return res.status(403).json({ error: 'Forbidden: Super Admin privileges required.' }); + } + next(); +} + +// ========================================== +// GET /api/admin/dashboard +// Global Platform Stats +// ========================================== +router.get('/dashboard', verifyToken, requireSuperAdmin, (req, res) => { + try { + // Platform Totals + const totalUsers = db.prepare("SELECT COUNT(*) as count FROM users").get().count; + const totalOtps = db.prepare("SELECT COUNT(*) as count FROM otp_logs").get().count; + const failedOtps = db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE status='failed'").get().count; + + // Last 30 Days Global Usage + const thirtyDaysUsage = db.prepare(` + SELECT + date(created_at) as date, + COUNT(*) as volume + FROM otp_logs + WHERE created_at >= date('now', '-30 days') + GROUP BY date(created_at) + ORDER BY date(created_at) ASC + `).all(); + + res.json({ + kpis: { + total_registered_users: totalUsers, + total_platform_otps: totalOtps, + global_failed_otps: failedOtps + }, + chart_data: thirtyDaysUsage + }); + + } catch (err) { + console.error('Admin Dashboard Error:', err); + res.status(500).json({ error: 'Failed to fetch admin dashboard stats' }); + } +}); + +// ========================================== +// GET /api/admin/users +// User Management List +// ========================================== +router.get('/users', verifyToken, requireSuperAdmin, (req, res) => { + try { + const baseTrialLimit = getSandboxMonthlyMessageLimit(); + + // Join users with their total OTP count + const usersList = db.prepare(` + SELECT + u.id, u.name, u.company, u.email, u.role, u.created_at, + COALESCE(s.trial_status, 'not_applied') as trial_status, + COALESCE(s.trial_bonus_credits, 0) as trial_bonus_credits, + (SELECT COUNT(*) FROM otp_logs o WHERE o.user_id = u.id) as otps_sent, + ( + SELECT COALESCE(SUM(COALESCE(o.quota_units, 1)), 0) + FROM otp_logs o + WHERE o.user_id = u.id + AND COALESCE(o.quota_mode, o.environment_mode, 'sandbox') = 'sandbox' + AND o.created_at >= datetime('now', 'start of month') + ) as sandbox_used_this_month + FROM users u + LEFT JOIN settings s ON s.user_id = u.id + ORDER BY u.created_at DESC + `).all(); + + res.json({ + users: usersList.map((user) => { + const bonus = Number(user.trial_bonus_credits || 0); + const used = Number(user.sandbox_used_this_month || 0); + const total = Number((baseTrialLimit + bonus).toFixed(2)); + return { + ...user, + trial_base_credits: baseTrialLimit, + trial_total_credits: total, + trial_remaining_credits: Number(Math.max(0, total - used).toFixed(2)), + }; + }) + }); + + } catch (err) { + console.error('Admin Users Error:', err); + res.status(500).json({ error: 'Failed to fetch user list' }); + } +}); + +// ========================================== +// POST /api/admin/users/:id/trial-credits +// Increase free trial credits for a user +// ========================================== +router.post('/users/:id/trial-credits', verifyToken, requireSuperAdmin, (req, res) => { + try { + const userId = Number(req.params.id); + const increment = Number(req.body?.credits); + + if (!Number.isInteger(userId) || userId <= 0) { + return res.status(400).json({ error: 'Invalid user id' }); + } + + if (!Number.isFinite(increment) || increment <= 0) { + return res.status(400).json({ error: 'credits must be a positive number' }); + } + + const user = db.prepare('SELECT id FROM users WHERE id = ?').get(userId); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + db.prepare(` + UPDATE settings + SET trial_bonus_credits = COALESCE(trial_bonus_credits, 0) + ? + WHERE user_id = ? + `).run(increment, userId); + + const updated = db.prepare('SELECT COALESCE(trial_bonus_credits, 0) as trial_bonus_credits FROM settings WHERE user_id = ?').get(userId); + const base = getSandboxMonthlyMessageLimit(); + const bonus = Number(updated?.trial_bonus_credits || 0); + + return res.json({ + success: true, + message: `Added ${increment} trial credits successfully`, + user_id: userId, + trial_base_credits: base, + trial_bonus_credits: Number(bonus.toFixed(2)), + trial_total_credits: Number((base + bonus).toFixed(2)) + }); + } catch (err) { + console.error('Admin Trial Credits Error:', err); + res.status(500).json({ error: 'Failed to update trial credits' }); + } +}); + +// ========================================== +// GET /api/admin/payment-config +// Payment methods configuration +// ========================================== +router.get('/payment-config', verifyToken, requireSuperAdmin, (req, res) => { + try { + const config = db.prepare(` + SELECT + payments_enabled, + upi_id, + upi_name, + payment_note, + updated_at + FROM payment_config + WHERE id = 1 + `).get(); + + if (!config) { + return res.status(404).json({ error: 'Payment configuration not found' }); + } + + return res.json({ + config: { + payments_enabled: Number(config.payments_enabled || 0) === 1, + upi_id: config.upi_id || '', + upi_name: config.upi_name || 'Veriflo', + payment_note: config.payment_note || '', + updated_at: config.updated_at || null, + } + }); + } catch (err) { + console.error('Admin Payment Config Fetch Error:', err); + return res.status(500).json({ error: 'Failed to fetch payment config' }); + } +}); + +// ========================================== +// PUT /api/admin/payment-config +// Update payment methods configuration +// ========================================== +router.put('/payment-config', verifyToken, requireSuperAdmin, (req, res) => { + try { + const paymentsEnabled = req.body?.payments_enabled === true ? 1 : 0; + const upiId = String(req.body?.upi_id || '').trim(); + const upiName = String(req.body?.upi_name || 'Veriflo').trim(); + const paymentNote = String(req.body?.payment_note || '').trim(); + + if (paymentsEnabled === 1 && !upiId) { + return res.status(400).json({ error: 'UPI ID is required when payments are enabled' }); + } + + db.prepare(` + UPDATE payment_config + SET payments_enabled = ?, + upi_id = ?, + upi_name = ?, + payment_note = ?, + updated_by = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = 1 + `).run(paymentsEnabled, upiId, upiName || 'Veriflo', paymentNote, req.user.id); + + return res.json({ + success: true, + message: 'Payment configuration saved', + config: { + payments_enabled: paymentsEnabled === 1, + upi_id: upiId, + upi_name: upiName || 'Veriflo', + payment_note: paymentNote, + } + }); + } catch (err) { + console.error('Admin Payment Config Update Error:', err); + return res.status(500).json({ error: 'Failed to update payment config' }); + } +}); + +// ========================================== +// GET /api/admin/payments +// Payment approvals queue +// ========================================== +router.get('/payments', verifyToken, requireSuperAdmin, (req, res) => { + try { + const status = String(req.query.status || 'all').trim().toLowerCase(); + const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 100, 10), 300); + + let rows; + if (status === 'all') { + rows = db.prepare(` + SELECT + p.id, + p.request_ref, + p.package_name, + p.credits, + p.amount_inr, + p.utr, + p.status, + p.admin_note, + p.created_at, + p.updated_at, + p.approved_at, + u.id as user_id, + u.name as user_name, + u.email as user_email + FROM payment_requests p + JOIN users u ON u.id = p.user_id + ORDER BY p.created_at DESC + LIMIT ? + `).all(limit); + } else { + rows = db.prepare(` + SELECT + p.id, + p.request_ref, + p.package_name, + p.credits, + p.amount_inr, + p.utr, + p.status, + p.admin_note, + p.created_at, + p.updated_at, + p.approved_at, + u.id as user_id, + u.name as user_name, + u.email as user_email + FROM payment_requests p + JOIN users u ON u.id = p.user_id + WHERE p.status = ? + ORDER BY p.created_at DESC + LIMIT ? + `).all(status, limit); + } + + res.json({ payments: rows }); + } catch (err) { + console.error('Admin Payments Error:', err); + res.status(500).json({ error: 'Failed to fetch payments' }); + } +}); + +// ========================================== +// POST /api/admin/payments/:id/approve +// Approve payment and credit live bonus bucket +// ========================================== +router.post('/payments/:id/approve', verifyToken, requireSuperAdmin, (req, res) => { + try { + const id = Number(req.params.id); + const adminNote = String(req.body?.admin_note || '').trim(); + if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: 'Invalid payment id' }); + + const payment = db.prepare('SELECT id, user_id, credits, status FROM payment_requests WHERE id = ?').get(id); + if (!payment) return res.status(404).json({ error: 'Payment not found' }); + if (payment.status === 'approved') return res.status(400).json({ error: 'Payment already approved' }); + if (payment.status === 'rejected') return res.status(400).json({ error: 'Rejected payment cannot be approved' }); + + const tx = db.transaction(() => { + db.prepare(` + UPDATE payment_requests + SET status = 'approved', + admin_note = ?, + approved_by = ?, + approved_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run(adminNote || null, req.user.id, id); + + db.prepare(` + UPDATE settings + SET live_bonus_credits = COALESCE(live_bonus_credits, 0) + ? + WHERE user_id = ? + `).run(payment.credits, payment.user_id); + }); + tx(); + + res.json({ success: true, message: 'Payment approved and credits added' }); + } catch (err) { + console.error('Approve Payment Error:', err); + res.status(500).json({ error: 'Failed to approve payment' }); + } +}); + +// ========================================== +// POST /api/admin/payments/:id/reject +// ========================================== +router.post('/payments/:id/reject', verifyToken, requireSuperAdmin, (req, res) => { + try { + const id = Number(req.params.id); + const adminNote = String(req.body?.admin_note || '').trim(); + if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: 'Invalid payment id' }); + + const payment = db.prepare('SELECT id, status FROM payment_requests WHERE id = ?').get(id); + if (!payment) return res.status(404).json({ error: 'Payment not found' }); + if (payment.status === 'approved') return res.status(400).json({ error: 'Approved payment cannot be rejected' }); + + db.prepare(` + UPDATE payment_requests + SET status = 'rejected', + admin_note = ?, + approved_by = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run(adminNote || null, req.user.id, id); + + res.json({ success: true, message: 'Payment rejected' }); + } catch (err) { + console.error('Reject Payment Error:', err); + res.status(500).json({ error: 'Failed to reject payment' }); + } +}); + +// ========================================== +// GET /api/admin/logs +// Global Trace Viewer +// ========================================== +router.get('/logs', verifyToken, requireSuperAdmin, (req, res) => { + try { + const limit = parseInt(req.query.limit) || 100; + + const logs = db.prepare(` + SELECT + o.request_id, o.phone, o.status, o.ip_address, o.created_at, + u.name as user_name, u.company as user_company + FROM otp_logs o + JOIN users u ON o.user_id = u.id + ORDER BY o.created_at DESC + LIMIT ? + `).all(limit); + + res.json({ logs }); + + } catch (err) { + console.error('Admin Logs Error:', err); + res.status(500).json({ error: 'Failed to fetch global logs' }); + } +}); + +// ========================================== +// GET /api/admin/whatsapp/status +// Check local puppeteer session status +// ========================================== +router.get('/whatsapp/status', verifyToken, requireSuperAdmin, (req, res) => { + try { + res.json({ + connected: req.app.locals.waReady, + uptime: process.uptime(), // seconds + memory: process.memoryUsage() + }); + } catch (err) { + res.status(500).json({ error: 'Failed to fetch system status' }); + } +}); + +module.exports = router; diff --git a/src/routes/analytics.js b/src/routes/analytics.js new file mode 100644 index 0000000..e054303 --- /dev/null +++ b/src/routes/analytics.js @@ -0,0 +1,290 @@ +const express = require('express'); +const { db } = require('../db'); +const { verifyToken } = require('../middlewares/auth'); + +const router = express.Router(); +const OTP_COST_INR = 0.2; + +// ========================================== +// GET /api/user/analytics/summary +// Summary for Dashboard Home & Analytics +// ========================================== +router.get('/summary', verifyToken, (req, res) => { + try { + const userId = req.user.id; + const settings = db.prepare('SELECT environment_mode, trial_status, trial_applied_at, trial_activated_at FROM settings WHERE user_id = ?').get(userId); + const user = db.prepare('SELECT created_at FROM users WHERE id = ?').get(userId); + + // Get lifetime stats + const totalSent = db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND status != 'pending'").get(userId).count; + const totalDelivered = db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND status IN ('sent', 'verified', 'expired')").get(userId).count; + const totalFailed = db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND status = 'failed'").get(userId).count; + const totalVerified = db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND status = 'verified'").get(userId).count; + const totalExpired = db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND status = 'expired'").get(userId).count; + const totalFailedAttempts = db.prepare("SELECT COALESCE(SUM(COALESCE(failed_attempts, 0)), 0) as count FROM otp_logs WHERE user_id = ?").get(userId).count; + const uniqueRecipients = db.prepare("SELECT COUNT(DISTINCT phone) as count FROM otp_logs WHERE user_id = ?").get(userId).count; + + // Delivery rate + const deliveryRate = totalSent > 0 ? ((totalDelivered / totalSent) * 100).toFixed(1) : 0; + const verificationRate = totalSent > 0 ? ((totalVerified / totalSent) * 100).toFixed(1) : 0; + + // Daily breakdown for the last 7 days for the Charts + const dailyStats = db.prepare(` + SELECT + date(created_at) as date, + COUNT(*) as total, + SUM(CASE WHEN status IN ('sent', 'verified', 'expired') THEN 1 ELSE 0 END) as delivered, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed + FROM otp_logs + WHERE user_id = ? AND created_at >= date('now', '-7 days') + GROUP BY date(created_at) + ORDER BY date(created_at) ASC + `).all(userId); + + // Get average latency + const latencyAvg = db.prepare(` + SELECT AVG(delivery_latency_ms) as avg + FROM otp_logs + WHERE user_id = ? AND delivery_latency_ms IS NOT NULL + `).get(userId).avg; + + const latencyWindow = db.prepare(` + SELECT + MIN(delivery_latency_ms) as min, + MAX(delivery_latency_ms) as max, + AVG(delivery_latency_ms) as avg + FROM otp_logs + WHERE user_id = ? AND delivery_latency_ms IS NOT NULL + `).get(userId); + + const recent24h = db.prepare(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN status IN ('sent', 'verified', 'expired') THEN 1 ELSE 0 END) as delivered, + SUM(CASE WHEN status = 'verified' THEN 1 ELSE 0 END) as verified, + SUM(CASE WHEN status IN ('failed', 'failed_attempt') THEN 1 ELSE 0 END) as failed + FROM otp_logs + WHERE user_id = ? AND created_at >= datetime('now', '-1 day') + `).get(userId); + + const recent7d = db.prepare(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN status IN ('sent', 'verified', 'expired') THEN 1 ELSE 0 END) as delivered, + SUM(CASE WHEN status = 'verified' THEN 1 ELSE 0 END) as verified, + SUM(CASE WHEN status IN ('failed', 'failed_attempt') THEN 1 ELSE 0 END) as failed + FROM otp_logs + WHERE user_id = ? AND created_at >= datetime('now', '-7 days') + `).get(userId); + + const recent30d = db.prepare(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN status IN ('sent', 'verified', 'expired') THEN 1 ELSE 0 END) as delivered, + SUM(CASE WHEN status = 'verified' THEN 1 ELSE 0 END) as verified, + SUM(CASE WHEN status IN ('failed', 'failed_attempt') THEN 1 ELSE 0 END) as failed + FROM otp_logs + WHERE user_id = ? AND created_at >= datetime('now', '-30 days') + `).get(userId); + + const topDay = db.prepare(` + SELECT date(created_at) as date, COUNT(*) as total + FROM otp_logs + WHERE user_id = ? + GROUP BY date(created_at) + ORDER BY total DESC, date DESC + LIMIT 1 + `).get(userId); + + const latestRequest = db.prepare(` + SELECT request_id, phone, status, created_at + FROM otp_logs + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT 1 + `).get(userId); + + const activeKeys = db.prepare(` + SELECT COUNT(*) as count + FROM api_keys + WHERE user_id = ? + `).get(userId).count; + + const topKey = db.prepare(` + SELECT ak.name, COUNT(o.id) as total + FROM api_keys ak + LEFT JOIN otp_logs o ON o.api_key_id = ak.id + WHERE ak.user_id = ? + GROUP BY ak.id, ak.name + ORDER BY total DESC, ak.created_at DESC + LIMIT 1 + `).get(userId); + + const recentActivity = db.prepare(` + SELECT type, title, meta, created_at + FROM activity_logs + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT 8 + `).all(userId); + + res.json({ + lifetime: { + total_sent: totalSent, + total_delivered: totalDelivered, + total_failed: totalFailed, + total_verified: totalVerified, + total_expired: totalExpired, + total_failed_attempts: totalFailedAttempts, + unique_recipients: uniqueRecipients, + delivery_rate: `${deliveryRate}%`, + verification_rate: `${verificationRate}%`, + avg_latency_ms: latencyAvg ? Math.round(latencyAvg) : 0 + }, + operational: { + active_keys: activeKeys, + estimated_cost_inr: Number((totalSent * OTP_COST_INR).toFixed(2)), + top_day: topDay || null, + top_key: topKey?.name ? topKey : null, + latest_request: latestRequest || null, + latency: { + min_ms: latencyWindow?.min ? Math.round(latencyWindow.min) : 0, + avg_ms: latencyWindow?.avg ? Math.round(latencyWindow.avg) : 0, + max_ms: latencyWindow?.max ? Math.round(latencyWindow.max) : 0, + } + }, + plan: { + name: (settings?.trial_status || 'not_applied') === 'active' ? 'Free Trial' : 'Trial Required', + mode: settings?.environment_mode || 'sandbox', + label: (settings?.trial_status || 'not_applied') === 'active' + ? `${settings?.environment_mode === 'live' ? 'Live' : 'Sandbox'} / Free Trial` + : 'Free Trial Not Activated', + started_at: user?.created_at || null, + active: (settings?.trial_status || 'not_applied') === 'active', + trial_status: settings?.trial_status || 'not_applied', + trial_applied_at: settings?.trial_applied_at || null, + trial_activated_at: settings?.trial_activated_at || null, + }, + windows: { + last_24h: { + total: recent24h.total || 0, + delivered: recent24h.delivered || 0, + verified: recent24h.verified || 0, + failed: recent24h.failed || 0, + }, + last_7d: { + total: recent7d.total || 0, + delivered: recent7d.delivered || 0, + verified: recent7d.verified || 0, + failed: recent7d.failed || 0, + }, + last_30d: { + total: recent30d.total || 0, + delivered: recent30d.delivered || 0, + verified: recent30d.verified || 0, + failed: recent30d.failed || 0, + } + }, + chart_data: dailyStats, + recent_activity: recentActivity + }); + + } catch (err) { + console.error('Analytics Data Error:', err); + res.status(500).json({ error: 'Failed to aggregate analytics data' }); + } +}); + +// ========================================== +// GET /api/user/analytics/logs +// Raw logs for the table +// ========================================== +router.get('/logs', verifyToken, (req, res) => { + try { + const userId = req.user.id; + const pageSizeRaw = parseInt(req.query.page_size, 10); + const pageRaw = parseInt(req.query.page, 10); + const pageSize = Number.isFinite(pageSizeRaw) ? Math.min(Math.max(pageSizeRaw, 5), 100) : 20; + const page = Number.isFinite(pageRaw) ? Math.max(pageRaw, 1) : 1; + const offset = (page - 1) * pageSize; + + const total = db.prepare(` + SELECT COUNT(*) as count + FROM otp_logs + WHERE user_id = ? + `).get(userId).count; + + const logs = db.prepare(` + SELECT request_id, phone, status, delivery_latency_ms, created_at, expires_at + FROM otp_logs + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT ? + OFFSET ? + `).all(userId, pageSize, offset); + + res.json({ + logs, + pagination: { + total, + page, + page_size: pageSize, + total_pages: Math.max(1, Math.ceil(total / pageSize)), + has_prev: page > 1, + has_next: page * pageSize < total, + } + }); + + } catch (err) { + console.error('Logs fetch Error:', err); + res.status(500).json({ error: 'Failed to fetch logs' }); + } +}); + +// ========================================== +// GET /api/user/analytics/activity +// Activity logs history for overview modal +// ========================================== +router.get('/activity', verifyToken, (req, res) => { + try { + const userId = req.user.id; + const pageSizeRaw = parseInt(req.query.page_size, 10); + const pageRaw = parseInt(req.query.page, 10); + const pageSize = Number.isFinite(pageSizeRaw) ? Math.min(Math.max(pageSizeRaw, 5), 100) : 20; + const page = Number.isFinite(pageRaw) ? Math.max(pageRaw, 1) : 1; + const offset = (page - 1) * pageSize; + + const total = db.prepare(` + SELECT COUNT(*) as count + FROM activity_logs + WHERE user_id = ? + `).get(userId).count; + + const activity = db.prepare(` + SELECT type, title, meta, created_at + FROM activity_logs + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT ? + OFFSET ? + `).all(userId, pageSize, offset); + + res.json({ + activity, + pagination: { + total, + page, + page_size: pageSize, + total_pages: Math.max(1, Math.ceil(total / pageSize)), + has_prev: page > 1, + has_next: page * pageSize < total, + } + }); + + } catch (err) { + console.error('Activity fetch Error:', err); + res.status(500).json({ error: 'Failed to fetch activity logs' }); + } +}); + +module.exports = router; diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..8d00c41 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,375 @@ +const express = require('express'); +const bcrypt = require('bcrypt'); +const crypto = require('crypto'); +const rateLimit = require('express-rate-limit'); +const { db } = require('../db'); +const { generateToken } = require('../middlewares/auth'); +const { logActivity } = require('../utils/activity'); +const { DEFAULT_TEMPLATE } = require('../utils/messageTemplate'); +const { resolveWhatsAppChatId } = require('../utils/whatsapp'); + +const router = express.Router(); + +const signupOtpLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 5, + message: { error: 'Too many signup OTP requests. Please wait a minute and try again.' } +}); + +const passwordResetLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 5, + message: { error: 'Too many password reset OTP requests. Please wait a minute and try again.' } +}); + +function sanitizePhone(phone) { + return String(phone || '').replace(/\D/g, ''); +} + +function normalizeEmail(email) { + return String(email || '').trim().toLowerCase(); +} + +function generateOTP(length = 6) { + const digits = '0123456789'; + let otp = ''; + for (let i = 0; i < length; i++) { + otp += digits[crypto.randomInt(0, 10)]; + } + return otp; +} + +router.post('/signup/send-otp', signupOtpLimiter, async (req, res) => { + const waReady = req.app.locals.waReady; + const waClient = req.app.locals.waClient; + const sanitizedPhone = sanitizePhone(req.body.phone); + + if (!waReady) { + return res.status(503).json({ error: 'WhatsApp delivery engine is offline right now. Try again shortly.' }); + } + + if (sanitizedPhone.length < 7 || sanitizedPhone.length > 15) { + return res.status(400).json({ error: 'Enter a valid WhatsApp number' }); + } + + try { + const existingUser = db.prepare('SELECT id FROM users WHERE phone = ?').get(sanitizedPhone); + if (existingUser) { + return res.status(409).json({ error: 'This WhatsApp number is already registered' }); + } + + db.prepare("UPDATE signup_otps SET status = 'superseded' WHERE phone = ? AND status = 'sent'").run(sanitizedPhone); + + const requestId = `signup_${crypto.randomBytes(8).toString('hex')}`; + const otp = generateOTP(6); + const otpHash = await bcrypt.hash(otp, 8); + const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString(); + const recipient = await resolveWhatsAppChatId(waClient, sanitizedPhone); + if (!recipient.ok) { + return res.status(400).json({ error: recipient.error, code: recipient.code }); + } + const chatId = recipient.chatId; + const textMsg = `*Veriflo verification*\n\nThis is exactly how your users will receive OTPs from Veriflo on WhatsApp.\n\nYour signup code is: *${otp}*\n\nEnter this code on the signup screen to verify your number. This code expires in 5 minutes.`; + + await waClient.sendMessage(chatId, textMsg); + + db.prepare(` + INSERT INTO signup_otps (request_id, phone, otp_hash, expires_at, status) + VALUES (?, ?, ?, ?, 'sent') + `).run(requestId, sanitizedPhone, otpHash, expiresAt); + + res.status(200).json({ + success: true, + request_id: requestId, + expires_at: expiresAt, + message: 'OTP sent to WhatsApp' + }); + } catch (err) { + console.error('Signup Send OTP Error:', err); + res.status(500).json({ error: 'Could not send signup OTP' }); + } +}); + +router.post('/signup/verify', async (req, res) => { + const { request_id, otp, name, company, password, phone } = req.body; + const email = normalizeEmail(req.body.email); + + if (!request_id || !otp || !name || !company || !email || !password || !phone) { + return res.status(400).json({ error: 'Phone, OTP, name, occupation, email, and password are required' }); + } + + const sanitizedPhone = sanitizePhone(phone); + + try { + const pendingSignup = db.prepare(` + SELECT id, phone, otp_hash, status, expires_at + FROM signup_otps + WHERE request_id = ? + `).get(request_id); + + if (!pendingSignup) { + return res.status(404).json({ error: 'Signup OTP request not found' }); + } + + if (pendingSignup.phone !== sanitizedPhone) { + return res.status(400).json({ error: 'This OTP does not belong to that WhatsApp number' }); + } + + if (pendingSignup.status === 'verified') { + return res.status(400).json({ error: 'This signup OTP has already been used' }); + } + + if (pendingSignup.status !== 'sent') { + return res.status(400).json({ error: `This signup OTP cannot be verified (${pendingSignup.status})` }); + } + + if (new Date().toISOString() > pendingSignup.expires_at) { + db.prepare("UPDATE signup_otps SET status = 'expired' WHERE id = ?").run(pendingSignup.id); + return res.status(400).json({ error: 'This OTP has expired' }); + } + + const isMatch = await bcrypt.compare(String(otp), pendingSignup.otp_hash); + if (!isMatch) { + return res.status(400).json({ error: 'Invalid OTP code' }); + } + + const existingEmail = db.prepare('SELECT id FROM users WHERE email = ?').get(email); + if (existingEmail) { + return res.status(409).json({ error: 'Email already registered' }); + } + + const existingPhone = db.prepare('SELECT id FROM users WHERE phone = ?').get(sanitizedPhone); + if (existingPhone) { + return res.status(409).json({ error: 'This WhatsApp number is already registered' }); + } + + const passwordHash = await bcrypt.hash(password, 10); + const insertUser = db.prepare( + 'INSERT INTO users (name, company, email, phone, password_hash) VALUES (?, ?, ?, ?, ?)' + ); + const result = insertUser.run(name, company, email, sanitizedPhone, passwordHash); + const userId = result.lastInsertRowid; + + db.prepare( + 'INSERT INTO settings (user_id, sender_name, greeting, message_template) VALUES (?, ?, ?, ?)' + ).run(userId, company || name, 'Hello!', DEFAULT_TEMPLATE); + + db.prepare(` + UPDATE signup_otps + SET status = 'verified', name = ?, company = ?, email = ?, password_hash = ? + WHERE id = ? + `).run(name, company, email, passwordHash, pendingSignup.id); + + const user = { id: userId, email, role: 'user' }; + const token = generateToken(user); + logActivity(userId, 'signup_completed', 'Account created', 'WhatsApp OTP signup completed'); + + res.status(201).json({ + message: 'Account created successfully', + token, + user: { id: userId, name, email, company, phone: sanitizedPhone, role: 'user' } + }); + } catch (err) { + console.error('Signup Verify Error:', err); + res.status(500).json({ error: 'Could not verify OTP and create account' }); + } +}); + +router.post('/password-reset/send-otp', passwordResetLimiter, async (req, res) => { + const waReady = req.app.locals.waReady; + const waClient = req.app.locals.waClient; + const email = normalizeEmail(req.body.email); + const sanitizedPhone = sanitizePhone(req.body.phone); + + if (!email || !sanitizedPhone) { + return res.status(400).json({ error: 'Email and WhatsApp number are required' }); + } + + if (!waReady) { + return res.status(503).json({ error: 'WhatsApp delivery engine is offline right now. Try again shortly.' }); + } + + try { + const user = db.prepare('SELECT id, email, phone FROM users WHERE email = ?').get(email); + if (!user || user.phone !== sanitizedPhone) { + return res.status(404).json({ error: 'No account matched that email and WhatsApp number' }); + } + + db.prepare("UPDATE password_reset_otps SET status = 'superseded' WHERE user_id = ? AND status = 'sent'").run(user.id); + + const requestId = `reset_${crypto.randomBytes(8).toString('hex')}`; + const otp = generateOTP(6); + const otpHash = await bcrypt.hash(otp, 8); + const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString(); + const recipient = await resolveWhatsAppChatId(waClient, sanitizedPhone); + if (!recipient.ok) { + return res.status(400).json({ error: recipient.error, code: recipient.code }); + } + const chatId = recipient.chatId; + const textMsg = `*Veriflo password reset*\n\nWe confirmed your account email as *${email}*.\n\nYour password reset OTP is: *${otp}*\n\nEnter this code on the reset screen to continue. This code expires in 5 minutes.`; + + await waClient.sendMessage(chatId, textMsg); + + db.prepare(` + INSERT INTO password_reset_otps (request_id, user_id, phone, email, otp_hash, expires_at, status) + VALUES (?, ?, ?, ?, ?, ?, 'sent') + `).run(requestId, user.id, sanitizedPhone, email, otpHash, expiresAt); + + res.json({ + success: true, + request_id: requestId, + expires_at: expiresAt, + message: 'Password reset OTP sent to WhatsApp' + }); + } catch (err) { + console.error('Password Reset Send OTP Error:', err); + res.status(500).json({ error: 'Could not send password reset OTP' }); + } +}); + +router.post('/password-reset/verify', async (req, res) => { + const { request_id, email, phone, otp, password } = req.body; + const sanitizedPhone = sanitizePhone(phone); + const normalizedEmail = normalizeEmail(email); + + if (!request_id || !normalizedEmail || !sanitizedPhone || !otp || !password) { + return res.status(400).json({ error: 'Email, WhatsApp number, OTP, and new password are required' }); + } + + try { + const resetRequest = db.prepare(` + SELECT id, user_id, phone, email, otp_hash, status, expires_at + FROM password_reset_otps + WHERE request_id = ? + `).get(request_id); + + if (!resetRequest) { + return res.status(404).json({ error: 'Password reset request not found' }); + } + + if (resetRequest.phone !== sanitizedPhone || resetRequest.email !== normalizedEmail) { + return res.status(400).json({ error: 'The email or WhatsApp number does not match this reset request' }); + } + + if (resetRequest.status === 'verified') { + return res.status(400).json({ error: 'This password reset OTP has already been used' }); + } + + if (resetRequest.status !== 'sent') { + return res.status(400).json({ error: `This password reset OTP cannot be verified (${resetRequest.status})` }); + } + + if (new Date().toISOString() > resetRequest.expires_at) { + db.prepare("UPDATE password_reset_otps SET status = 'expired' WHERE id = ?").run(resetRequest.id); + return res.status(400).json({ error: 'This OTP has expired' }); + } + + const isMatch = await bcrypt.compare(String(otp), resetRequest.otp_hash); + if (!isMatch) { + return res.status(400).json({ error: 'Invalid OTP code' }); + } + + const passwordHash = await bcrypt.hash(password, 10); + db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, resetRequest.user_id); + db.prepare("UPDATE password_reset_otps SET status = 'verified' WHERE id = ?").run(resetRequest.id); + + res.json({ + success: true, + message: 'Password updated successfully' + }); + } catch (err) { + console.error('Password Reset Verify Error:', err); + res.status(500).json({ error: 'Could not verify OTP and update password' }); + } +}); + +// User Registration +router.post('/signup', async (req, res) => { + const { name, company, password } = req.body; + const email = normalizeEmail(req.body.email); + + if (!name || !email || !password) { + return res.status(400).json({ error: 'Name, email, and password are required' }); + } + + try { + // Check if user exists + const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email); + if (existing) { + return res.status(409).json({ error: 'Email already registered' }); + } + + // Hash password + const saltRounds = 10; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // Insert user + const insertUser = db.prepare( + 'INSERT INTO users (name, company, email, password_hash) VALUES (?, ?, ?, ?)' + ); + const result = insertUser.run(name, company || '', email, passwordHash); + const userId = result.lastInsertRowid; + + // Initialize default settings for user + const initSettings = db.prepare( + `INSERT INTO settings (user_id, sender_name, greeting, message_template) VALUES (?, ?, ?, ?)` + ); + initSettings.run(userId, company || name, 'Hello!', DEFAULT_TEMPLATE); + + // Generate Token + const user = { id: userId, email, role: 'user' }; + const token = generateToken(user); + + res.status(201).json({ + message: 'Account created successfully', + token, + user: { id: userId, name, email, company } + }); + + } catch (err) { + console.error('Signup Error:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// User Login +router.post('/login', async (req, res) => { + const { password } = req.body; + const rememberMe = req.body.remember_me === true; + const email = normalizeEmail(req.body.email); + + if (!email || !password) { + return res.status(400).json({ error: 'Email and password are required' }); + } + + try { + // Find user + const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email); + if (!user) { + return res.status(401).json({ error: 'Invalid email or password' }); + } + + // Check password + const match = await bcrypt.compare(password, user.password_hash); + if (!match) { + return res.status(401).json({ error: 'Invalid email or password' }); + } + + // Generate Token + const token = generateToken(user, rememberMe ? '3d' : '1d'); + + res.json({ + message: 'Login successful', + token, + user: { id: user.id, name: user.name, email: user.email, company: user.company, role: user.role } + }); + + logActivity(user.id, 'login', 'Logged in', 'Dashboard session started'); + + } catch (err) { + console.error('Login Error:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/src/routes/otp.js b/src/routes/otp.js new file mode 100644 index 0000000..03da9d4 --- /dev/null +++ b/src/routes/otp.js @@ -0,0 +1,570 @@ +const express = require('express'); +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); +const rateLimit = require('express-rate-limit'); +const { ipKeyGenerator } = require('express-rate-limit'); +const { db } = require('../db'); +const { requireApiKey } = require('../middlewares/apiKey'); +const { logActivity } = require('../utils/activity'); +const { postWebhook } = require('../utils/webhooks'); +const { DEFAULT_TEMPLATE, renderMessageTemplate } = require('../utils/messageTemplate'); +const { + getUserSandboxMonthlyMessageLimit, + getUserLiveMonthlyMessageLimit, + getUserLiveBonusCredits, + getSandboxIntegrationQuotaUnits, + getSandboxUsageThisMonth, + getLiveUsageThisMonth +} = require('../utils/sandbox'); +const { resolveWhatsAppChatId } = require('../utils/whatsapp'); + +const router = express.Router(); + +function getRateLimitIpKey(req) { + return ipKeyGenerator(req.ip || req.socket?.remoteAddress || '0.0.0.0'); +} + +function isIntegrationTestRequest(req) { + const headerValue = String(req.get('x-veriflo-test') || '').toLowerCase(); + return req.body?.integration_test === true || req.body?.test_mode === true || headerValue === '1' || headerValue === 'true'; +} + +// ── Rate Limiters ───────────────────────────────────────────────────────── + +// Send: per-IP — 100 req/min (blocks IP-level flood) +const sendIpLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 100, + keyGenerator: (req) => getRateLimitIpKey(req), + message: { error: 'Rate limit exceeded: too many OTP send requests from this IP. Try again in a minute.', code: 'RATE_LIMIT_IP' }, +}); + +// Send: per-user/API-key — 100 req/min (prevents one account flooding) +const sendUserLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 100, + keyGenerator: (req) => req.apiKeyUser?.id ? `user_send_${req.apiKeyUser.id}` : `ip_send_${getRateLimitIpKey(req)}`, + message: { error: 'Rate limit exceeded: max 100 OTP send requests per minute per account.', code: 'RATE_LIMIT_USER' }, +}); + +// Verify: per-IP — 120 req/min (generous for verify) +const verifyIpLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 120, + keyGenerator: (req) => getRateLimitIpKey(req), + message: { error: 'Rate limit exceeded: too many verification requests from this IP. Try again in a minute.', code: 'RATE_LIMIT_IP' }, +}); + +// Verify: per request_id — 10 attempts / 15 min (brute-force OTP protection) +const verifyRequestLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + keyGenerator: (req) => `verify_rid_${req.body?.request_id || getRateLimitIpKey(req)}`, + message: { error: 'Too many verification attempts for this OTP. Request a new OTP.', code: 'RATE_LIMIT_OTP_ATTEMPTS' }, +}); + +// Resend: per-user — 10 req/min (prevents resend spam per account) +const resendUserLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 10, + keyGenerator: (req) => req.apiKeyUser?.id ? `user_resend_${req.apiKeyUser.id}` : `ip_resend_${getRateLimitIpKey(req)}`, + message: { error: 'Rate limit exceeded: too many resend requests. Try again in a minute.', code: 'RATE_LIMIT_RESEND' }, +}); + +// Helper to generate N digits +function generateOTP(length) { + const digits = '0123456789'; + let otp = ''; + for (let i = 0; i < length; i++) { + otp += digits[crypto.randomInt(0, 10)]; + } + return otp; +} + +function resolveQuotaMode({ userId, environmentMode, quotaUnits }) { + const mode = environmentMode === 'live' ? 'live' : 'sandbox'; + if (mode === 'live') { + return 'live'; + } + + // Priority rule: if user has purchased/live credits, consume live bucket first. + const liveBonusCredits = getUserLiveBonusCredits(userId); + if (liveBonusCredits > 0) { + const liveUsed = getLiveUsageThisMonth(userId); + const liveLimit = getUserLiveMonthlyMessageLimit(userId); + if (liveUsed + quotaUnits <= liveLimit) { + return 'live'; + } + } + + return 'sandbox'; +} + +// ========================================== +// POST /v1/otp/send (PUBLIC SDK ENDPOINT) +// ========================================== +router.post('/send', requireApiKey, sendIpLimiter, sendUserLimiter, async (req, res) => { + const startTime = Date.now(); + const waReady = req.app.locals.waReady; + const waClient = req.app.locals.waClient; + + if (!waReady) { + return res.status(503).json({ error: 'WhatsApp delivery engine is officially offline. Try again later.' }); + } + + const { phone, otp: customOtp } = req.body; + if (!phone) { + return res.status(400).json({ error: 'Recipient phone number is required' }); + } + + // Validate custom OTP if provided + if (customOtp !== undefined) { + const otpStr = String(customOtp); + if (!/^\d{4,16}$/.test(otpStr)) { + return res.status(400).json({ error: 'Custom otp must be 4–16 digits (numeric only)', code: 'INVALID_CUSTOM_OTP' }); + } + } + + const userId = req.apiKeyUser.id; + const keyMode = req.apiKeyUser.key_mode === 'live' ? 'live' : 'sandbox'; + + try { + // 1. Fetch user settings + const settings = db.prepare('SELECT sender_name, greeting, message_template, otp_length, expiry_seconds, webhook_url, return_otp_in_response FROM settings WHERE user_id = ?').get(userId); + const user = db.prepare('SELECT phone FROM users WHERE id = ?').get(userId); + const isIntegrationTest = isIntegrationTestRequest(req); + const quotaUnits = keyMode === 'sandbox' && isIntegrationTest + ? getSandboxIntegrationQuotaUnits() + : 1; + const quotaMode = resolveQuotaMode({ + userId, + environmentMode: keyMode, + quotaUnits, + }); + + // 2. Format inputs + const sanitizedPhone = String(phone).replace(/\D/g, ""); + if (sanitizedPhone.length < 7 || sanitizedPhone.length > 15) { + return res.status(400).json({ error: "Invalid phone number format" }); + } + + if (keyMode === 'sandbox' && user?.phone && sanitizedPhone !== user.phone) { + return res.status(403).json({ + error: 'Sandbox mode only allows OTP delivery to your registered WhatsApp number', + code: 'SANDBOX_RECIPIENT_RESTRICTED', + allowed_phone: user.phone, + environment_mode: 'sandbox' + }); + } + + if (quotaMode === 'sandbox') { + const usedThisMonth = getSandboxUsageThisMonth(userId); + const monthlyLimit = getUserSandboxMonthlyMessageLimit(userId); + if (usedThisMonth + quotaUnits > monthlyLimit) { + return res.status(403).json({ + error: 'Sandbox mode monthly message limit reached', + code: 'SANDBOX_MONTHLY_LIMIT_REACHED', + monthly_limit: monthlyLimit, + used_this_month: usedThisMonth, + attempted_quota_units: quotaUnits, + environment_mode: 'sandbox' + }); + } + } + if (quotaMode === 'live') { + const usedThisMonth = getLiveUsageThisMonth(userId); + const monthlyLimit = getUserLiveMonthlyMessageLimit(userId); + if (usedThisMonth + quotaUnits > monthlyLimit) { + return res.status(403).json({ + error: 'Live mode monthly message limit reached', + code: 'LIVE_MONTHLY_LIMIT_REACHED', + monthly_limit: monthlyLimit, + used_this_month: usedThisMonth, + attempted_quota_units: quotaUnits, + environment_mode: 'live' + }); + } + } + + const recipient = await resolveWhatsAppChatId(waClient, sanitizedPhone); + if (!recipient.ok) { + await postWebhook(settings.webhook_url, { + event: 'otp.delivery_failed', + request_id: null, + phone: sanitizedPhone, + status: 'failed', + environment_mode: keyMode, + provider: 'whatsapp', + error: recipient.error, + code: recipient.code, + timestamp: new Date().toISOString(), + }); + return res.status(400).json({ error: recipient.error, code: recipient.code }); + } + const chatId = recipient.chatId; + + const requestID = `req_${crypto.randomBytes(8).toString('hex')}`; + const otpValue = customOtp !== undefined ? String(customOtp) : generateOTP(settings.otp_length); + const expiresAt = new Date(Date.now() + settings.expiry_seconds * 1000).toISOString(); + + // Securely hash OTP for verification endpoint later + const otpHash = await bcrypt.hash(otpValue, 8); + + // 3. Construct Message + const textMsg = renderMessageTemplate(settings.message_template || DEFAULT_TEMPLATE, { + greeting: settings.greeting || 'Hello!', + sender_name: settings.sender_name || 'Veriflo', + otp: otpValue, + expiry_seconds: settings.expiry_seconds, + }); + + // 4. Send via WhatsApp + try { + await waClient.sendMessage(chatId, textMsg); + } catch (waError) { + console.error("WA Send Fail:", waError); + await postWebhook(settings.webhook_url, { + event: 'otp.delivery_failed', + request_id: requestID, + phone: sanitizedPhone, + status: 'failed', + environment_mode: keyMode, + provider: 'whatsapp', + error: 'WhatsApp routing failed', + timestamp: new Date().toISOString(), + }); + return res.status(500).json({ error: 'WhatsApp routing failed' }); + } + + const latency = Date.now() - startTime; + + // 5. Log transaction into SQLite Analytics Database + db.prepare(` + INSERT INTO otp_logs (request_id, user_id, api_key_id, phone, otp_hash, status, expires_at, ip_address, user_agent, delivery_latency_ms, quota_units, environment_mode, quota_mode) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + requestID, userId, req.apiKeyUser.key_id, sanitizedPhone, + settings.return_otp_in_response ? otpValue : otpHash, // Store raw if requested for deep debug + 'sent', expiresAt, + req.apiKeyUser.ip, req.apiKeyUser.ua, latency, quotaUnits, keyMode, quotaMode + ); + + // 6. Return standard success payload + const payload = { + success: true, + request_id: requestID, + expires_at: expiresAt, + message: 'OTP Sent successfully to WhatsApp', + quota_units: quotaUnits, + quota_mode: quotaMode, + }; + + // If super admin analytics debug flag enabled, echo back + if (settings.return_otp_in_response) { + payload.debug_otp = otpValue; // Feature explicitly requested by user in checkpoint + } + + logActivity(userId, 'otp_sent', 'OTP sent', `Recipient: ${sanitizedPhone}`); + await postWebhook(settings.webhook_url, { + event: 'otp.delivered', + request_id: requestID, + phone: sanitizedPhone, + status: 'sent', + environment_mode: keyMode, + provider: 'whatsapp', + expires_at: expiresAt, + delivery_latency_ms: latency, + timestamp: new Date().toISOString(), + }); + + res.status(200).json(payload); + + } catch (err) { + console.error('Send OTP Endpoint Error:', err); + res.status(500).json({ error: 'Internal API Server Error' }); + } +}); + +// ========================================== +// POST /v1/otp/verify (PUBLIC SDK ENDPOINT) +// ========================================== +router.post('/verify', requireApiKey, verifyIpLimiter, verifyRequestLimiter, async (req, res) => { + const { request_id, otp } = req.body; + const userId = req.apiKeyUser.id; + const keyMode = req.apiKeyUser.key_mode === 'live' ? 'live' : 'sandbox'; + + if (!request_id || !otp) { + return res.status(400).json({ error: 'request_id and otp are required' }); + } + + try { + // 1. Fetch the log, including failed_attempts counter + const log = db.prepare( + 'SELECT id, otp_hash, status, expires_at, failed_attempts, environment_mode FROM otp_logs WHERE request_id = ? AND user_id = ?' + ).get(request_id, userId); + + if (!log) { + return res.status(404).json({ error: 'OTP request not found or does not belong to your account' }); + } + + if ((log.environment_mode || 'sandbox') !== keyMode) { + return res.status(403).json({ + error: `This request belongs to ${log.environment_mode || 'sandbox'} mode. Use a ${log.environment_mode || 'sandbox'} key.`, + code: 'OTP_MODE_MISMATCH', + }); + } + + if (log.status === 'verified') { + return res.status(400).json({ error: 'This OTP has already been verified previously', code: 'ALREADY_VERIFIED' }); + } + + if (log.status === 'failed') { + return res.status(400).json({ error: 'This OTP has been invalidated due to too many failed attempts. Request a new OTP.', code: 'OTP_INVALIDATED' }); + } + + if (log.status === 'superseded') { + return res.status(400).json({ error: 'This OTP was superseded by a resend. Use the newest request_id.', code: 'OTP_SUPERSEDED' }); + } + + // Allow 'sent' and legacy 'failed_attempt' statuses through to the OTP check + if (log.status === 'expired') { + return res.status(400).json({ error: 'This OTP has expired', expired: true, code: 'OTP_EXPIRED' }); + } + + // 2. Check Expiry Time against strict UTC + const now = new Date().toISOString(); + if (now > log.expires_at) { + db.prepare("UPDATE otp_logs SET status = 'expired' WHERE id = ?").run(log.id); + return res.status(400).json({ error: 'This OTP has expired', expired: true, code: 'OTP_EXPIRED' }); + } + + const failedAttempts = log.failed_attempts || 0; + + // 3. Guard: lock out after 5 failed attempts + if (failedAttempts >= 5) { + db.prepare("UPDATE otp_logs SET status = 'failed' WHERE id = ?").run(log.id); + return res.status(400).json({ + error: 'This OTP has been invalidated due to too many failed attempts. Request a new OTP.', + verified: false, + code: 'OTP_INVALIDATED', + }); + } + + // 4. Check OTP value (hashed or raw debug mode) + let isMatch = false; + if (log.otp_hash.startsWith('$2b$')) { + isMatch = await bcrypt.compare(String(otp), log.otp_hash); + } else { + isMatch = log.otp_hash === String(otp); + } + + if (!isMatch) { + const newCount = failedAttempts + 1; + const attemptsRemaining = 5 - newCount; + + if (attemptsRemaining <= 0) { + // Lock out on 5th failed attempt + db.prepare("UPDATE otp_logs SET status = 'failed', failed_attempts = ? WHERE id = ?").run(newCount, log.id); + logActivity(userId, 'otp_verification_failed', 'OTP invalidated after too many attempts', `Request: ${request_id}`); + return res.status(400).json({ + error: 'Too many failed attempts. This OTP has been invalidated. Request a new OTP.', + verified: false, + code: 'OTP_INVALIDATED', + }); + } + + db.prepare('UPDATE otp_logs SET failed_attempts = ? WHERE id = ?').run(newCount, log.id); + logActivity(userId, 'otp_verification_failed', 'OTP verification failed', `Request: ${request_id}`); + return res.status(400).json({ + error: 'Invalid OTP code provided', + verified: false, + attempts_remaining: attemptsRemaining, + }); + } + + // 5. Mark as verified + db.prepare("UPDATE otp_logs SET status = 'verified' WHERE id = ?").run(log.id); + logActivity(userId, 'otp_verified', 'OTP verified', `Request: ${request_id}`); + + res.json({ success: true, verified: true, message: 'OTP Verified successfully' }); + + } catch (err) { + console.error('Verify OTP Endpoint Error:', err); + res.status(500).json({ error: 'Internal API Server Error' }); + } +}); + +// ========================================== +// POST /v1/otp/resend (PUBLIC SDK ENDPOINT) +// ========================================== +router.post('/resend', requireApiKey, resendUserLimiter, async (req, res) => { + const startTime = Date.now(); + const waReady = req.app.locals.waReady; + const waClient = req.app.locals.waClient; + const { request_id, otp: customOtp } = req.body; + const userId = req.apiKeyUser.id; + const keyMode = req.apiKeyUser.key_mode === 'live' ? 'live' : 'sandbox'; + + if (!request_id) { + return res.status(400).json({ error: 'request_id of the original OTP is required' }); + } + + if (customOtp !== undefined) { + const otpStr = String(customOtp); + if (!/^\d{4,16}$/.test(otpStr)) { + return res.status(400).json({ error: 'Custom otp must be 4–16 digits (numeric only)', code: 'INVALID_CUSTOM_OTP' }); + } + } + + if (!waReady) { + return res.status(503).json({ error: 'WhatsApp delivery engine is offline. Try again later.' }); + } + + try { + // 1. Look up original OTP log bound to this user + const original = db.prepare( + 'SELECT id, phone, status, expires_at, environment_mode FROM otp_logs WHERE request_id = ? AND user_id = ?' + ).get(request_id, userId); + + if (!original) { + return res.status(404).json({ error: 'Original OTP request not found or does not belong to your account' }); + } + + if ((original.environment_mode || 'sandbox') !== keyMode) { + return res.status(403).json({ + error: `This request belongs to ${original.environment_mode || 'sandbox'} mode. Use a ${original.environment_mode || 'sandbox'} key.`, + code: 'OTP_MODE_MISMATCH', + }); + } + + if (original.status === 'verified') { + return res.status(400).json({ error: 'This OTP has already been verified. No need to resend.', code: 'ALREADY_VERIFIED' }); + } + + if (original.status === 'superseded') { + return res.status(400).json({ error: 'This OTP was already superseded by a previous resend. Use the latest request_id.', code: 'OTP_SUPERSEDED' }); + } + + // 2. Fetch user settings + const settings = db.prepare( + 'SELECT sender_name, greeting, message_template, otp_length, expiry_seconds, webhook_url, return_otp_in_response FROM settings WHERE user_id = ?' + ).get(userId); + const user = db.prepare('SELECT phone FROM users WHERE id = ?').get(userId); + const isIntegrationTest = isIntegrationTestRequest(req); + const quotaUnits = keyMode === 'sandbox' && isIntegrationTest + ? getSandboxIntegrationQuotaUnits() + : 1; + const quotaMode = resolveQuotaMode({ + userId, + environmentMode: keyMode, + quotaUnits, + }); + + // 3. Sandbox: resend also restricted to registered number + if (keyMode === 'sandbox' && user?.phone && original.phone !== user.phone) { + return res.status(403).json({ + error: 'Sandbox mode only allows OTP delivery to your registered WhatsApp number', + code: 'SANDBOX_RECIPIENT_RESTRICTED', + }); + } + + // 4. Sandbox monthly limit check + if (quotaMode === 'sandbox') { + const usedThisMonth = getSandboxUsageThisMonth(userId); + const monthlyLimit = getUserSandboxMonthlyMessageLimit(userId); + if (usedThisMonth + quotaUnits > monthlyLimit) { + return res.status(403).json({ + error: 'Sandbox monthly message limit reached', + code: 'SANDBOX_MONTHLY_LIMIT_REACHED', + monthly_limit: monthlyLimit, + used_this_month: usedThisMonth, + attempted_quota_units: quotaUnits, + }); + } + } + if (quotaMode === 'live') { + const usedThisMonth = getLiveUsageThisMonth(userId); + const monthlyLimit = getUserLiveMonthlyMessageLimit(userId); + if (usedThisMonth + quotaUnits > monthlyLimit) { + return res.status(403).json({ + error: 'Live monthly message limit reached', + code: 'LIVE_MONTHLY_LIMIT_REACHED', + monthly_limit: monthlyLimit, + used_this_month: usedThisMonth, + attempted_quota_units: quotaUnits, + }); + } + } + + const recipient = await resolveWhatsAppChatId(waClient, original.phone); + if (!recipient.ok) { + return res.status(400).json({ error: recipient.error, code: recipient.code }); + } + const chatId = recipient.chatId; + const newRequestId = `req_${crypto.randomBytes(8).toString('hex')}`; + const otpValue = customOtp !== undefined ? String(customOtp) : generateOTP(settings.otp_length); + const expiresAt = new Date(Date.now() + settings.expiry_seconds * 1000).toISOString(); + const otpHash = await bcrypt.hash(otpValue, 8); + + const textMsg = renderMessageTemplate(settings.message_template || DEFAULT_TEMPLATE, { + greeting: settings.greeting || 'Hello!', + sender_name: settings.sender_name || 'Veriflo', + otp: otpValue, + expiry_seconds: settings.expiry_seconds, + }); + + // 5. Send new OTP via WhatsApp + try { + await waClient.sendMessage(chatId, textMsg); + } catch (waError) { + console.error('WA Resend Fail:', waError); + return res.status(500).json({ error: 'WhatsApp delivery failed during resend' }); + } + + const latency = Date.now() - startTime; + + // 6. Supersede the old OTP and log the new one + db.prepare("UPDATE otp_logs SET status = 'superseded' WHERE id = ?").run(original.id); + db.prepare(` + INSERT INTO otp_logs (request_id, user_id, api_key_id, phone, otp_hash, status, expires_at, ip_address, user_agent, delivery_latency_ms, resend_of, quota_units, environment_mode, quota_mode) + VALUES (?, ?, ?, ?, ?, 'sent', ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + newRequestId, userId, req.apiKeyUser.key_id, original.phone, + settings.return_otp_in_response ? otpValue : otpHash, + expiresAt, req.apiKeyUser.ip, req.apiKeyUser.ua, latency, request_id, quotaUnits, keyMode, quotaMode + ); + + logActivity(userId, 'otp_resent', 'OTP resent', `Recipient: ${original.phone}`); + + await postWebhook(settings.webhook_url, { + event: 'otp.delivered', + request_id: newRequestId, + original_request_id: request_id, + phone: original.phone, + status: 'sent', + environment_mode: keyMode, + provider: 'whatsapp', + expires_at: expiresAt, + delivery_latency_ms: latency, + timestamp: new Date().toISOString(), + }); + + const payload = { + success: true, + request_id: newRequestId, + expires_at: expiresAt, + message: 'OTP resent successfully via WhatsApp', + quota_units: quotaUnits, + quota_mode: quotaMode, + }; + if (settings.return_otp_in_response) payload.debug_otp = otpValue; + + res.status(200).json(payload); + + } catch (err) { + console.error('Resend OTP Endpoint Error:', err); + res.status(500).json({ error: 'Internal API Server Error' }); + } +}); + +module.exports = router; diff --git a/src/routes/user.js b/src/routes/user.js new file mode 100644 index 0000000..05f0859 --- /dev/null +++ b/src/routes/user.js @@ -0,0 +1,671 @@ +const express = require('express'); +const crypto = require('crypto'); +const rateLimit = require('express-rate-limit'); +const { ipKeyGenerator } = require('express-rate-limit'); +const { db } = require('../db'); +const { verifyToken } = require('../middlewares/auth'); +const { logActivity } = require('../utils/activity'); +const { postWebhook } = require('../utils/webhooks'); +const { DEFAULT_TEMPLATE, getTemplateMaxChars, validateMessageTemplate } = require('../utils/messageTemplate'); +const { + getSandboxMonthlyMessageLimit, + getLiveMonthlyMessageLimit, + getUserSandboxBonusCredits, + getUserSandboxMonthlyMessageLimit, + getUserLiveBonusCredits, + getUserLiveMonthlyMessageLimit, + getSandboxIntegrationQuotaUnits, + getSandboxUsageThisMonth, + getLiveUsageThisMonth +} = require('../utils/sandbox'); +const { resolveWhatsAppChatId } = require('../utils/whatsapp'); + +const router = express.Router(); +const TEST_OTP_USER_PER_MIN = Number(process.env.VERTIFLO_TEST_OTP_USER_PER_MIN || 8); +const TEST_OTP_IP_PER_MIN = Number(process.env.VERTIFLO_TEST_OTP_IP_PER_MIN || 20); + +function getRateLimitIpKey(req) { + return ipKeyGenerator(req.ip || req.socket?.remoteAddress || '0.0.0.0'); +} + +const testOtpIpLimiter = rateLimit({ + windowMs: 60 * 1000, + max: TEST_OTP_IP_PER_MIN, + keyGenerator: (req) => getRateLimitIpKey(req), + message: { + error: `Rate limit exceeded: max ${TEST_OTP_IP_PER_MIN} test OTP requests per minute from this IP.`, + code: 'RATE_LIMIT_TEST_OTP_IP' + } +}); + +const testOtpUserLimiter = rateLimit({ + windowMs: 60 * 1000, + max: TEST_OTP_USER_PER_MIN, + keyGenerator: (req) => `user_test_otp_${req.user?.id || getRateLimitIpKey(req)}`, + message: { + error: `Rate limit exceeded: max ${TEST_OTP_USER_PER_MIN} test OTP requests per minute per account.`, + code: 'RATE_LIMIT_TEST_OTP_USER' + } +}); + +function normalizeEmail(email) { + return String(email || '').trim().toLowerCase(); +} + +const OTP_COST_INR = 0.2; + +// Get User Profile & Settings +router.get('/profile', verifyToken, (req, res) => { + try { + const user = db.prepare('SELECT id, name, company, email, phone, role, created_at FROM users WHERE id = ?').get(req.user.id); + const settings = db.prepare('SELECT * FROM settings WHERE user_id = ?').get(req.user.id); + + // Get stats + const stats = db.prepare('SELECT count(*) as total_otps FROM otp_logs WHERE user_id = ?').get(req.user.id); + const sandboxUsageThisMonth = getSandboxUsageThisMonth(req.user.id); + const liveUsageThisMonth = getLiveUsageThisMonth(req.user.id); + const sandboxMonthlyLimitBase = getSandboxMonthlyMessageLimit(); + const liveMonthlyLimitBase = getLiveMonthlyMessageLimit(); + const sandboxBonusCredits = getUserSandboxBonusCredits(req.user.id); + const liveBonusCredits = getUserLiveBonusCredits(req.user.id); + const sandboxMonthlyLimit = getUserSandboxMonthlyMessageLimit(req.user.id); + const liveMonthlyLimit = getUserLiveMonthlyMessageLimit(req.user.id); + const trialStatus = settings?.trial_status || 'not_applied'; + const trialActive = trialStatus === 'active'; + const trialRemaining = trialActive ? Math.max(0, sandboxMonthlyLimit - sandboxUsageThisMonth) : 0; + + if (!user) return res.status(404).json({ error: 'User not found' }); + + res.json({ + user, + settings, + plan: { + name: trialActive ? 'Free Trial' : 'Trial Required', + mode: settings?.environment_mode || 'sandbox', + label: trialActive + ? `${settings?.environment_mode === 'live' ? 'Live' : 'Sandbox'} / Free Trial` + : 'Free Trial Not Activated', + started_at: user.created_at, + active: trialActive, + trial_status: trialStatus, + trial_applied_at: settings?.trial_applied_at || null, + trial_activated_at: settings?.trial_activated_at || null, + }, + limits: { + message_template_max_chars: getTemplateMaxChars(), + sandbox_monthly_message_limit: sandboxMonthlyLimit, + sandbox_monthly_message_limit_base: sandboxMonthlyLimitBase, + sandbox_bonus_credits: sandboxBonusCredits, + live_monthly_message_limit: liveMonthlyLimit, + live_monthly_message_limit_base: liveMonthlyLimitBase, + live_bonus_credits: liveBonusCredits, + test_otp_user_per_min: TEST_OTP_USER_PER_MIN, + test_otp_ip_per_min: TEST_OTP_IP_PER_MIN + }, + stats: { + total_otps: stats.total_otps, + sandbox_used_this_month: sandboxUsageThisMonth, + live_used_this_month: liveUsageThisMonth, + sandbox_bonus_credits: sandboxBonusCredits, + live_bonus_credits: liveBonusCredits, + trial_total_credits: sandboxMonthlyLimit, + trial_used_credits: trialActive ? sandboxUsageThisMonth : 0, + trial_remaining_credits: trialRemaining, + credits_remaining: trialRemaining, + live_total_credits: liveMonthlyLimit, + live_remaining_credits: Math.max(0, liveMonthlyLimit - liveUsageThisMonth) + } + }); + } catch (err) { + res.status(500).json({ error: 'Failed to fetch profile' }); + } +}); + +router.post('/billing/payment-request', verifyToken, (req, res) => { + try { + const credits = Number(req.body?.credits); + const amountInr = Number(req.body?.amount_inr); + const packageName = String(req.body?.package_name || 'Custom Pack').trim(); + const paymentConfig = db.prepare(` + SELECT payments_enabled, upi_id, upi_name + FROM payment_config + WHERE id = 1 + `).get(); + const paymentsEnabled = Number(paymentConfig?.payments_enabled || 0) === 1; + const upiId = String(paymentConfig?.upi_id || process.env.VERTIFLO_UPI_ID || '').trim(); + const payeeName = encodeURIComponent(paymentConfig?.upi_name || process.env.VERTIFLO_UPI_NAME || 'Veriflo'); + + if (!paymentsEnabled) { + return res.status(503).json({ error: 'Payments are currently disabled. Please contact admin.' }); + } + + if (!upiId) { + return res.status(500).json({ error: 'UPI ID is not configured by admin' }); + } + if (!Number.isFinite(credits) || credits <= 0) { + return res.status(400).json({ error: 'credits must be a positive number' }); + } + if (!Number.isFinite(amountInr) || amountInr <= 0) { + return res.status(400).json({ error: 'amount_inr must be a positive number' }); + } + + const requestRef = `pay_${crypto.randomBytes(6).toString('hex')}`; + const note = encodeURIComponent(`Veriflo ${packageName} ${requestRef}`); + const upiLink = `upi://pay?pa=${encodeURIComponent(upiId)}&pn=${payeeName}&am=${amountInr.toFixed(2)}&cu=INR&tn=${note}`; + + db.prepare(` + INSERT INTO payment_requests (request_ref, user_id, package_name, credits, amount_inr, upi_link, status) + VALUES (?, ?, ?, ?, ?, ?, 'pending') + `).run(requestRef, req.user.id, packageName, credits, amountInr, upiLink); + + logActivity(req.user.id, 'payment_requested', 'Payment initiated', `${packageName} • ₹${amountInr.toFixed(2)} • ${credits} credits`); + return res.json({ + success: true, + payment: { + request_ref: requestRef, + package_name: packageName, + credits, + amount_inr: amountInr, + upi_link: upiLink, + status: 'pending', + } + }); + } catch (err) { + console.error('Create Payment Request Error:', err); + return res.status(500).json({ error: 'Failed to create payment request' }); + } +}); + +router.get('/billing/config', verifyToken, (req, res) => { + try { + const paymentConfig = db.prepare(` + SELECT payments_enabled, upi_id, upi_name, payment_note + FROM payment_config + WHERE id = 1 + `).get(); + + const upiId = String(paymentConfig?.upi_id || process.env.VERTIFLO_UPI_ID || '').trim(); + + return res.json({ + config: { + payments_enabled: Number(paymentConfig?.payments_enabled || 0) === 1, + upi_available: Boolean(upiId), + upi_name: paymentConfig?.upi_name || process.env.VERTIFLO_UPI_NAME || 'Veriflo', + payment_note: paymentConfig?.payment_note || 'Pay via UPI and submit UTR for admin approval.' + } + }); + } catch (err) { + console.error('Billing Config Fetch Error:', err); + return res.status(500).json({ error: 'Failed to load billing config' }); + } +}); + +router.post('/billing/payment-request/:requestRef/submit', verifyToken, (req, res) => { + try { + const requestRef = String(req.params.requestRef || '').trim(); + const utr = String(req.body?.utr || '').trim(); + if (!requestRef) return res.status(400).json({ error: 'requestRef is required' }); + + const payment = db.prepare(` + SELECT id, status + FROM payment_requests + WHERE request_ref = ? AND user_id = ? + `).get(requestRef, req.user.id); + + if (!payment) return res.status(404).json({ error: 'Payment request not found' }); + if (!['pending', 'submitted'].includes(payment.status)) { + return res.status(400).json({ error: `Cannot submit this payment in ${payment.status} state` }); + } + + db.prepare(` + UPDATE payment_requests + SET status = 'submitted', + utr = COALESCE(NULLIF(?, ''), utr), + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `).run(utr, payment.id); + + logActivity(req.user.id, 'payment_submitted', 'Payment proof submitted', `Ref: ${requestRef}`); + return res.json({ success: true, message: 'Payment submitted for admin review' }); + } catch (err) { + console.error('Submit Payment Request Error:', err); + return res.status(500).json({ error: 'Failed to submit payment request' }); + } +}); + +router.get('/billing/payments', verifyToken, (req, res) => { + try { + const payments = db.prepare(` + SELECT request_ref, package_name, credits, amount_inr, utr, status, admin_note, created_at, updated_at, approved_at + FROM payment_requests + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT 30 + `).all(req.user.id); + + return res.json({ payments }); + } catch (err) { + console.error('List Billing Payments Error:', err); + return res.status(500).json({ error: 'Failed to load payment history' }); + } +}); + +router.put('/profile', verifyToken, async (req, res) => { + const { name, company, new_password } = req.body; + + if (!name || !String(name).trim()) { + return res.status(400).json({ error: 'Name is required' }); + } + + try { + db.prepare('UPDATE users SET name = ?, company = ? WHERE id = ?') + .run(String(name).trim(), String(company || '').trim(), req.user.id); + + if (new_password && String(new_password).trim()) { + const passwordHash = await require('bcrypt').hash(String(new_password), 10); + db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(passwordHash, req.user.id); + } + + const user = db.prepare('SELECT id, name, company, email, phone, role, created_at FROM users WHERE id = ?').get(req.user.id); + logActivity(req.user.id, 'profile_updated', 'Profile updated', 'Account details changed from dashboard settings'); + res.json({ message: 'Profile updated successfully', user }); + } catch (err) { + console.error('Profile Update Error:', err); + res.status(500).json({ error: 'Failed to update profile' }); + } +}); + +router.get('/api-keys', verifyToken, (req, res) => { + try { + const keys = db.prepare(` + SELECT + ak.id, + ak.name, + ak.prefix, + ak.mode, + ak.last_used_ip, + ak.last_used_at, + ak.created_at, + COALESCE(day_stats.total, 0) as usage_today, + COALESCE(week_stats.total, 0) as usage_week, + COALESCE(month_stats.total, 0) as usage_month + FROM api_keys ak + LEFT JOIN ( + SELECT api_key_id, COUNT(*) as total + FROM otp_logs + WHERE created_at >= datetime('now', '-1 day') + GROUP BY api_key_id + ) day_stats ON day_stats.api_key_id = ak.id + LEFT JOIN ( + SELECT api_key_id, COUNT(*) as total + FROM otp_logs + WHERE created_at >= datetime('now', '-7 days') + GROUP BY api_key_id + ) week_stats ON week_stats.api_key_id = ak.id + LEFT JOIN ( + SELECT api_key_id, COUNT(*) as total + FROM otp_logs + WHERE created_at >= datetime('now', '-30 days') + GROUP BY api_key_id + ) month_stats ON month_stats.api_key_id = ak.id + WHERE ak.user_id = ? + ORDER BY ak.created_at DESC + `).all(req.user.id); + + res.json({ + keys: keys.map((key) => ({ + ...key, + cost_today: Number((key.usage_today * OTP_COST_INR).toFixed(2)), + cost_week: Number((key.usage_week * OTP_COST_INR).toFixed(2)), + cost_month: Number((key.usage_month * OTP_COST_INR).toFixed(2)), + })), + limit_per_mode: 2, + limit_total: 4 + }); + } catch (err) { + console.error('List API Keys Error:', err); + res.status(500).json({ error: 'Failed to load API keys' }); + } +}); + +router.post('/trial/apply', verifyToken, (req, res) => { + try { + const settings = db.prepare('SELECT trial_status FROM settings WHERE user_id = ?').get(req.user.id); + if (!settings) { + return res.status(404).json({ error: 'Settings not found for this user' }); + } + + if (settings.trial_status === 'active') { + return res.json({ success: true, trial_status: 'active', message: 'Free trial is already active' }); + } + + db.prepare(` + UPDATE settings + SET trial_status = 'applied', + trial_applied_at = COALESCE(trial_applied_at, CURRENT_TIMESTAMP) + WHERE user_id = ? + `).run(req.user.id); + + logActivity(req.user.id, 'trial_applied', 'Free trial applied', 'User applied for free trial access'); + return res.json({ success: true, trial_status: 'applied', message: 'Free trial application submitted' }); + } catch (err) { + console.error('Trial Apply Error:', err); + return res.status(500).json({ error: 'Failed to apply for free trial' }); + } +}); + +router.post('/trial/activate', verifyToken, (req, res) => { + try { + const settings = db.prepare('SELECT trial_status FROM settings WHERE user_id = ?').get(req.user.id); + if (!settings) { + return res.status(404).json({ error: 'Settings not found for this user' }); + } + + if (settings.trial_status === 'active') { + return res.json({ success: true, trial_status: 'active', message: 'Free trial is already active' }); + } + + if (settings.trial_status !== 'applied') { + return res.status(400).json({ + error: 'Apply for free trial first before activation', + code: 'TRIAL_NOT_APPLIED' + }); + } + + db.prepare(` + UPDATE settings + SET trial_status = 'active', + trial_activated_at = COALESCE(trial_activated_at, CURRENT_TIMESTAMP) + WHERE user_id = ? + `).run(req.user.id); + + logActivity(req.user.id, 'trial_activated', 'Free trial activated', 'User activated free trial'); + return res.json({ success: true, trial_status: 'active', message: 'Free trial activated successfully' }); + } catch (err) { + console.error('Trial Activate Error:', err); + return res.status(500).json({ error: 'Failed to activate free trial' }); + } +}); + +// Generate new API Key +router.post('/api-key', verifyToken, (req, res) => { + try { + const settings = db.prepare('SELECT trial_status FROM settings WHERE user_id = ?').get(req.user.id); + if ((settings?.trial_status || 'not_applied') !== 'active') { + return res.status(403).json({ + error: 'Activate free trial to create API keys', + code: 'TRIAL_NOT_ACTIVE' + }); + } + + const keyName = String(req.body.name || '').trim(); + const keyMode = req.body.mode === 'live' ? 'live' : 'sandbox'; + if (!keyName) { + return res.status(400).json({ error: 'Key name is required' }); + } + + const currentModeCount = db.prepare('SELECT COUNT(*) as count FROM api_keys WHERE user_id = ? AND mode = ?').get(req.user.id, keyMode).count; + if (currentModeCount >= 2) { + return res.status(400).json({ error: `You can only have 2 active ${keyMode} API keys at a time.` }); + } + const totalCount = db.prepare('SELECT COUNT(*) as count FROM api_keys WHERE user_id = ?').get(req.user.id).count; + if (totalCount >= 4) { + return res.status(400).json({ error: 'You can only have 4 active API keys in total (2 sandbox + 2 live).' }); + } + + // 1. Generate a secure random string (32 bytes = 64 hex chars) + const rawSecret = crypto.randomBytes(32).toString('hex'); + const prefix = keyMode === 'live' ? 'vt_live_' : 'vt_sandbox_'; + + // The actual key the user sees ONCE + const plainApiKey = `${prefix}${rawSecret}`; + + // 2. Hash it for DB storage (SHA-256 is fast and sufficient for API keys) + const keyHash = crypto.createHash('sha256').update(plainApiKey).digest('hex'); + + // 3. Save to DB + const result = db.prepare('INSERT INTO api_keys (user_id, name, key_hash, prefix, mode) VALUES (?, ?, ?, ?, ?)') + .run(req.user.id, keyName, keyHash, prefix, keyMode); + + logActivity(req.user.id, 'api_key_generated', 'API key generated', `Key: ${keyName} (${keyMode})`); + + res.json({ + message: 'New API Key generated successfully.', + key: { + id: result.lastInsertRowid, + name: keyName, + prefix, + mode: keyMode, + created_at: new Date().toISOString(), + }, + apiKey: plainApiKey, + warning: 'Please copy this key now. It will never be shown again.' + }); + + } catch (err) { + console.error('API Key Generation Error:', err); + res.status(500).json({ error: 'Failed to generate API Key' }); + } +}); + +router.delete('/api-key/:id', verifyToken, (req, res) => { + try { + const key = db.prepare('SELECT id, name FROM api_keys WHERE id = ? AND user_id = ?').get(req.params.id, req.user.id); + if (!key) { + return res.status(404).json({ error: 'API key not found' }); + } + + db.prepare('DELETE FROM api_keys WHERE id = ? AND user_id = ?').run(req.params.id, req.user.id); + logActivity(req.user.id, 'api_key_deleted', 'API key deleted', `Key: ${key.name}`); + + res.json({ message: 'API key deleted successfully' }); + } catch (err) { + console.error('Delete API Key Error:', err); + res.status(500).json({ error: 'Failed to delete API key' }); + } +}); + +// Update Message Configuration Settings +router.put('/settings', verifyToken, (req, res) => { + const { sender_name, greeting, message_template, otp_length, expiry_seconds, environment_mode, webhook_url, return_otp_in_response } = req.body; + + if (environment_mode && !['sandbox', 'live'].includes(environment_mode)) { + return res.status(400).json({ error: 'environment_mode must be either sandbox or live' }); + } + + if (message_template !== undefined) { + const validation = validateMessageTemplate(message_template); + if (!validation.valid) { + return res.status(400).json({ error: validation.error }); + } + } + + try { + const stmt = db.prepare(` + UPDATE settings + SET sender_name = COALESCE(?, sender_name), + greeting = COALESCE(?, greeting), + message_template = COALESCE(?, message_template), + otp_length = COALESCE(?, otp_length), + expiry_seconds = COALESCE(?, expiry_seconds), + environment_mode = COALESCE(?, environment_mode), + webhook_url = COALESCE(?, webhook_url), + return_otp_in_response = COALESCE(?, return_otp_in_response) + WHERE user_id = ? + `); + + stmt.run( + sender_name, + greeting, + message_template, + otp_length, + expiry_seconds, + environment_mode, + webhook_url, + return_otp_in_response !== undefined ? (return_otp_in_response ? 1 : 0) : null, + req.user.id + ); + + const updatedSettings = db.prepare('SELECT * FROM settings WHERE user_id = ?').get(req.user.id); + if (!updatedSettings.message_template) { + db.prepare('UPDATE settings SET message_template = ? WHERE user_id = ?').run(DEFAULT_TEMPLATE, req.user.id); + updatedSettings.message_template = DEFAULT_TEMPLATE; + } + logActivity(req.user.id, 'settings_updated', 'Message settings updated', `Mode: ${updatedSettings.environment_mode || 'sandbox'}`); + res.json({ message: 'Settings updated', settings: updatedSettings }); + + } catch (err) { + console.error('Settings Update Error:', err); + res.status(500).json({ error: 'Failed to update settings' }); + } +}); + +// Send Test OTP (Internal Dashboard Utility) +router.post('/test-otp', verifyToken, testOtpIpLimiter, testOtpUserLimiter, async (req, res) => { + const { phone } = req.body; + if (!phone) return res.status(400).json({ error: 'Phone number is required' }); + + const waReady = req.app.locals.waReady; + const waClient = req.app.locals.waClient; + + if (!waReady) { + return res.status(503).json({ error: 'WhatsApp engine is offline' }); + } + + try { + const sanitizedPhone = String(phone).replace(/\D/g, ""); + const user = db.prepare('SELECT phone FROM users WHERE id = ?').get(req.user.id); + const settings = db.prepare('SELECT environment_mode FROM settings WHERE user_id = ?').get(req.user.id); + const environmentMode = settings?.environment_mode === 'live' ? 'live' : 'sandbox'; + + if (environmentMode === 'sandbox' && user?.phone && sanitizedPhone !== user.phone) { + return res.status(403).json({ + error: 'Sandbox mode only allows OTP delivery to your registered WhatsApp number', + code: 'SANDBOX_RECIPIENT_RESTRICTED', + allowed_phone: user.phone + }); + } + + const quotaUnits = environmentMode === 'sandbox' ? getSandboxIntegrationQuotaUnits() : 1; + let quotaMode = environmentMode; + if (environmentMode === 'sandbox') { + const liveBonusCredits = getUserLiveBonusCredits(req.user.id); + if (liveBonusCredits > 0) { + const liveUsed = getLiveUsageThisMonth(req.user.id); + const liveLimit = getUserLiveMonthlyMessageLimit(req.user.id); + if (liveUsed + quotaUnits <= liveLimit) { + quotaMode = 'live'; + } + } + } + + if (quotaMode === 'sandbox') { + const usedThisMonth = getSandboxUsageThisMonth(req.user.id); + const monthlyLimit = getUserSandboxMonthlyMessageLimit(req.user.id); + if (usedThisMonth + quotaUnits > monthlyLimit) { + return res.status(403).json({ + error: 'Sandbox mode monthly message limit reached', + code: 'SANDBOX_MONTHLY_LIMIT_REACHED', + monthly_limit: monthlyLimit, + used_this_month: usedThisMonth, + attempted_quota_units: quotaUnits + }); + } + } + if (quotaMode === 'live') { + const usedThisMonth = getLiveUsageThisMonth(req.user.id); + const monthlyLimit = getUserLiveMonthlyMessageLimit(req.user.id); + if (usedThisMonth + quotaUnits > monthlyLimit) { + return res.status(403).json({ + error: 'Live mode monthly message limit reached', + code: 'LIVE_MONTHLY_LIMIT_REACHED', + monthly_limit: monthlyLimit, + used_this_month: usedThisMonth, + attempted_quota_units: quotaUnits + }); + } + } + + const recipient = await resolveWhatsAppChatId(waClient, sanitizedPhone); + if (!recipient.ok) { + return res.status(400).json({ error: recipient.error, code: recipient.code }); + } + const chatId = recipient.chatId; + const testOtp = Math.floor(100000 + Math.random() * 900000); + const requestId = `test_${crypto.randomBytes(8).toString('hex')}`; + const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString(); + + const message = `🚀 *Veriflo Test Message*\n\nYour test verification code is: *${testOtp}*\n\nIf you're seeing this, your dashboard integration is working perfectly!`; + + await waClient.sendMessage(chatId, message); + + db.prepare(` + INSERT INTO otp_logs (request_id, user_id, api_key_id, phone, otp_hash, status, expires_at, ip_address, user_agent, delivery_latency_ms, quota_units, environment_mode, quota_mode) + VALUES (?, ?, NULL, ?, ?, 'sent', ?, ?, ?, NULL, ?, ?, ?) + `).run( + requestId, + req.user.id, + sanitizedPhone, + String(testOtp), + expiresAt, + req.ip || null, + req.get('user-agent') || null, + quotaUnits, + environmentMode, + quotaMode + ); + + logActivity(req.user.id, 'test_otp_sent', 'Test OTP sent', `Recipient: ${sanitizedPhone}`); + + res.json({ success: true, message: 'Test message sent to ' + phone, quota_units: quotaUnits, quota_mode: quotaMode }); + } catch (err) { + console.error('Test OTP Error:', err); + res.status(500).json({ error: 'Failed to send test message' }); + } +}); + +router.post('/test-webhook', verifyToken, async (req, res) => { + try { + const settings = db.prepare('SELECT webhook_url, environment_mode FROM settings WHERE user_id = ?').get(req.user.id); + const user = db.prepare('SELECT email, phone FROM users WHERE id = ?').get(req.user.id); + + if (!settings?.webhook_url) { + return res.status(400).json({ error: 'No webhook URL configured' }); + } + + const payload = { + event: 'otp.test_delivery_summary', + status: 'sent', + environment_mode: settings.environment_mode || 'sandbox', + timestamp: new Date().toISOString(), + summary: { + total_sent_today: db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND created_at >= datetime('now', '-1 day')").get(req.user.id).count, + total_verified_today: db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND status = 'verified' AND created_at >= datetime('now', '-1 day')").get(req.user.id).count, + total_failed_today: db.prepare("SELECT COUNT(*) as count FROM otp_logs WHERE user_id = ? AND status IN ('failed', 'failed_attempt') AND created_at >= datetime('now', '-1 day')").get(req.user.id).count, + }, + sample_request: { + request_id: `test_${crypto.randomBytes(6).toString('hex')}`, + phone: user?.phone || null, + email: user?.email || null, + } + }; + + const result = await postWebhook(settings.webhook_url, payload); + logActivity(req.user.id, 'webhook_test_sent', 'Webhook test sent', `Status: ${result.status || result.error || 'unknown'}`); + + if (result.ok) { + return res.json({ success: true, message: 'Test webhook delivered successfully', status: result.status }); + } + + return res.status(502).json({ + error: 'Webhook endpoint did not accept the test payload', + status: result.status, + details: result.error || null + }); + } catch (err) { + console.error('Test Webhook Error:', err); + res.status(500).json({ error: 'Failed to send test webhook' }); + } +}); + +module.exports = router; diff --git a/src/utils/activity.js b/src/utils/activity.js new file mode 100644 index 0000000..4e50082 --- /dev/null +++ b/src/utils/activity.js @@ -0,0 +1,18 @@ +const { db } = require('../db'); + +function logActivity(userId, type, title, meta = '') { + if (!userId || !type || !title) return; + + try { + db.prepare(` + INSERT INTO activity_logs (user_id, type, title, meta) + VALUES (?, ?, ?, ?) + `).run(userId, type, title, meta); + } catch (err) { + console.error('Activity Log Error:', err); + } +} + +module.exports = { + logActivity +}; diff --git a/src/utils/env.js b/src/utils/env.js new file mode 100644 index 0000000..85825c5 --- /dev/null +++ b/src/utils/env.js @@ -0,0 +1,31 @@ +const fs = require('fs'); +const path = require('path'); + +function loadEnvFile() { + const envPath = path.join(__dirname, '..', '..', '.env'); + if (!fs.existsSync(envPath)) return; + + const content = fs.readFileSync(envPath, 'utf8'); + for (const rawLine of content.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + + const index = line.indexOf('='); + if (index === -1) continue; + + const key = line.slice(0, index).trim(); + let value = line.slice(index + 1).trim(); + + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + if (!(key in process.env)) { + process.env[key] = value; + } + } +} + +module.exports = { + loadEnvFile +}; diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..718682b --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,148 @@ +const fs = require('fs'); +const path = require('path'); + +const ANSI = { + reset: '\x1b[0m', + gray: '\x1b[90m', + cyan: '\x1b[36m', + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', +}; + +// Ensure logs directory exists +const logsDir = path.join(__dirname, '../../logs'); +if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); +} + +// Log file path with date-based naming +const getLogFilePath = () => { + const today = new Date().toISOString().split('T')[0]; + return path.join(logsDir, `requests-${today}.log`); +}; + +/** + * Express middleware to log all incoming requests + * Logs: timestamp, method, path, query, IP, status code, response time + */ +const requestLogger = (req, res, next) => { + const startTime = Date.now(); + + // Capture the original res.end function + const originalEnd = res.end; + + res.end = function(chunk, encoding) { + const duration = Date.now() - startTime; + const logEntry = formatLogEntry(req, res, duration); + appendLog(logEntry); + printConsoleLog(req, res, duration); + + // Call original res.end + originalEnd.call(this, chunk, encoding); + }; + + next(); +}; + +function getStatusColor(statusCode) { + if (statusCode >= 500) return ANSI.red; + if (statusCode >= 400) return ANSI.yellow; + if (statusCode >= 300) return ANSI.cyan; + return ANSI.green; +} + +function getMethodColor(method) { + if (method === 'POST') return ANSI.cyan; + if (method === 'PUT' || method === 'PATCH') return ANSI.yellow; + if (method === 'DELETE') return ANSI.red; + return ANSI.green; +} + +function printConsoleLog(req, res, duration) { + const timestamp = new Date().toISOString(); + const method = req.method; + const statusCode = res.statusCode; + const methodColor = getMethodColor(method); + const statusColor = getStatusColor(statusCode); + const pathValue = req.originalUrl || req.path; + + const line = [ + `${ANSI.gray}[${timestamp}]${ANSI.reset}`, + `${methodColor}${method}${ANSI.reset}`, + `${pathValue}`, + `${statusColor}${statusCode}${ANSI.reset}`, + `${ANSI.gray}${duration}ms${ANSI.reset}`, + ].join(' '); + + console.log(line); +} + +/** + * Format log entry with timestamp and request details + */ +const formatLogEntry = (req, res, duration) => { + const timestamp = new Date().toISOString(); + const ip = req.ip || req.connection.remoteAddress || 'UNKNOWN'; + const method = req.method; + const path = req.path; + const query = Object.keys(req.query).length > 0 ? JSON.stringify(req.query) : '-'; + const statusCode = res.statusCode; + + return `[${timestamp}] ${method} ${path} ${query} | IP: ${ip} | Status: ${statusCode} | Duration: ${duration}ms`; +}; + +/** + * Append log entry to file + */ +const appendLog = (logEntry) => { + const logFile = getLogFilePath(); + + try { + fs.appendFileSync(logFile, logEntry + '\n'); + } catch (error) { + console.error('Failed to write to log file:', error.message); + } +}; + +/** + * Utility to get recent logs (optional, for viewing logs via API) + */ +const getRecentLogs = (lines = 50) => { + const logFile = getLogFilePath(); + + try { + if (!fs.existsSync(logFile)) { + return []; + } + + const content = fs.readFileSync(logFile, 'utf-8'); + return content.split('\n').filter(line => line.trim()).slice(-lines); + } catch (error) { + console.error('Failed to read log file:', error.message); + return []; + } +}; + +/** + * Clear logs (dangerous, use with caution) + */ +const clearLogs = () => { + const logFile = getLogFilePath(); + + try { + if (fs.existsSync(logFile)) { + fs.unlinkSync(logFile); + return true; + } + } catch (error) { + console.error('Failed to clear log file:', error.message); + } + return false; +}; + +module.exports = { + requestLogger, + getRecentLogs, + clearLogs +}; diff --git a/src/utils/messageTemplate.js b/src/utils/messageTemplate.js new file mode 100644 index 0000000..1b19647 --- /dev/null +++ b/src/utils/messageTemplate.js @@ -0,0 +1,58 @@ +const DEFAULT_TEMPLATE = `{greeting} Your {sender_name} verification code is: + +*{otp}* + +This code expires in {expiry_seconds} seconds. Do not share it.`; + +const ALLOWED_VARIABLES = ['greeting', 'sender_name', 'otp', 'expiry_seconds']; + +function getTemplateMaxChars() { + const value = Number(process.env.VERTIFLO_MESSAGE_TEMPLATE_MAX_CHARS || 320); + return Number.isFinite(value) && value > 0 ? value : 320; +} + +function renderMessageTemplate(template, values) { + let output = template || DEFAULT_TEMPLATE; + + for (const variable of ALLOWED_VARIABLES) { + const pattern = new RegExp(`\\{${variable}\\}`, 'g'); + output = output.replace(pattern, String(values[variable] ?? '')); + } + + return output; +} + +function validateMessageTemplate(template) { + const maxChars = getTemplateMaxChars(); + const input = String(template || '').trim(); + + if (!input) { + return { valid: false, error: 'Message template cannot be empty' }; + } + + if (input.length > maxChars) { + return { valid: false, error: `Message template cannot exceed ${maxChars} characters` }; + } + + const matches = input.match(/\{([^}]+)\}/g) || []; + for (const match of matches) { + const variable = match.slice(1, -1); + if (!ALLOWED_VARIABLES.includes(variable)) { + return { valid: false, error: `Unsupported variable ${match}` }; + } + } + + if (!input.includes('{otp}')) { + return { valid: false, error: 'Message template must include {otp}' }; + } + + return { valid: true }; +} + +module.exports = { + ALLOWED_VARIABLES, + DEFAULT_TEMPLATE, + getTemplateMaxChars, + renderMessageTemplate, + validateMessageTemplate +}; diff --git a/src/utils/sandbox.js b/src/utils/sandbox.js new file mode 100644 index 0000000..ea57263 --- /dev/null +++ b/src/utils/sandbox.js @@ -0,0 +1,76 @@ +const { db } = require('../db'); + +function getSandboxMonthlyMessageLimit() { + const value = Number(process.env.VERTIFLO_SANDBOX_MONTHLY_MESSAGE_LIMIT || 500); + return Number.isFinite(value) && value > 0 ? value : 500; +} + +function getLiveMonthlyMessageLimit() { + const value = Number(process.env.VERTIFLO_LIVE_MONTHLY_MESSAGE_LIMIT || 100); + return Number.isFinite(value) && value > 0 ? value : 100; +} + +function getUserSandboxBonusCredits(userId) { + const row = db.prepare('SELECT trial_bonus_credits FROM settings WHERE user_id = ?').get(userId); + const bonus = Number(row?.trial_bonus_credits || 0); + return Number.isFinite(bonus) && bonus > 0 ? Number(bonus.toFixed(2)) : 0; +} + +function getUserSandboxMonthlyMessageLimit(userId) { + const base = getSandboxMonthlyMessageLimit(); + const bonus = getUserSandboxBonusCredits(userId); + return Number((base + bonus).toFixed(2)); +} + +function getUserLiveBonusCredits(userId) { + const row = db.prepare('SELECT live_bonus_credits FROM settings WHERE user_id = ?').get(userId); + const bonus = Number(row?.live_bonus_credits || 0); + return Number.isFinite(bonus) && bonus > 0 ? Number(bonus.toFixed(2)) : 0; +} + +function getUserLiveMonthlyMessageLimit(userId) { + const base = getLiveMonthlyMessageLimit(); + const bonus = getUserLiveBonusCredits(userId); + return Number((base + bonus).toFixed(2)); +} + +function getSandboxIntegrationQuotaUnits() { + // Sandbox test OTP now always consumes 1 credit. + return 1; +} + +function getSandboxUsageThisMonth(userId) { + const result = db.prepare(` + SELECT COALESCE(SUM(COALESCE(quota_units, 1)), 0) as used + FROM otp_logs + WHERE user_id = ? + AND COALESCE(quota_mode, environment_mode, 'sandbox') = 'sandbox' + AND created_at >= datetime('now', 'start of month') + `).get(userId); + + return Number((result?.used || 0).toFixed(2)); +} + +function getLiveUsageThisMonth(userId) { + const result = db.prepare(` + SELECT COALESCE(SUM(COALESCE(quota_units, 1)), 0) as used + FROM otp_logs + WHERE user_id = ? + AND COALESCE(quota_mode, environment_mode, 'sandbox') = 'live' + AND created_at >= datetime('now', 'start of month') + `).get(userId); + + return Number((result?.used || 0).toFixed(2)); +} + +module.exports = { + getSandboxMonthlyMessageLimit, + getLiveMonthlyMessageLimit, + getUserSandboxBonusCredits, + getUserSandboxMonthlyMessageLimit, + getUserLiveBonusCredits, + getUserLiveMonthlyMessageLimit, + getSandboxIntegrationQuotaUnits, + getSandboxUsageThisMonth, + getLiveUsageThisMonth +}; diff --git a/src/utils/webhooks.js b/src/utils/webhooks.js new file mode 100644 index 0000000..a8ec31c --- /dev/null +++ b/src/utils/webhooks.js @@ -0,0 +1,28 @@ +async function postWebhook(url, payload) { + if (!url) return { skipped: true }; + + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + return { + ok: response.ok, + status: response.status, + }; + } catch (err) { + console.error('Webhook POST Error:', err); + return { + ok: false, + error: err.message || 'Webhook request failed', + }; + } +} + +module.exports = { + postWebhook +}; diff --git a/src/utils/whatsapp.js b/src/utils/whatsapp.js new file mode 100644 index 0000000..fdba6a5 --- /dev/null +++ b/src/utils/whatsapp.js @@ -0,0 +1,52 @@ +function extractChatId(numberId) { + if (!numberId) return null; + if (typeof numberId === 'string') return numberId; + if (typeof numberId._serialized === 'string') return numberId._serialized; + if (numberId.id && typeof numberId.id._serialized === 'string') return numberId.id._serialized; + return null; +} + +async function resolveWhatsAppChatId(waClient, phone) { + const normalizedPhone = String(phone || '').replace(/\D/g, ''); + if (normalizedPhone.length < 7 || normalizedPhone.length > 15) { + return { ok: false, code: 'INVALID_PHONE', error: 'Invalid phone number format' }; + } + + if (!waClient || typeof waClient.getNumberId !== 'function') { + return { ok: true, chatId: `${normalizedPhone}@c.us` }; + } + + try { + const numberId = await waClient.getNumberId(normalizedPhone); + const chatId = extractChatId(numberId); + + if (!chatId) { + return { + ok: false, + code: 'WHATSAPP_RECIPIENT_NOT_FOUND', + error: 'This number is not reachable on WhatsApp', + }; + } + + return { ok: true, chatId }; + } catch (error) { + const message = String(error?.message || ''); + if (message.includes('No LID for user')) { + return { + ok: false, + code: 'WHATSAPP_RECIPIENT_NOT_FOUND', + error: 'This number is not reachable on WhatsApp', + }; + } + + return { + ok: false, + code: 'WHATSAPP_LOOKUP_FAILED', + error: 'Could not validate WhatsApp recipient at this time', + }; + } +} + +module.exports = { + resolveWhatsAppChatId, +}; \ No newline at end of file