From 7d717f1e5b5bf3390c722952e2244cab517f590b Mon Sep 17 00:00:00 2001 From: cliffkoome Date: Mon, 19 Oct 2026 20:49:48 +0300 Subject: [PATCH 1/6] Replace Express/Sequelize app with Next.js 16 + TypeScript scaffold Removes the Express server, Sequelize models and static HTML/JS pages and sets up Next.js (App Router), TypeScript, ESLint, legacy *.html redirects and security headers. --- .env.example | 53 + .gitignore | 27 +- .vscode/settings.json | 3 - eslint.config.mjs | 24 + next.config.ts | 57 + package-lock.json | 12685 +++++++++++++++---- package.json | 117 +- public/admin-dashboard.html | 121 - public/admin-settings.html | 148 - public/auth-success.html | 30 - public/feedback-page.html | 77 - public/forgot-password.html | 58 - public/index.html | 100 - public/login.html | 66 - public/manage-stages.html | 198 - public/manage-users.html | 129 - public/mat-admin-dashboard.html | 109 - public/mat-admin-vehicles.html | 82 - public/scripts/admin-dashboard.js | 171 - public/scripts/feedback-page.js | 158 - public/scripts/forgot-password.js | 102 - public/scripts/index.js | 97 - public/scripts/login.js | 137 - public/scripts/manage-stages.js | 345 - public/scripts/manage-users.js | 220 - public/scripts/mat-admin-dashboard.js | 119 - public/scripts/mat-admin-vehicles.js | 294 - public/scripts/select-stage.js | 130 - public/scripts/signup.js | 43 - public/scripts/stage-info.js | 167 - public/scripts/user-profile.js | 223 - public/select-stage.html | 54 - public/signup.html | 55 - public/stage-details.html | 121 - public/stage-info.html | 80 - public/styles/admin-dashboard.css | 498 - public/styles/admin-settings.css | 220 - public/styles/feedback-page.css | 301 - public/styles/general.css | 155 - public/styles/index.css | 334 - public/styles/login.css | 260 - public/styles/main.css | 167 - public/styles/manage-stages.css | 534 - public/styles/manage-users.css | 224 - public/styles/mat-admin-vehicles.css | 168 - public/styles/mat-admin.css | 252 - public/styles/select-stage.css | 241 - public/styles/signup.css | 164 - public/styles/stage-details.css | 440 - public/styles/stage-info.css | 262 - public/styles/user-profile.css | 238 - public/user-profile.html | 134 - server/config/database.js | 20 - server/config/passport.js | 56 - server/controllers/adminController.js | 367 - server/controllers/authController.js | 659 - server/controllers/etaController.js | 31 - server/controllers/feedbackController.js | 51 - server/controllers/matAdminController.js | 434 - server/controllers/stageController.js | 92 - server/middleware/authMiddleware.js | 50 - server/middleware/errorHandler.js | 31 - server/middleware/rateLimiters.js | 82 - server/models/DailyDistance.js | 41 - server/models/DailyTrip.js | 41 - server/models/Feedback.js | 38 - server/models/Stage.js | 22 - server/models/User.js | 67 - server/models/Vehicle.js | 59 - server/models/index.js | 78 - server/routes/adminRoutes.js | 91 - server/routes/authRoutes.js | 121 - server/routes/etaRoutes.js | 9 - server/routes/feedbackRoutes.js | 24 - server/routes/matAdminRoutes.js | 33 - server/routes/stageRoutes.js | 30 - server/server.js | 82 - server/services/cronService.js | 208 - server/services/distanceTrackingService.js | 57 - server/services/etaCalculationService.js | 145 - server/services/firebaseService.js | 88 - server/services/geolocationService.js | 65 - server/services/googleMapsService.js | 117 - server/services/locationCache.js | 61 - server/services/smsService.js | 42 - server/services/tokenBlocklistService.js | 81 - tsconfig.json | 26 + 87 files changed, 10310 insertions(+), 14381 deletions(-) create mode 100644 .env.example delete mode 100644 .vscode/settings.json create mode 100644 eslint.config.mjs create mode 100644 next.config.ts delete mode 100644 public/admin-dashboard.html delete mode 100644 public/admin-settings.html delete mode 100644 public/auth-success.html delete mode 100644 public/feedback-page.html delete mode 100644 public/forgot-password.html delete mode 100644 public/index.html delete mode 100644 public/login.html delete mode 100644 public/manage-stages.html delete mode 100644 public/manage-users.html delete mode 100644 public/mat-admin-dashboard.html delete mode 100644 public/mat-admin-vehicles.html delete mode 100644 public/scripts/admin-dashboard.js delete mode 100644 public/scripts/feedback-page.js delete mode 100644 public/scripts/forgot-password.js delete mode 100644 public/scripts/index.js delete mode 100644 public/scripts/login.js delete mode 100644 public/scripts/manage-stages.js delete mode 100644 public/scripts/manage-users.js delete mode 100644 public/scripts/mat-admin-dashboard.js delete mode 100644 public/scripts/mat-admin-vehicles.js delete mode 100644 public/scripts/select-stage.js delete mode 100644 public/scripts/signup.js delete mode 100644 public/scripts/stage-info.js delete mode 100644 public/scripts/user-profile.js delete mode 100644 public/select-stage.html delete mode 100644 public/signup.html delete mode 100644 public/stage-details.html delete mode 100644 public/stage-info.html delete mode 100644 public/styles/admin-dashboard.css delete mode 100644 public/styles/admin-settings.css delete mode 100644 public/styles/feedback-page.css delete mode 100644 public/styles/general.css delete mode 100644 public/styles/index.css delete mode 100644 public/styles/login.css delete mode 100644 public/styles/main.css delete mode 100644 public/styles/manage-stages.css delete mode 100644 public/styles/manage-users.css delete mode 100644 public/styles/mat-admin-vehicles.css delete mode 100644 public/styles/mat-admin.css delete mode 100644 public/styles/select-stage.css delete mode 100644 public/styles/signup.css delete mode 100644 public/styles/stage-details.css delete mode 100644 public/styles/stage-info.css delete mode 100644 public/styles/user-profile.css delete mode 100644 public/user-profile.html delete mode 100644 server/config/database.js delete mode 100644 server/config/passport.js delete mode 100644 server/controllers/adminController.js delete mode 100644 server/controllers/authController.js delete mode 100644 server/controllers/etaController.js delete mode 100644 server/controllers/feedbackController.js delete mode 100644 server/controllers/matAdminController.js delete mode 100644 server/controllers/stageController.js delete mode 100644 server/middleware/authMiddleware.js delete mode 100644 server/middleware/errorHandler.js delete mode 100644 server/middleware/rateLimiters.js delete mode 100644 server/models/DailyDistance.js delete mode 100644 server/models/DailyTrip.js delete mode 100644 server/models/Feedback.js delete mode 100644 server/models/Stage.js delete mode 100644 server/models/User.js delete mode 100644 server/models/Vehicle.js delete mode 100644 server/models/index.js delete mode 100644 server/routes/adminRoutes.js delete mode 100644 server/routes/authRoutes.js delete mode 100644 server/routes/etaRoutes.js delete mode 100644 server/routes/feedbackRoutes.js delete mode 100644 server/routes/matAdminRoutes.js delete mode 100644 server/routes/stageRoutes.js delete mode 100644 server/server.js delete mode 100644 server/services/cronService.js delete mode 100644 server/services/distanceTrackingService.js delete mode 100644 server/services/etaCalculationService.js delete mode 100644 server/services/firebaseService.js delete mode 100644 server/services/geolocationService.js delete mode 100644 server/services/googleMapsService.js delete mode 100644 server/services/locationCache.js delete mode 100644 server/services/smsService.js delete mode 100644 server/services/tokenBlocklistService.js create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..36eb41a --- /dev/null +++ b/.env.example @@ -0,0 +1,53 @@ +# Copy to .env (read by Next.js, the Prisma CLI and scripts) and fill in. Never commit the real file. +# Only DATABASE_URL, DIRECT_URL and the two JWT secrets are required; everything else degrades gracefully. + +# --- Database: Neon Postgres -------------------------------------------------------------------- +# Neon console → Connect → enable "Connection pooling" for the pooled URL (host contains "-pooler"). +# The app uses the pooled URL at runtime; migrations use the direct (unpooled) one. +DATABASE_URL="postgresql://USER:PASSWORD@ep-xxxx-pooler.REGION.aws.neon.tech/neondb?sslmode=require" +DIRECT_URL="postgresql://USER:PASSWORD@ep-xxxx.REGION.aws.neon.tech/neondb?sslmode=require" +# Only needed for `prisma migrate dev` / drift checks when you can't let Prisma create one +# (e.g. a dedicated Neon branch such as "shadow"). +# SHADOW_DATABASE_URL="postgresql://USER:PASSWORD@ep-shadow.REGION.aws.neon.tech/neondb?sslmode=require" +# Driver: "neon" (serverless driver, default for *.neon.tech) or "pg" (node-postgres, default otherwise). +# DATABASE_DRIVER=neon + +# Public base URL (used for Google OAuth callback and password-reset links) +APP_URL="http://localhost:3000" + +# --- Auth ------------------------------------------------------------------------------------------ +# Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" +JWT_SECRET="" +JWT_REFRESH_SECRET="" + +# Google OAuth 2.0 (authorised redirect URI: ${APP_URL}/api/auth/google/callback) +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" + +# --- Integrations ----------------------------------------------------------------------------------- +GOOGLE_MAPS_API_KEY="" + +# Firebase Realtime Database (GPS ingestion). base64 of the service-account JSON: +# base64 -w0 firebase-service-account.json +FIREBASE_CREDENTIALS_BASE64="" +FIREBASE_DATABASE_URL="https://matatu-eta-default-rtdb.europe-west1.firebasedatabase.app/" + +# Africa's Talking SMS ("sandbox" uses the sandbox API) +AFRICAS_TALKING_API_KEY="" +AFRICAS_TALKING_USERNAME="sandbox" + +# SMTP for password-reset emails (without these, dev uses an Ethereal test inbox) +EMAIL_HOST="smtp.gmail.com" +EMAIL_PORT=465 +EMAIL_SECURE=true +EMAIL_USER="" +EMAIL_PASS="" +EMAIL_FROM="" + +# --- Background ETA cycle ------------------------------------------------------------------------------ +# interval: run every 30 s inside `next start` (default when not on Vercel) +# cron: a scheduler calls /api/cron/eta-cycle (default on Vercel, see vercel.json) +# off: disabled (e.g. when running `npm run eta:worker` separately) +# ETA_CYCLE_MODE=interval +# Required for /api/cron/eta-cycle (Vercel sends it as `Authorization: Bearer `) +CRON_SECRET="" diff --git a/.gitignore b/.gitignore index a5f2540..4c1ebf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,27 @@ +# dependencies /node_modules + +# Next.js +/.next/ +/out/ +next-env.d.ts +*.tsbuildinfo + +# Prisma client (generated by `prisma generate` / postinstall) +/src/generated/ + +# env files — commit only the example .env +.env.* +!.env.example + +# testing +/coverage + +# misc +.DS_Store +*.pem +npm-debug.log* +.vercel requests.rest -server/config/firebase-service-account.json -/tests -repomix* \ No newline at end of file +repomix* diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index aef8443..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "liveServer.settings.port": 5501 -} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..1647f2e --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,24 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +export default defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([".next/**", "node_modules/**", "src/generated/**", "coverage/**", "next-env.d.ts"]), + { + rules: { + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + }, + }, + { + // Server code must not be imported from client components and vice versa. + files: ["src/app/**/*.tsx", "src/components/**/*.tsx", "src/lib/client/**/*.ts"], + rules: { + "no-restricted-imports": [ + "error", + { patterns: [{ group: ["@/server/*", "@/generated/*"], message: "Client code must not import server modules." }] }, + ], + }, + }, +]); diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..5819eda --- /dev/null +++ b/next.config.ts @@ -0,0 +1,57 @@ +import type { NextConfig } from "next"; + +/** Old static pages → new App Router routes, so existing bookmarks and printed QR links keep working. */ +const legacyPages: Record = { + "/index.html": "/", + "/login.html": "/login", + "/signup.html": "/signup", + "/forgot-password.html": "/forgot-password", + "/auth-success.html": "/auth/success", + "/select-stage.html": "/stages", + "/feedback-page.html": "/feedback", + "/user-profile.html": "/profile", + "/admin-dashboard.html": "/admin", + "/manage-stages.html": "/admin/stages", + "/manage-users.html": "/admin/users", + "/mat-admin-dashboard.html": "/mat-admin", + "/mat-admin-vehicles.html": "/mat-admin/vehicles", +}; + +const securityHeaders = [ + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "DENY" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, + { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" }, +]; + +const nextConfig: NextConfig = { + reactStrictMode: true, + poweredByHeader: false, + // Server-only SDKs that should be required at runtime rather than bundled. + serverExternalPackages: ["firebase-admin", "pg", "nodemailer"], + images: { + remotePatterns: [{ protocol: "https", hostname: "lh3.googleusercontent.com" }], + }, + async redirects() { + return [ + { + source: "/stage-info.html", + has: [{ type: "query", key: "stageId", value: "(?\\d+)" }], + destination: "/stages/:stageId", + permanent: true, + }, + { source: "/stage-info.html", destination: "/stages", permanent: true }, + ...Object.entries(legacyPages).map(([source, destination]) => ({ + source, + destination, + permanent: true, + })), + ]; + }, + async headers() { + return [{ source: "/:path*", headers: securityHeaders }]; + }, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 94e809f..63ae954 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1997 +1,7347 @@ { - "name": "grp-a-isp-cliffkoome", - "version": "1.0.0", + "name": "matconnect", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "grp-a-isp-cliffkoome", - "version": "1.0.0", + "name": "matconnect", + "version": "2.0.0", + "hasInstallScript": true, "license": "ISC", "dependencies": { - "africastalking": "^0.7.7", - "axios": ">=1.18.0", - "bcrypt": "^6.0.0", - "body-parser": ">=2.3.0", - "brace-expansion": ">=5.0.9", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dotenv": "^17.2.2", - "express": "^5.1.0", - "fast-xml-parser": ">=5.7.0", - "firebase-admin": "^13.5.0", - "form-data": ">=4.0.6", - "joi": ">=18.2.1", - "jsonwebtoken": "^9.0.2", - "lodash": ">=4.18.0", - "mariadb": ">=3.4.6", - "node-forge": ">=1.4.0", - "nodemailer": ">=9.0.1", - "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "path": "^0.12.7", - "protobufjs": ">=7.6.5", + "@neondatabase/serverless": "^1.2.0", + "@prisma/adapter-neon": "^7.10.1", + "@prisma/adapter-pg": "^7.10.1", + "@prisma/client": "^7.10.1", + "bcryptjs": "^3.0.3", + "chart.js": "^4.5.1", + "firebase-admin": "^14.5.0", + "jose": "^6.2.12", + "next": "^16.4.2", + "nodemailer": "^10.1.7", + "otplib": "^13.5.0", + "pg": "^8.23.1", "qrcode": "^1.5.4", - "rate-limit-redis": "^4.2.0", - "sequelize": "^6.37.7", - "speakeasy": "^2.0.0", - "uuid": ">=11.1.1", - "websocket-driver": ">=0.7.5" + "react": "^19.3.1", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.3.1", + "server-only": "^0.0.1", + "zod": "^4.6.7" }, "devDependencies": { - "axios": ">=1.18.0", - "body-parser": ">=2.3.0", - "brace-expansion": ">=5.0.9", - "fast-xml-parser": ">=5.7.0", - "form-data": ">=4.0.6", - "joi": ">=18.2.1", - "lodash": ">=4.18.0", - "node-forge": ">=1.4.0", - "nodemailer": ">=9.0.1", - "nodemon": "^3.1.10", - "protobufjs": ">=7.6.5", - "uuid": ">=11.1.1", - "websocket-driver": ">=0.7.5" - } - }, - "node_modules/@fastify/busboy": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", - "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", - "license": "MIT" - }, - "node_modules/@firebase/app-check-interop-types": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.4.tgz", - "integrity": "sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==", - "license": "Apache-2.0" - }, - "node_modules/@firebase/app-types": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.5.tgz", - "integrity": "sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==", - "license": "Apache-2.0", + "@types/node": "^22.20.5", + "@types/nodemailer": "^8.0.2", + "@types/pg": "^8.23.1", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "dotenv": "^18.0.8", + "eslint": "^9.39.5", + "eslint-config-next": "^16.4.2", + "prisma": "^7.10.1", + "tsx": "^4.23.15", + "typescript": "^6.0.3", + "vitest": "^5.0.5", + "vitest-mock-extended": "^5.1.1" + }, + "engines": { + "node": ">=20.9.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/logger": "0.5.1" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@firebase/auth-interop-types": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.5.tgz", - "integrity": "sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==", - "license": "Apache-2.0" + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@firebase/component": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.3.tgz", - "integrity": "sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==", - "license": "Apache-2.0", + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/util": "1.15.1", - "tslib": "^2.1.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@firebase/database": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.3.tgz", - "integrity": "sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==", - "license": "Apache-2.0", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/app-check-interop-types": "0.3.4", - "@firebase/auth-interop-types": "0.2.5", - "@firebase/component": "0.7.3", - "@firebase/logger": "0.5.1", - "@firebase/util": "1.15.1", - "faye-websocket": "0.11.4", - "tslib": "^2.1.0" + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@firebase/database-compat": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.4.tgz", - "integrity": "sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==", - "license": "Apache-2.0", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", "dependencies": { - "@firebase/component": "0.7.3", - "@firebase/database": "1.1.3", - "@firebase/database-types": "1.0.20", - "@firebase/logger": "0.5.1", - "@firebase/util": "1.15.1", - "tslib": "^2.1.0" + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@firebase/database-types": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.20.tgz", - "integrity": "sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==", - "license": "Apache-2.0", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", "dependencies": { - "@firebase/app-types": "0.9.5", - "@firebase/util": "1.15.1" + "yallist": "^3.0.2" } }, - "node_modules/@firebase/logger": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.1.tgz", - "integrity": "sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@firebase/util": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.1.tgz", - "integrity": "sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/firestore": { - "version": "7.11.6", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", - "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", - "license": "Apache-2.0", - "optional": true, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", "dependencies": { - "@opentelemetry/api": "^1.3.0", - "fast-deep-equal": "^3.1.1", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^4.3.3", - "protobufjs": "^7.2.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=14.0.0" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/firestore/node_modules/protobufjs": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", - "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "optional": true, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=12.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@google-cloud/paginator": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", - "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/projectify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", - "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", - "license": "Apache-2.0", - "optional": true, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/promisify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", - "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", - "license": "Apache-2.0", - "optional": true, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/storage": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", - "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", - "license": "Apache-2.0", - "optional": true, + "node_modules/@babel/helpers": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", + "dev": true, + "license": "MIT", "dependencies": { - "@google-cloud/paginator": "^5.0.0", - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "<4.1.0", - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "duplexify": "^4.1.3", - "fast-xml-parser": "^5.3.4", - "gaxios": "^6.0.2", - "google-auth-library": "^9.6.3", - "html-entities": "^2.5.2", - "mime": "^3.0.0", - "p-limit": "^3.0.1", - "retry-request": "^7.0.0", - "teeny-request": "^9.0.0", - "uuid": "^8.0.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" }, "engines": { - "node": ">=14" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/storage/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "optional": true, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "dev": true, + "license": "MIT", "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=14" + "node": ">=6.0.0" } }, - "node_modules/@google-cloud/storage/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "optional": true, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=14" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/storage/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "optional": true, + "node_modules/@babel/traverse": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.10.tgz", + "integrity": "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.9", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, "engines": { - "node": ">=14" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/storage/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "yocto-queue": "^0.1.0" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@google-cloud/storage/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } + "node_modules/@electric-sql/pglite": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.3.tgz", + "integrity": "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ==", + "devOptional": true, + "license": "Apache-2.0" }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", - "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.3.tgz", + "integrity": "sha512-LAciWM0M1dCL8hlsxu2venbVZcdxema0BtDfpWYVqr+Y468UADw0pFWidhKw1M8sfJ8rdLT71tjMmnirf/IZRQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" + "bin": { + "pglite-server": "dist/scripts/server.js" }, - "engines": { - "node": ">=12.10.0" + "peerDependencies": { + "@electric-sql/pglite": "0.4.3" } }, - "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", - "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.3.tgz", + "integrity": "sha512-AlzLJTRJ8+UFgK8CmxIpyIpJ0+YaFw02IiOSdYrqxwPXdSyeIShz8aa9Tq+tYFXdPwcaMp/Fc80mQZ1dkOQ/wg==", + "devOptional": true, "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", "optional": true, "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@grpc/grpc-js/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", + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", "optional": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "tslib": "^2.4.0" } }, - "node_modules/@grpc/grpc-js/node_modules/protobufjs": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", - "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", "optional": true, "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" + "tslib": "^2.4.0" } }, - "node_modules/@grpc/grpc-js/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==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "os": [ + "aix" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@grpc/grpc-js/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", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@grpc/grpc-js/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", "optional": true, - "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" - }, + "os": [ + "android" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@grpc/grpc-js/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", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/@grpc/proto-loader/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", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, + "os": [ + "darwin" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@grpc/proto-loader/node_modules/protobufjs": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", - "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, + "os": [ + "freebsd" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/@grpc/proto-loader/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==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/@grpc/proto-loader/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", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/@grpc/proto-loader/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", "optional": true, - "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" - }, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@grpc/proto-loader/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", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@hapi/address": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", - "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" - }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/@hapi/formula": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", - "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/hoek": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/pinpoint": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", - "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", - "license": "BSD-3-Clause" + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@hapi/tlds": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", - "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", - "license": "BSD-3-Clause", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/@hapi/topo": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" ], - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "license": "Apache-2.0", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=8.0.0" + "node": ">=18" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.3.tgz", + "integrity": "sha512-fmo/IKgkenPzz3qTKG+sIUXdF/Kv83AkvvOXJHPTHJeKJbat8SsL1ihUaFaDotqoEl4UZ1Lf8Eb/3vK6eaWX2A==", + "license": "MIT" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.5.tgz", + "integrity": "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz", + "integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.2" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.6.tgz", + "integrity": "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.5.tgz", + "integrity": "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.5.tgz", + "integrity": "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.7.tgz", + "integrity": "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/database": "1.1.5", + "@firebase/database-types": "1.0.22", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + }, + "peerDependenciesMeta": { + "@firebase/app": { + "optional": true + }, + "@firebase/app-compat": { + "optional": true + } + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.22.tgz", + "integrity": "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.6", + "@firebase/util": "1.15.3" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.2.tgz", + "integrity": "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz", + "integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-9.3.1.tgz", + "integrity": "sha512-ofGTiSpfXrHrrS3KaDOmsupSzCHr4WVl1+s6Ke0F8rPBRyb/OuZSC8u1NjCKtt/fO7A1GvqASQGhdAwVsnGpQA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/firestore-api": "^0.2.0", + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^6.0.1", + "protobufjs": "^7.5.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/firestore-api": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore-api/-/firestore-api-0.2.0.tgz", + "integrity": "sha512-wfHJPZmLqzpy6FN6U+7HkkP/AKhGsbHSpEy/qL2TyI3/pKRAvpamQxZ2qvps5hMRgAjfuHCim+oeJj9RveCaqg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/gcp-metadata": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.4.tgz", + "integrity": "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-gax": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.8.tgz", + "integrity": "sha512-M4vpZcXQIC1gqIVGQ7eaU3jXQA6zecStyTXu514TYfThlgSurYJOxHZo9fzU6hAgwPWvuEynAVHWyaIk80VeEA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "3.0.4", + "protobufjs": "^7.5.4", + "retry-request": "^8.0.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/retry-request": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.4.tgz", + "integrity": "sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/teeny-request": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.4.tgz", + "integrity": "sha512-R1Cg4Vu0UULeDfHL/kjABLaTW++9yD/B6n2g48y5dJ04hsEaxcfmAqbNDzNsbqAYJyIpZafjklLG9YxRu9uzOg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-7.2.0.tgz", + "integrity": "sha512-l+x6YObHGTuqBCMvC0SXo3Cnyqg4FkWWKYRptf46PACHJ5GAUuhsd2b3wphgGrg1ybs/LAb+q5MnUTuG/3+v7w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-6.2.0.tgz", + "integrity": "sha512-QXBPu/D+1zgJ9Ydw+GXYws9OipW62nxfwsGl7x3JUTDim9NCGb7R+h7Sj3HKo1WMYb/StvOYyUBZrqU2pkhuiw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-6.2.0.tgz", + "integrity": "sha512-0i3JkkwAdGGk6AlMnFm1Vf+XtpIL73ki/MZWvXDPoN37nABf/9LFHtYeo8tW77vHSQS00ykC2ygHnUL1gkYAXg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/storage": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-8.4.0.tgz", + "integrity": "sha512-M3kA6UA2Tjp5aznCrgKcm9CzOjiQyRMnquqalqXAeS5wvkqqZxN8wpNjjHafOSYs6lQ82U7f2i8we4eTfIrpkA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^7.0.1", + "@google-cloud/projectify": "^6.0.1", + "@google-cloud/promisify": "^6.0.1", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^5.3.4", + "gaxios": "^7.1.4", + "google-auth-library": "^11.0.0", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^9.0.1", + "teeny-request": "^11.0.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.6.tgz", + "integrity": "sha512-SgC/B3/VZ8lTyOZ4Cwk6f1skWhmkNAaVfiX90qd5u1rRXGKk1//8FTUEN9GvOUq7rfWIqAYkA3CoyumYVpOS6g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT", + "optional": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@neondatabase/serverless": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-1.2.0.tgz", + "integrity": "sha512-tnaqgixUengZQNd9ZrS3ytQfamJzLKY8QxgqWQTNHHtB+KrFQWk77JTbbpg4RD+J3qE4eO2lVytvGDX5Kj6FdQ==", + "license": "MIT", + "engines": { + "node": ">=19.0.0" + } + }, + "node_modules/@next/env": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.2.tgz", + "integrity": "sha512-nTUTFH7G5/J/fmpkjVLegOkqyKOP0CEsJcMsyO4UCYeN1su+YMW/MfaSErnJskL7XM+UDK6aTvslxsRCyya/sA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.4.2.tgz", + "integrity": "sha512-gCTWlw0e+Y6kajpFyKKgA195jUJCRhlzJE0lV7ufe8a+ghIsANWZ0/FiJwg6DuOG1+OvRbcZDcFBoRh3mNJx0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "4.9.1", + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.2.tgz", + "integrity": "sha512-ONkhFvZNFWlsEHOM/81AtxdyuJP2KRYKYR0JxPwvcu1AuPMMMgcL+wW2luldJvMtZwKtUxoSkCychW5wIeoYSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.2.tgz", + "integrity": "sha512-luPCMbOsCoaF+kXJEUjmjmOZg1lC3BflhmUu+ZmW4tBu2vW/4AXak8JBFvePizGkxVmNiAw/r9ra+6UviCWtWQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.2.tgz", + "integrity": "sha512-xhwFTAncJyaCNy5KqKKuKaIIdCy1/1ZV1s5S1G0hhT7wGMkDTlfJ6aQbfRFnIG/s3/kDrxQ8aycVe+c5QnP8ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.2.tgz", + "integrity": "sha512-T1PNmfbPcxr5ndtWUL/3wQtLXsz808lq6sc2GrNT5k2oquF7UyqZddHSuAewHVsWsGXe6nsl9HoN1YNriVjuSg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.2.tgz", + "integrity": "sha512-N9cDUOCGtkJG5PNKDFZUF3xXndrJJcpLp121uAGjIGvO/U2p9tfdNOM9htj7+5f72l0m8rMw/cvYacUJBQoudg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.2.tgz", + "integrity": "sha512-yY2uh1YhX6ufuYy2FCbw1dqblbhI57GLYAJfCkqD9auTv6pLrH67Uccjc3QMKBeCYAG6lkgu08ExmX1bbjejjQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.2.tgz", + "integrity": "sha512-j/ArWRIHpMMB6wYLMhr2puTguBt70lNU/Cmjl9p6dTDE8DGknYJMP9PdwPk2dqpCNL/KIX9yfngDvIesxJPmsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.2.tgz", + "integrity": "sha512-AA4edyWHDko3NCSo0x6GomsdgFb0oR1J3MXU1b5xt6FRFg4AucrMPW3S0hCe/9onU0O1BbpGQBsqo1r0Gneu1g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz", + "integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@otplib/core": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/core/-/core-13.5.0.tgz", + "integrity": "sha512-2rURdkYkb3BDhMs3j/oCCPTve1ybJ6ruLfLfSe1ZSPV+y6RFTbLfAuT0m0ZCnps88ogkIq9t/+Li/kg5RofQwQ==", + "license": "MIT" + }, + "node_modules/@otplib/hotp": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/hotp/-/hotp-13.5.0.tgz", + "integrity": "sha512-1EwwAti05CeWJn4xXOVMBK9N6dIJlJw/cQkQyiL9OVlkeS452wAve3ffodi+5IUZdWoF1wxVug38RisdxDqDWw==", + "license": "MIT", + "dependencies": { + "@otplib/core": "13.5.0", + "@otplib/uri": "13.5.0" + } + }, + "node_modules/@otplib/plugin-base32-scure": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/plugin-base32-scure/-/plugin-base32-scure-13.5.0.tgz", + "integrity": "sha512-3JEIHindUMiIeNL0jXepSAkZ/7IkWq4sPdG9eXK0lrMXZG9RdKu5oz/4tuuTodtsFUPYhKlRMqv5xUvQIkMIaA==", + "license": "MIT", + "dependencies": { + "@otplib/core": "13.5.0", + "@scure/base": "^2.2.0" + } + }, + "node_modules/@otplib/plugin-crypto-noble": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/plugin-crypto-noble/-/plugin-crypto-noble-13.5.0.tgz", + "integrity": "sha512-fihOAGFvc4b8XTKyIK3jifFP2nLrUNc2bOaJ3UkUKJjy1XI2FbHKG0WmOH+jFsPfa6VsOnouCnEmSznDxIe0pA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.2.0", + "@otplib/core": "13.5.0" + } + }, + "node_modules/@otplib/totp": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/totp/-/totp-13.5.0.tgz", + "integrity": "sha512-GD9LzQnbHDwXCp79s8AaiA5cJR6luX5fEr9AN32AWx4ooRvllPbMkG80gHSA0jbGCnAIX/ChyOtTZ8tgJHMkSg==", + "license": "MIT", + "dependencies": { + "@otplib/core": "13.5.0", + "@otplib/hotp": "13.5.0", + "@otplib/uri": "13.5.0" + } + }, + "node_modules/@otplib/uri": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@otplib/uri/-/uri-13.5.0.tgz", + "integrity": "sha512-LsL1hqTEgJHY40U2eb/Qp6OR1tKkNGwHQTrqqjIuJj0cawGWkLEmeUFK1MHzWBQ4bgy5IdeB5Et7gIDSAH/CRw==", + "license": "MIT", + "dependencies": { + "@otplib/core": "13.5.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/adapter-neon": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-neon/-/adapter-neon-7.10.1.tgz", + "integrity": "sha512-qqOFao+72wNBA8q5pX/1/9sn+h7vj8/Qy6weMqPk7F2eALJqevYwvjkIi5RMgxAYcNJwkGES+pXbLMqPNPeH1Q==", + "license": "Apache-2.0", + "dependencies": { + "@neondatabase/serverless": ">0.6.0 <2", + "@prisma/driver-adapter-utils": "7.10.1", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.10.1.tgz", + "integrity": "sha512-EP9ruPYHjKP80WZlb5qVaEXKgZjJieBFjAxcBtQnDAHD6+16UN4keVmBh6L/RUpuDQT2FQRbvKNgq5d4Ix+IkQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.10.1", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, + "node_modules/@prisma/client": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.10.1.tgz", + "integrity": "sha512-5tgAbFGh//zVOC6WPNVgziOErJHYkUE+wmqCwbIyq6IrTXUYtv9y2iBFGh/Owo99Mx0+cr0j1FOrxkYBMuyhaQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.10.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.10.1.tgz", + "integrity": "sha512-AzoATVIgmO3lcZfM443+9SI7+cyLMW2+Psw4op8WQHpHvHCX//CKxv8fwhiL0yGWF8DjdQrFewxrZo5McBdB8g==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.10.1.tgz", + "integrity": "sha512-+0swF5ELUa3P/9cIR26+gpYjjCbGE0qvEBZhV4zMDkYsrtHjZ884MQbOnWFWXj600N0sSJNnHbtBqe12yDngWg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "8.0.2", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.10.1.tgz", + "integrity": "sha512-eFCAkGZ13D/qPg48JBlG5IWi9cSBBOC7oG3abDPcAkYioDWlIjh0YJJLK2J7qo5S+xWWD8I6egAN/8KbCaiMCA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.17", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.17.tgz", + "integrity": "sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.3", + "@electric-sql/pglite-socket": "0.1.3", + "@electric-sql/pglite-tools": "0.3.3", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.11", + "find-my-way": "9.7.0", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.4.2", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.10.1.tgz", + "integrity": "sha512-DJ4c6xvVnm3s9ZUyql9nCUicADSqyvH+CjXRCM9L6m5zJxCNmebCElG6X4irICq3rosBwlX+S19Dxq/4IrZk3A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.1" + } + }, + "node_modules/@prisma/engines": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.10.1.tgz", + "integrity": "sha512-R6/ghv1Bu36FhCdQLFof1pNtyoWoeDzJLYeTKjgqb4eGtOAJx7ZqJtcm4pzkahLfZ9rYz8AkmsMDwkyiYbXPCQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.1", + "@prisma/engines-version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8", + "@prisma/fetch-engine": "7.10.1", + "@prisma/get-platform": "7.10.1" + } + }, + "node_modules/@prisma/engines-version": { + "version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8.tgz", + "integrity": "sha512-XjXmwjN2NE340RpXoXv8ZF8aNAvr0FE+7gNfkKeW6vyjXs7F8uxvEbWQoVrhioXDQoIsnNmv7r+wxvzfdEZaWA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.1.tgz", + "integrity": "sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.1" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.10.1.tgz", + "integrity": "sha512-qZezeaExLWG2ptbHOrMxdiWluKHgqEclTdJbvY6pk6+Wg7EiVkeq5U3cXz9ti8nd0bngNvsDgledLHVRg5LF1w==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.1", + "@prisma/engines-version": "8.1.0-1.d1b3f377ca835d73fd50954a5e22128b9ebdd6b8", + "@prisma/get-platform": "7.10.1" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.10.1.tgz", + "integrity": "sha512-iVnghHne5vcaobBjPVCMEjh5r/1UCj394EIGTUclasFRatpR/MQwcM2veLv5kgoFSyYP54nch/lNFCQRYcDxvg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.10.1" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.11.tgz", + "integrity": "sha512-0TcebL559MByKqTJ+SsrFIEg228iw8UCVRFckzgfRSiJqczhs+MuAgWOF9lnOIV/IVqvu+KMnFTH0eDeTQMpUg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.2.0", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/streams-local/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@prisma/streams-local/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/studio-core": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.33.0.tgz", + "integrity": "sha512-V2fX/nKEymNTrHXwfP26PGjoLStO35Ogu+ex7CFJbLrMYEcZxxZpiSNOs7px23Hk5mzLWvM5RsqG6Ka+rha+wg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "@visx/curve": "4.0.1-alpha.0", + "@visx/event": "4.0.1-alpha.0", + "@visx/grid": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/responsive": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "d3-array": "3.2.4", + "d3-shape": "3.2.0", + "elkjs": "0.11.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "license": "BSD-3-Clause", - "optional": true + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.26", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.26.tgz", + "integrity": "sha512-48NWfiY6oNcKOh47Z4h+sIvwcQR91h6KPhaYKgeMIrkT2jV82Gy90SErj60vW8jQbeMSOSbhIv3bZ7JoqPPVhA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.2.tgz", + "integrity": "sha512-c7M5ox8p0nEOfbJ2E9Qcmt8/QCu/VzsiAhzBiZbvky2PhKZDHpKB3sQHBM5MEZNkCfBOaS6S9//AGPRTNB7IaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.2.tgz", + "integrity": "sha512-YF4wgg5iUX5Obywoac27B56FhozMNFhdJSbh11H5GBAwmBtfyRhmgTQqW9tRNNmHDvsZ2CX6UGzoz+YfYn2Leg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.2.tgz", + "integrity": "sha512-y5Ukl3oVp7e/VGxqIoEXoeb/Roj71PAJ4WG/N/RGj+eeDvKRLM15kGr9MijLbN0i03S52zxu31Z98E5gWChJKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.2.tgz", + "integrity": "sha512-zozlN0UA32bSG2zkKAgQyRYSbEISVoyU6vvkKYo2aSny74D8Jh4e+POkJ6Lk0dlezZi5x7TtD+88Xb06IB8nhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.71.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@visx/curve": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", + "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/event": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", + "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/point": "4.0.1-alpha.0" + } + }, + "node_modules/@visx/grid": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", + "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/point": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", + "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", + "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@visx/responsive": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", + "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/scale": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", + "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/shape": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", + "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/vendor": "4.0.0-alpha.0", + "classnames": "^2.3.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/vendor": { + "version": "4.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", + "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", + "devOptional": true, + "license": "MIT and ISC", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-path": "3.1.1", + "@types/d3-scale": "4.0.2", + "@types/d3-shape": "3.1.7", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-path": "3.1.0", + "d3-scale": "4.0.2", + "d3-shape": "3.2.0", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-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/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.2.0.tgz", + "integrity": "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "es-shim-unscopables": "^1.1.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axe-core": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.14.0.tgz", + "integrity": "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "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/baseline-browser-mapping": { + "version": "2.11.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.29.tgz", + "integrity": "sha512-YeesxhUmR4h5y5V8KF8iBCqJb8YFrBJ++XBTeGcWIaQOYXGN19JM0FoiOjAhwuDoln8owMgXcWBmgHtYSkky2w==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.10.0.tgz", + "integrity": "sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001816", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001816.tgz", + "integrity": "sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/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", + "optional": true, + "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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.2.tgz", + "integrity": "sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==", + "devOptional": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/deepmerge-ts" + } + ], + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause", - "optional": true + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@protobufjs/codegen": { + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/destr": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause", - "optional": true + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause", + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dotenv": { + "version": "18.0.8", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", + "integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "dotenv": "dist/index.cjs" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "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/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT", "optional": true }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", + "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/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.459", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.459.tgz", + "integrity": "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz", + "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==", + "devOptional": true, + "license": "EPL-2.0" + }, + "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/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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", "optional": true, "dependencies": { - "@protobufjs/aspromise": "^1.1.1" + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/@protobufjs/pool": { + "node_modules/es-shim-unscopables": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause", - "optional": true + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", - "license": "BSD-3-Clause", - "optional": true + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } }, - "node_modules/@tootallnate/once": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", - "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { - "node": ">= 10" + "node": ">=6" } }, - "node_modules/@types/caseless": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", - "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "node_modules/eslint-config-next": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.4.2.tgz", + "integrity": "sha512-P33vjcETMXc+qBjpF77TeJHZLzk6cPcTy8KjpxLucYB3mCIFIORMcZ+cBXcAiIYjE5RM7jyknEEOwMNdnx/ADQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/ms": "*" + "@next/eslint-plugin-next": "16.4.2", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.1.0", + "globals": "16.4.0", + "typescript-eslint": "^8.56.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" + "node_modules/eslint-config-next/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "node_modules/eslint-config-next/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/ms": "*", - "@types/node": "*" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "node_modules/eslint-config-next/node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/ignore": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.13.tgz", + "integrity": "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==", + "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": ">= 4" + } }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "node_modules/eslint-config-next/node_modules/typescript-eslint": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.2.tgz", + "integrity": "sha512-Rz4effbMy2g9wnvOJZG6SbVavX313Lap1vNEbk1RUochzickvzP8P4HB+bkNg5e50eVz1hQGjh9K1omI1xB4Bw==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "@typescript-eslint/eslint-plugin": "8.71.2", + "@typescript-eslint/parser": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@types/request": { - "version": "2.48.13", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.13.tgz", - "integrity": "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==", + "node_modules/eslint-config-next/node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.2.tgz", + "integrity": "sha512-K72/pvT7qpm8q3k8G5u93NCSN1RcLxX5h7Gw0bQpbMXviBHeY6Nx1WEwmk9wP+eKTSQ8ecxvcgt/ZX/rB938xQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.5" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/type-utils": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.71.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "node_modules/eslint-config-next/node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.2.tgz", + "integrity": "sha512-cC8UaalCViJkb3rH/xLIvidEwoFRtMZMUqDjjJMGEKaTFwYRHYBcoczlEGHuvkHEO8svRDrgZ0yQgOArFmwang==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.35", - "safe-buffer": "^5.2.1" + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/utils": "8.71.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">= 0.12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@types/request/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/eslint-config-next/node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.2.tgz", + "integrity": "sha512-aTtmZjp/x3gsGHkDs04/1I8kNogBJkpQaeGaN0PfiG43NzDEmW2n+OP5im9wfsvyX0yP1UZWBrAGtDBfH9jEVw==", + "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3" + }, "engines": { - "node": ">= 0.6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@types/request/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/eslint-config-next/node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.2.tgz", + "integrity": "sha512-DgNOEsl4pdYzwSL+N/Cl33tX1Hlw3FvId73LnKXfq7jnrF2viOEn7X+P7RBkkSWCVm92gnWd28GIC5vJuLREeA==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "mime-db": "1.52.0" + "@typescript-eslint/project-service": "8.71.2", + "@typescript-eslint/tsconfig-utils": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/visitor-keys": "8.71.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">= 0.6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "node_modules/eslint-config-next/node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.2.tgz", + "integrity": "sha512-Pd68GsLVh5ZXlcEccFPdhSOvtHZ/2WU0XsA8zmXOS3F158lVlcTLhfQ6qvtL1RzbMie0Kp8xtbVwAg4nejKPHw==", + "dev": true, "license": "MIT", - "optional": true + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.71.2", + "@typescript-eslint/types": "^8.71.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@types/validator": { - "version": "13.15.10", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "license": "MIT" + "node_modules/eslint-config-next/node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.2.tgz", + "integrity": "sha512-jz49TAWokm/7yWD9/o837nYax9DyALxIPOImdImvVzo6canBCZYzin8zJyqNR5k1j9K1DE0bVvr0XYB+99uAdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "node_modules/eslint-config-next/node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.71.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.2.tgz", + "integrity": "sha512-RyKa1G1M9rKHDhrk+HJtiF1MvLD5Ux2JW3TSkzw+02pRALdpJb+QzG1Y5em+LvCgzGwgg8H7mBRFKljNz7ImPw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "event-target-shim": "^5.0.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.71.2", + "@typescript-eslint/types": "8.71.2", + "@typescript-eslint/typescript-estree": "8.71.2" }, "engines": { - "node": ">=6.5" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "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==", + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "license": "MIT", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" + "ms": "^2.1.1" } }, - "node_modules/africastalking": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/africastalking/-/africastalking-0.7.9.tgz", - "integrity": "sha512-wb0NhvShFoTXVjzpmxjgXGuQkD1DhNqnC6DyEW7SeY/Q7fNN3X2qQjIb0XM8UPgRlx6rZYnvuCf0NW9Vmk33nA==", + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, "license": "MIT", "dependencies": { - "axios": "1.13.5", - "dotenv": "17.2.4", - "google-libphonenumber": "3.2.44", - "joi": "18.0.2", - "lodash": "4.17.23" + "debug": "^3.2.7" }, "engines": { - "node": ">=18" + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/africastalking/node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "ms": "^2.1.1" } }, - "node_modules/africastalking/node_modules/joi": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", - "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", - "license": "BSD-3-Clause", + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", "dependencies": { - "@hapi/address": "^5.1.1", - "@hapi/formula": "^3.0.2", - "@hapi/hoek": "^11.0.7", - "@hapi/pinpoint": "^2.0.1", - "@hapi/tlds": "^1.1.1", - "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.0.0" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" }, "engines": { - "node": ">= 20" + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/africastalking/node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "devOptional": true, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "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==", + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "ms": "^2.1.1" } }, - "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", + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", "dependencies": { - "color-convert": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "*" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, "engines": { - "node": ">=8" + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "retry": "0.13.1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, - "node_modules/axios": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", - "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.6", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "node_modules/axios/node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, "engines": { - "node": ">=10" + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, - "node_modules/base32.js": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz", - "integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ==", - "license": "MIT" - }, - "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/base64url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", - "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "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", + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">= 18" + "node": "*" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "balanced-match": "^4.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "20 || >=22" + "node": "*" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=0.10" } }, - "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", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" } }, - "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==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", + "optional": true, "engines": { "node": ">=6" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=12.0.0" } }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } + "node_modules/exsolve": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.3.tgz", + "integrity": "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==", + "devOptional": true, + "license": "MIT" }, - "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==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "pure-rand": "^6.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=8.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==", + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "devOptional": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": ">= 0.8" + "node": ">=8.6.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", + "license": "ISC", "dependencies": { - "safe-buffer": "5.2.1" + "is-glob": "^4.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 6" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "fast-decode-uri-component": "^1.0.1" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.6" + "optional": true, + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" } }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "node_modules/fast-xml-parser": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.2.tgz", + "integrity": "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT", + "optional": true, "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" + "@nodable/entities": "^3.0.1", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", + "strnum": "^2.4.2", + "xml-naming": "^0.3.0" }, - "engines": { - "node": ">= 0.8.0" + "bin": { + "fxparser": "src/cli/cli.js" } }, - "node_modules/cookie-parser/node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } }, - "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", + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, "engines": { - "node": ">=6.6.0" + "node": ">=0.8.0" } }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" }, "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": "^12.20 || >= 14.13" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, "engines": { - "node": ">= 12" + "node": ">=16.0.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=8" } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/find-my-way": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "devOptional": true, "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=20" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "node_modules/firebase-admin": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-14.5.0.tgz", + "integrity": "sha512-DElmgmaH9aPXu2dWXGQiDoan33UfTnbfAAKaue4fwEZxmpdaNrTk5DmuFn+vVDki2KZ6TiaC0IHRWcEzcDxB3Q==", "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.1.4", + "@firebase/database-types": "^1.0.20", + "fast-deep-equal": "^3.1.1", + "google-auth-library": "^11.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^4.0.1" + }, + "engines": { + "node": ">=22" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^9.1.0", + "@google-cloud/storage": "^8.1.0" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, "engines": { - "node": ">=0.10" + "node": ">=16" } }, - "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==", + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "17.2.4", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz", - "integrity": "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==", - "license": "BSD-2-Clause", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, "engines": { - "node": ">=12" + "node": ">=14" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/dottie": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", - "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, - "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==", + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "fetch-blob": "^3.1.2" }, "engines": { - "node": ">= 0.4" + "node": ">=12.20.0" } }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "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" + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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==", + "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==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "once": "^1.4.0" + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-define-property": { + "node_modules/functional-red-black-tree": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "optional": true }, - "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==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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", + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "es-errors": "^1.3.0" + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" }, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", + "node_modules/gcp-metadata": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.1.tgz", + "integrity": "sha512-5/LvxarRKbGCA5LJ+ha2aV1iVm7/aALIW4PzeXnQR5dyXiNMWbeA4UOBnqERInLfH/xSFp0ZREcxodUzxhEw2Q==", + "license": "Apache-2.0", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "gaxios": "^8.3.0", + "google-logging-utils": "^2.0.0", + "json-bigint": "^1.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=22" } }, - "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", - "optional": true, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.1.tgz", + "integrity": "sha512-h4/R7ay7JFXU5kmAkp/Y4Y4Vmm7m83tD579GUgfQQsRRGQ4TzSedyHla6XgTWohQuVffT3ldN4FF6uCqlRGaEA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, "engines": { - "node": ">=6" + "node": ">=22" } }, - "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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", - "optional": true, "engines": { - "node": ">=6" + "node": ">=6.9.0" } }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.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/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/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "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==", + "dev": true, "license": "MIT", - "peer": true, + "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": ">= 16" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, "license": "MIT" }, - "node_modules/farmhash-modern": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", - "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "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==", + "dev": true, "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=18.0.0" + "node": ">= 0.4" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/fast-xml-parser": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", - "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "node_modules/giget": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", + "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.2.0", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.3.0", - "xml-naming": "^0.1.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { - "fxparser": "src/cli/cli.js" + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", "dependencies": { - "websocket-driver": ">=0.5.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=0.8.0" + "node": ">=10.13.0" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "license": "MIT", + "node_modules/google-auth-library": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.3.1.tgz", + "integrity": "sha512-mQS0R2Ev/DKkzuWSYtDeq15uhDeaWuT2qzLYqJ41LmooxVKHmn5ut3pc6dlHY6wfHW/D7FBmOYjAtkjaD1A7qA==", + "license": "Apache-2.0", "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" + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^8.3.0", + "gcp-metadata": "^9.0.0", + "google-logging-utils": "^2.0.0", + "jws": "^4.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=22" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", + "node_modules/google-auth-library/node_modules/gaxios": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.1.tgz", + "integrity": "sha512-h4/R7ay7JFXU5kmAkp/Y4Y4Vmm7m83tD579GUgfQQsRRGQ4TzSedyHla6XgTWohQuVffT3ldN4FF6uCqlRGaEA==", + "license": "Apache-2.0", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" }, "engines": { - "node": ">=8" + "node": ">=22" } }, - "node_modules/firebase-admin": { - "version": "13.10.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.10.0.tgz", - "integrity": "sha512-rbuCrJvYRwqBqvbccMS8fj/x2zsaMisdf5RQbRzQzr14Rbq9r2UlpuBHqWAwrO6c9dIRF56xF/xoepXsD5yDuQ==", + "node_modules/google-gax": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-6.12.1.tgz", + "integrity": "sha512-U/OUrt4+aUiELB8sgaPJAdVfPzFOPjo1ngXVOwbR1NcCHV7ZcPOW0Ecwdbp8J86NmxBwLMJUOj8AgRSOZm5iJQ==", "license": "Apache-2.0", + "optional": true, "dependencies": { - "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "^2.0.0", - "@firebase/database-types": "^1.0.6", - "farmhash-modern": "^1.1.0", - "fast-deep-equal": "^3.1.1", - "google-auth-library": "^10.6.1", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.1.0" + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "@opentelemetry/api": "^1.9.0", + "duplexify": "^4.1.3", + "google-auth-library": "^11.0.0", + "google-logging-utils": "^2.0.0", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^4.0.0", + "protobufjs": "^7.5.4", + "retry-request": "^9.0.0" }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@google-cloud/firestore": "^7.11.0", - "@google-cloud/storage": "^7.19.0" + "node": ">=22" } }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/google-logging-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.1.0.tgz", + "integrity": "sha512-1R6uiq6XhXW2ZrcGtZpGgVDsYGzSmveVOn8Pp37qe1fbThHZ34e3t3eXj5mVu6PFSVx9fvB1KNVnYgVtq/BRcA==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "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==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.13.tgz", + "integrity": "sha512-LnPnhOBLEJEVKS8WFDVaA397L9Kq55Q9oSITJiVLHVdhAclfUkWzQv74KhvZHKL2Q09Pb1XdsrOsZ4LfTFFTEg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", "license": "MIT", + "optional": true, "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" + "gaxios": "^7.0.0", + "jws": "^4.0.0" }, "engines": { - "node": ">= 6" + "node": ">=18" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "es-define-property": "^1.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, "license": "MIT", "dependencies": { - "fetch-blob": "^3.1.2" + "dunder-proto": "^1.0.0" }, "engines": { - "node": ">=12.20.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "function-bind": "^1.1.2" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.4" } }, - "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==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "hermes-estree": "0.25.1" } }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "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", - "optional": true - }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", "optional": true, "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gaxios/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", - "optional": true, "engines": { "node": ">= 14" } }, - "node_modules/gaxios/node_modules/https-proxy-agent": { + "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", - "optional": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -2000,114 +7350,102 @@ "node": ">= 14" } }, - "node_modules/gaxios/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "devOptional": true, "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=18" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/gcp-metadata/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==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": ">= 4" } }, - "node_modules/gcp-metadata/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "license": "Apache-2.0", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gcp-metadata/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==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, "engines": { - "node": ">= 14" + "node": ">=0.8.19" } }, - "node_modules/gcp-metadata/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "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", + "optional": true + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "node": ">= 0.4" } }, - "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==", + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "devOptional": true, "license": "ISC", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=12" } }, - "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==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, "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" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -2116,232 +7454,197 @@ "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==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "has-bigints": "^1.0.2" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" + "node": ">= 0.4" }, - "engines": { - "node": ">=18" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-auth-library/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==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/google-auth-library/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "license": "Apache-2.0", "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-auth-library/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==", + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" + "semver": "^7.7.1" } }, - "node_modules/google-auth-library/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-gax": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", - "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", - "license": "Apache-2.0", - "optional": true, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", "dependencies": { - "@grpc/grpc-js": "^1.10.9", - "@grpc/proto-loader": "^0.7.13", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "google-auth-library": "^9.3.0", - "node-fetch": "^2.7.0", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.2", - "protobufjs": "^7.3.2", - "retry-request": "^7.0.0", - "uuid": "^9.0.1" + "hasown": "^2.0.4" }, "engines": { - "node": ">=14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-gax/node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "optional": true, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">=14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-gax/node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "optional": true, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-gax/node_modules/protobufjs": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", - "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "optional": true, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" + "call-bound": "^1.0.4" }, "engines": { - "node": ">=12.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-gax/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/google-libphonenumber": { - "version": "3.2.44", - "resolved": "https://registry.npmjs.org/google-libphonenumber/-/google-libphonenumber-3.2.44.tgz", - "integrity": "sha512-9p2TghluF2LTChFMLWsDRD5N78SZDsILdUk4gyqYxBXluCyxoPiOq+Fqt7DKM+LUd33+OgRkdrc+cPR93AypCQ==", - "license": "(MIT AND Apache-2.0)", + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { - "node": ">=0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", + "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": ">=14" + "node": ">=8" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, "engines": { "node": ">= 0.4" }, @@ -2349,34 +7652,37 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=0.10.0" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2385,13 +7691,25 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2400,237 +7718,279 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "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==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "call-bound": "^1.0.3" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "devOptional": true, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "6", - "debug": "4" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">= 6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "which-typed-array": "^1.1.16" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", - "engines": [ - "node >= 0.4.0" + "node_modules/is-unsafe": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", + "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } ], - "license": "MIT" - }, - "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" + "license": "MIT", + "optional": true }, - "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==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "call-bound": "^1.0.3" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/panva" } }, - "node_modules/joi": { - "version": "18.2.8", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.8.tgz", - "integrity": "sha512-G2TX62h58ZHuwqetJgP2F4ualakqAmZtBYe3jWen7gxQRw5xApX6crnFtuB91WC0c3ESBnva+kGSnb3+6pIQDQ==", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", "dependencies": { - "@hapi/address": "^5.1.1", - "@hapi/formula": "^3.0.2", - "@hapi/hoek": "^11.0.7", - "@hapi/pinpoint": "^2.0.1", - "@hapi/tlds": "^1.1.1", - "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.1.0" + "argparse": "^2.0.1" }, - "engines": { - "node": ">= 20" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/json-bigint": { @@ -2642,6 +8002,40 @@ "bignumber.js": "^9.0.0" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -2664,6 +8058,22 @@ "npm": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2676,19 +8086,20 @@ } }, "node_modules/jwks-rsa": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", - "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-4.1.0.tgz", + "integrity": "sha512-sbkByqyATKYJP5F4RXj03N5TUNC0QLTjCAZvwTzC4BwJZ8e0/cWxN8YROnyUth2g1/ONWi4eSFHeu6oYalrc3Q==", "license": "MIT", "dependencies": { "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", - "jose": "^4.15.4", + "jose": "^6.1.3", "limiter": "^1.1.5", - "lru-memoizer": "^2.2.0" + "lru-cache": "^11.0.0", + "lru-memoizer": "^3.0.0" }, "engines": { - "node": ">=14" + "node": "^20.19.0 || ^22.12.0 || >= 23.0.0" } }, "node_modules/jws": { @@ -2701,6 +8112,323 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -2722,6 +8450,7 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "devOptional": true, "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -2773,6 +8502,13 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -2786,81 +8522,96 @@ "devOptional": true, "license": "Apache-2.0" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/lru-memoizer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", - "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-3.0.0.tgz", + "integrity": "sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==", "license": "MIT", "dependencies": { "lodash.clonedeep": "^4.5.0", - "lru-cache": "6.0.0" + "lru-cache": "^11.0.1" } }, - "node_modules/lru-memoizer/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, + "node_modules/lru.min": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.5.tgz", + "integrity": "sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==", + "devOptional": true, + "license": "MIT", "engines": { - "node": ">=10" + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" } }, - "node_modules/mariadb": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.5.4.tgz", - "integrity": "sha512-3m0vdfgRkjle5/9hHl4R7rlVvnDA7QfuFSNkdIAQNROWKOS0J3b9IQK9HaZSCGEVKiDUbFXFiINfC+cmR5P/aA==", - "license": "LGPL-2.1-or-later", + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@types/geojson": "^7946.0.16", - "@types/node": ">=20", - "denque": "^2.1.0", - "iconv-lite": "^0.7.2", - "lru-cache": "^11.5.0" - }, - "engines": { - "node": ">= 20.0.0" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "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==", + "dev": true, "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==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 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==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8.6" } }, "node_modules/mime": { @@ -2876,476 +8627,965 @@ "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==", + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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/mysql2": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.24.4.tgz", + "integrity": "sha512-A2olluVlj0mvgyIRRISMEzXc51m+21mRtcMVjJyIpt2GG98+XrC9m9HzsqcMsX2LcnfccJvY5NB22g8fENBnOA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.3", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.5.tgz", + "integrity": "sha512-NFMjXXkyodBJnRrM0A6qxFO1OLUbrYLDhP1ZLetlXnyx41gKOAmVyROibq85j7I8dda4aZMs9L7kmRvvjNf7Cg==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/next/-/next-16.4.2.tgz", + "integrity": "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==", + "license": "MIT", + "dependencies": { + "@next/env": "16.4.2", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.4.2", + "@next/swc-darwin-x64": "16.4.2", + "@next/swc-linux-arm64-gnu": "16.4.2", + "@next/swc-linux-arm64-musl": "16.4.2", + "@next/swc-linux-x64-gnu": "16.4.2", + "@next/swc-linux-x64-musl": "16.4.2", + "@next/swc-win32-arm64-msvc": "16.4.2", + "@next/swc-win32-x64-msvc": "16.4.2", + "sharp": "^0.35.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.59", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.59.tgz", + "integrity": "sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nodemailer": { + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.1.7.tgz", + "integrity": "sha512-OQa6FDln5fLttmqSbjDiRYvFDOFNrBqfiOwHPlEbURil3b+vPB1v8xpi+Sp46drzNDlsTnNsS502vthmz56r4w==", + "license": "MIT-0", + "engines": { + "node": ">=20.0.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" }, "engines": { - "node": "*" + "node": ">= 0.4" } }, - "node_modules/minimatch/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": "*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, "engines": { - "node": "*" + "node": ">=12.20.0" } }, - "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==", + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "devOptional": true, "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/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", + "optional": true, + "dependencies": { + "wrappy": "1" } }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, "engines": { - "node": "^18 || ^20 || >= 21" + "node": ">= 0.8.0" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/otplib": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/otplib/-/otplib-13.5.0.tgz", + "integrity": "sha512-RpcC6aq4rANX6MverMuU3pqHVLgMPO7vl6qR8ga7YzoEHlPJknJ58cSVAaf3jBYnasiQVQHXXl2mj/4pYlsVEQ==", "license": "MIT", - "engines": { - "node": ">=10.5.0" + "dependencies": { + "@otplib/core": "13.5.0", + "@otplib/hotp": "13.5.0", + "@otplib/plugin-base32-scure": "13.5.0", + "@otplib/plugin-crypto-noble": "13.5.0", + "@otplib/totp": "13.5.0", + "@otplib/uri": "13.5.0" } }, - "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==", + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "whatwg-url": "^5.0.0" + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, "engines": { - "node": ">= 6.13.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/nodemailer": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.1.tgz", - "integrity": "sha512-c+gU9cL9HLDax3vjxL88kW+6NOgdtEUWaZ+AUtxdJR6LLhf0kGdCLExof7yiKW7zdO9EfXCSIgmhGyFmUM0mYQ==", - "dev": true, - "license": "MIT-0", + "dependencies": { + "p-limit": "^2.2.0" + }, "engines": { - "node": ">=20.0.0" + "node": ">=8" } }, - "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", - "dev": true, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" + "url": "https://github.com/sponsors/sindresorhus" } }, - "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==", - "dev": true, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/oauth": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", - "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", - "license": "MIT" + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0", + "optional": true }, - "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==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "license": "MIT", - "optional": true, "engines": { - "node": ">= 6" + "node": ">=8" } }, - "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==", + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], "license": "MIT", + "optional": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=14.0.0" } }, - "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==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "optional": true, "dependencies": { - "ee-first": "1.1.1" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC", - "dependencies": { - "wrappy": "1" - } + "optional": true }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" }, "engines": { - "node": ">=6" + "node": ">= 16.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } } }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=4" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "split2": "^4.1.0" } }, - "node_modules/passport": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", - "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "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/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", - "dependencies": { - "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" - }, "engines": { - "node": ">= 0.4.0" + "node": ">=8.6" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/passport-google-oauth20": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", - "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "node_modules/pkg-types": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.3.tgz", + "integrity": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "devOptional": true, "license": "MIT", "dependencies": { - "passport-oauth2": "1.x.x" - }, + "confbox": "^0.3.1", + "exsolve": "^1.1.1", + "pathe": "^2.0.3" + } + }, + "node_modules/pkg-types/node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" } }, - "node_modules/passport-oauth2": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", - "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "node_modules/postcss": { + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "base64url": "3.x.x", - "oauth": "0.10.x", - "passport-strategy": "1.x.x", - "uid2": "0.0.x", - "utils-merge": "1.x.x" + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 0.4.0" + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/jaredhanson" + "type": "individual", + "url": "https://github.com/sponsors/porsager" } }, - "node_modules/passport-strategy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", - "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", "engines": { - "node": ">= 0.4.0" + "node": ">=12" } }, - "node_modules/path": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", - "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, "engines": { - "node": ">=14.0.0" + "node": ">=0.10.0" } }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" - }, - "node_modules/pg-connection-string": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", - "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", - "license": "MIT" + "node_modules/prisma": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.10.1.tgz", + "integrity": "sha512-GoVeErHGGtsSjsflQ1r2BA3RKTXtxNraQJz8OLkLhoLr7OgCscdx4qw4WMM4HnERYc+a1cq2VnMNbep+vOjNcQ==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.10.1", + "@prisma/dev": "0.24.17", + "@prisma/engines": "7.10.1", + "@prisma/studio-core": "0.33.0", + "mysql2": "3.24.4", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">=10.13.0" + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">= 0.6.0" + "node": ">= 4" } }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, "node_modules/proto3-json-serializer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", - "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-4.1.0.tgz", + "integrity": "sha512-Z0Blk2BTZTPp9ZXOWnRG/P3bkEs53FtMv59sl4xU44xcDdYhKlVO/AqAHTYXAlW9HdislqQxjSornP3457wkag==", "license": "Apache-2.0", "optional": true, "dependencies": { - "protobufjs": "^7.2.5" + "protobufjs": "^7.5.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=22" } }, - "node_modules/proto3-json-serializer/node_modules/protobufjs": { + "node_modules/protobufjs": { "version": "7.6.6", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", @@ -3369,43 +9609,31 @@ "node": ">=12.0.0" } }, - "node_modules/protobufjs": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", - "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.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": ">=6" } }, - "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/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], "license": "MIT" }, "node_modules/qrcode": { @@ -3425,58 +9653,142 @@ "node": ">=10.13.0" } }, - "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", - "license": "BSD-3-Clause", + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=0.6" + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc9": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.1.0.tgz", + "integrity": "sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.7", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz", + "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/rate-limit-redis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.3.1.tgz", - "integrity": "sha512-+a1zU8+D7L8siDK9jb14refQXz60vq427VuiplgnaLk9B2LnvGe/APLTfhwb4uNIL7eWVknh8GnRp/unCj+lMA==", + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", "license": "MIT", - "engines": { - "node": ">= 16" - }, "peerDependencies": { - "express-rate-limit": ">= 6" + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "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==", + "node_modules/react-dom": { + "version": "19.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz", + "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "scheduler": "^0.28.0" }, - "engines": { - "node": ">= 0.10" + "peerDependencies": { + "react": "^19.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3493,16 +9805,71 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "license": "MIT", "dependencies": { - "picomatch": "^2.2.1" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { - "node": ">=8.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" } }, "node_modules/require-directory": { @@ -3514,12 +9881,76 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.1.tgz", + "integrity": "sha512-Z/nyvABwcRnRD2mMP7Kz6G1dFZZqX1mNErOnxF42OCQKE07b9k9hK9SQxHL3LFEZUdzQaLM4dId1E/E6Vm4WhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -3530,41 +9961,130 @@ "node": ">= 4" } }, - "node_modules/retry-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", - "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", - "license": "MIT" - }, "node_modules/retry-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", - "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-9.1.0.tgz", + "integrity": "sha512-FILyVbis+DTo3KRbtKceqpj1595wIF+XQFW/+AVOBdCQmxwNDgcNXXr2FNM0Ej2qD7zVXbJGfH7lo4jB0wwhKA==", "license": "MIT", "optional": true, "dependencies": { - "@types/request": "^2.48.8", - "extend": "^3.0.2", - "teeny-request": "^9.0.0" + "extend": "^3.0.2" }, "engines": { - "node": ">=14" + "node": ">=22" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "optional": true, "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "devOptional": true, + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" }, "engines": { - "node": ">= 18" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/safe-buffer": { @@ -3587,16 +10107,81 @@ ], "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.2.tgz", + "integrity": "sha512-tAtCGFPPP02VroDVzp4daGi6bC7jWG+41D47GKf18FeH/ubvKQHT9VmhAkvtjNOmJwqtApzWeUpEhkyEYULvmw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3605,140 +10190,145 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.4" } }, - "node_modules/sequelize": { - "version": "6.37.8", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", - "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/sequelize" - } - ], + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/debug": "^4.1.8", - "@types/validator": "^13.7.17", - "debug": "^4.3.4", - "dottie": "^2.0.6", - "inflection": "^1.13.4", - "lodash": "^4.17.21", - "moment": "^2.29.4", - "moment-timezone": "^0.5.43", - "pg-connection-string": "^2.6.1", - "retry-as-promised": "^7.0.4", - "semver": "^7.5.4", - "sequelize-pool": "^7.1.0", - "toposort-class": "^1.0.1", - "uuid": "^8.3.2", - "validator": "^13.9.0", - "wkx": "^0.5.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">=10.0.0" + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" }, - "peerDependenciesMeta": { - "ibm_db": { - "optional": true - }, - "mariadb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "oracledb": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-hstore": { - "optional": true - }, - "snowflake-sdk": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "tedious": { + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { "optional": true } } }, - "node_modules/sequelize-pool": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", - "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/sequelize/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "node": ">=8" } }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, "engines": { - "node": ">= 18" + "node": ">=8" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3758,6 +10348,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3774,6 +10365,7 @@ "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==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -3792,6 +10384,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -3807,38 +10400,79 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/speakeasy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", - "integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.2.tgz", + "integrity": "sha512-6CKD38c31SENivxOADeMNLdukOnUxUcflKtzVWzace7Riv1v7cAEym5Cx9Q7gYZ3ezIJI7ZpqecQq8cqNeSSFg==", + "devOptional": true, "license": "MIT", - "dependencies": { - "base32.js": "0.0.1" - }, "engines": { - "node": ">= 0.10.0" + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" } }, "node_modules/stream-events": { @@ -3879,84 +10513,342 @@ "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "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", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.1.0.tgz", + "integrity": "sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "get-intrinsic": "^1.3.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.4", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-ansi-cjs": { + "name": "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", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz", + "integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-11.1.1.tgz", + "integrity": "sha512-I5OLOkPN3QDN2l1H/eGueuKaq32CdAcXt91WOF/bOfadtZD1WQ8/Fk28WsqDcGjIyhAW/GQDagYhVEjhuZUFyQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=22" } }, - "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==", + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=20.0.0" } }, - "node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", - "devOptional": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": ">=18" + } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=4" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/teeny-request": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", - "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.9", - "stream-events": "^1.0.5", - "uuid": "^9.0.0" - }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/teeny-request/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, "license": "MIT", - "optional": true, - "bin": { - "uuid": "dist/bin/uuid" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/to-regex-range": { @@ -3972,37 +10864,59 @@ "node": ">=8.0" } }, - "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==", + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/toposort-class": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", - "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", - "license": "MIT" + "node_modules/ts-essentials": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.2.1.tgz", + "integrity": "sha512-+Id1fRkuir+CsgK2x04/icS2b4V1hQmq7ObzIrDjhN0ozfRYivnP7aaKMVJfLApQm0trjR39A6NIMVchiB9Erw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, "license": "MIT", - "optional": true + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } }, "node_modules/tslib": { "version": "2.8.1", @@ -4010,72 +10924,231 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, "license": "MIT", "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { - "node": ">= 18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "node_modules/typed-array-byte-offset": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.5.tgz", + "integrity": "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==", + "dev": true, "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.10" + }, "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/uid2": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", - "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", - "license": "MIT" + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "license": "MIT", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "inherits": "2.0.3" + "punycode": "^2.1.0" } }, "node_modules/util-deprecate": { @@ -4085,53 +11158,261 @@ "license": "MIT", "optional": true }, - "node_modules/util/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">= 0.4.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/uuid": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", - "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "node_modules/vite/node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", "dev": true, "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } ], "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" + "peer": true, + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "node_modules/validator": { - "version": "13.15.35", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", - "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">= 0.10" + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, - "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==", + "node_modules/vitest-mock-extended": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/vitest-mock-extended/-/vitest-mock-extended-5.1.1.tgz", + "integrity": "sha512-k5Ji2+t4+nsdepXeakCkilyKydtgaULtP0HsjwQGIZsYr5hDTqrwCtC0+Sh0YBKUS7cY/Wxplf7SVjcZstTIMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-essentials": "^10.2.1" + }, + "peerDependencies": { + "typescript": "3.x || 4.x || 5.x || 6.x || 7.x", + "vitest": ">=4.0.0" + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest/node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -4141,13 +11422,6 @@ "node": ">= 8" } }, - "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", - "optional": true - }, "node_modules/websocket-driver": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", @@ -4171,15 +11445,87 @@ "node": ">=0.8.0" } }, - "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==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-module": { @@ -4188,40 +11534,99 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, - "node_modules/wkx": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", - "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "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", + "optional": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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", + "optional": true, + "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" + "license": "ISC", + "optional": true }, "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "devOptional": true, + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", "funding": [ { "type": "github", @@ -4229,69 +11634,111 @@ } ], "license": "MIT", + "optional": true, "engines": { "node": ">=16.0.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", + "optional": true, "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=8" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "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", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, + "optional": true, "engines": { - "node": ">=6" + "node": ">=12" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index b878742..dd34073 100644 --- a/package.json +++ b/package.json @@ -1,66 +1,69 @@ { - "name": "grp-a-isp-cliffkoome", - "version": "1.0.0", - "description": "[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.[...]", - "main": "index.js", - "scripts": { - "dev": "nodemon server/server.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/cliffkoome/GRP-A-ISP-cliffkoome.git" - }, - "keywords": [], - "author": "", + "name": "matconnect", + "version": "2.0.0", + "private": true, + "type": "module", + "description": "MatConnect — IoT-enabled real-time matatu arrival display (Next.js + Prisma + Neon Postgres)", "license": "ISC", - "type": "commonjs", - "bugs": { - "url": "https://github.com/cliffkoome/GRP-A-ISP-cliffkoome/issues" + "engines": { + "node": ">=22" + }, + "scripts": { + "dev": "next dev", + "build": "prisma generate && next build", + "start": "next start", + "lint": "eslint .", + "typecheck": "next typegen && tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:integration": "vitest run --config vitest.integration.config.ts", + "postinstall": "prisma generate", + "db:generate": "prisma generate", + "db:validate": "prisma validate", + "db:format": "prisma format", + "db:migrate:dev": "prisma migrate dev", + "db:migrate:create": "prisma migrate dev --create-only", + "db:migrate:deploy": "prisma migrate deploy", + "db:migrate:status": "prisma migrate status", + "db:migrate:check": "node scripts/check-migrations.mjs", + "db:seed": "prisma db seed", + "db:studio": "prisma studio", + "eta:worker": "tsx --conditions=react-server scripts/eta-worker.ts" }, - "homepage": "https://github.com/cliffkoome/GRP-A-ISP-cliffkoome#readme", "dependencies": { - "africastalking": "^0.7.7", - "axios": ">=1.18.0", - "bcrypt": "^6.0.0", - "body-parser": ">=2.3.0", - "brace-expansion": ">=5.0.9", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dotenv": "^17.2.2", - "express": "^5.1.0", - "fast-xml-parser": ">=5.7.0", - "firebase-admin": "^13.5.0", - "form-data": ">=4.0.6", - "jsonwebtoken": "^9.0.2", - "joi": ">=18.2.1", - "lodash": ">=4.18.0", - "mariadb": ">=3.4.6", - "node-forge": ">=1.4.0", - "nodemailer": ">=9.0.1", - "passport": "^0.7.0", - "rate-limit-redis": "^4.2.0", - "passport-google-oauth20": "^2.0.0", - "path": "^0.12.7", - "protobufjs": ">=7.6.5", + "@neondatabase/serverless": "^1.2.0", + "@prisma/adapter-neon": "^7.10.1", + "@prisma/adapter-pg": "^7.10.1", + "@prisma/client": "^7.10.1", + "bcryptjs": "^3.0.3", + "chart.js": "^4.5.1", + "firebase-admin": "^14.5.0", + "jose": "^6.2.12", + "next": "^16.4.2", + "nodemailer": "^10.1.7", + "otplib": "^13.5.0", + "pg": "^8.23.1", "qrcode": "^1.5.4", - "sequelize": "^6.37.7", - "speakeasy": "^2.0.0", - "uuid": ">=11.1.1", - "websocket-driver": ">=0.7.5" + "react": "^19.3.1", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.3.1", + "server-only": "^0.0.1", + "zod": "^4.6.7" }, "devDependencies": { - "axios": ">=1.18.0", - "body-parser": ">=2.3.0", - "brace-expansion": ">=5.0.9", - "fast-xml-parser": ">=5.7.0", - "form-data": ">=4.0.6", - "joi": ">=18.2.1", - "lodash": ">=4.18.0", - "node-forge": ">=1.4.0", - "nodemailer": ">=9.0.1", - "nodemon": "^3.1.10", - "protobufjs": ">=7.6.5", - "uuid": ">=11.1.1", - "websocket-driver": ">=0.7.5" + "@types/node": "^22.20.5", + "@types/nodemailer": "^8.0.2", + "@types/pg": "^8.23.1", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "dotenv": "^18.0.8", + "eslint": "^9.39.5", + "eslint-config-next": "^16.4.2", + "prisma": "^7.10.1", + "tsx": "^4.23.15", + "typescript": "^6.0.3", + "vitest": "^5.0.5", + "vitest-mock-extended": "^5.1.1" } } diff --git a/public/admin-dashboard.html b/public/admin-dashboard.html deleted file mode 100644 index 7af9c9b..0000000 --- a/public/admin-dashboard.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - Admin Dashboard - MatConnect - - - - - - - -
- - -
-
-

Admin Dashboard

-

Welcome back! Here's an overview of the system.

-
- - -
-
-
- directions_bus -
-
-

Total Vehicles

- -- -
-
-
-
- signal_cellular_alt -
-
-

Online Vehicles

- -- -
-
-
-
- pin_drop -
-
-

Configured Stages

- -- -
-
-
- - -
-
-
-
-

Live Vehicle Status

- View All -
-
- - - - - - - - - - - - - -
VehicleRouteStatusLocation
Loading vehicle data...
-
-
-
- - -
-
-
- - - \ No newline at end of file diff --git a/public/admin-settings.html b/public/admin-settings.html deleted file mode 100644 index 0771311..0000000 --- a/public/admin-settings.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - Admin Settings - - - - - - -
- - -
- - -
- - -
-
-
-

Manage Routes

- -
-
- - - - - - - - - - - - - - - - - - - - - - - -
Route NameStart StageEnd StageActions
Route 1Stage AStage BEdit
Route 2Stage CStage DEdit
-
-
-
- - - -
-
-
-

Manage Stages

- -
-
- - - - - - - - - - - - - - - - - - - - -
Stage NameLocationActions
Stage ALocation 1Edit
Stage BLocation 2Edit
-
-
-
- - - -
-
-
-

Manage Devices

- -
-
- - - - - - - - - - - - - - - - - - - - -
Device IDStatusActions
Device 1ActiveEdit
Device 2InactiveEdit
-
-
-
-
-
-
- - \ No newline at end of file diff --git a/public/auth-success.html b/public/auth-success.html deleted file mode 100644 index 125ea17..0000000 --- a/public/auth-success.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - Authenticating... - - - -

Please wait, you are being redirected...

- - - \ No newline at end of file diff --git a/public/feedback-page.html b/public/feedback-page.html deleted file mode 100644 index 2ee12be..0000000 --- a/public/feedback-page.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - Feedback - - - - - - - - - -
- - -
- -
- -
-

© 2025 Ma3Route. All rights reserved.

-
-
- - \ No newline at end of file diff --git a/public/forgot-password.html b/public/forgot-password.html deleted file mode 100644 index fcdea25..0000000 --- a/public/forgot-password.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - MatConnect - Reset Password - - - - - - - -
-
-
-

Reset your Password

-

Enter your email and we'll send you a reset link.

-
- -
-
- -
- - -
- - -
-
-
- -
-
- -
-
- - - \ No newline at end of file diff --git a/public/index.html b/public/index.html deleted file mode 100644 index ac21262..0000000 --- a/public/index.html +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - MatConnect - - - - - - - - - - - - - -
-
-

Know when your matatu is arriving in real-time.

-

- MatConnect provides real-time arrival information for matatus, helping you plan your commute efficiently and - avoid unnecessary waiting. -

-
- - -
-
- -
-
-

Features

-

Everything you need for a stress-free commute.

-
-
-
- -
-

Real-Time ETA

-

Get accurate estimated arrival times for your matatu, updated in real-time based on - current traffic and route conditions.

-
-
- -
- -
-

Stage Information

-

Access detailed information about matatu stages, including routes, stops, and available - services.

-
-
- -
- -
-

Reliable Updates

-

Recieve timely notifications and updates about your matatu's progress, ensuring you're - always informed.

-
-
-
-
-
- - - - - \ No newline at end of file diff --git a/public/login.html b/public/login.html deleted file mode 100644 index 6604ae1..0000000 --- a/public/login.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - MatConnect Login - - - - - - - -
-
-
-

Welcome Back to MatConnect

-

Sign in to continue

-
- -
- -
-
- - -
-
- - -
- - -
- -
- -
-
-
- OR -
-
- -
- -
-
- - - \ No newline at end of file diff --git a/public/manage-stages.html b/public/manage-stages.html deleted file mode 100644 index a728a56..0000000 --- a/public/manage-stages.html +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - Manage Stages - - - - - - - -
- - -
- -
-
-

Stages

-
-
-
    - -

    Loading stages...

    -
-
-
- -
-
- - -
-
-

Select a Stage

-

Details will appear here.

-
-
-

Assigned Vehicles

-
    - -

    Select a stage to see assigned vehicles.

    -
-
-
-
- - - -
-
-
-
-
- - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/manage-users.html b/public/manage-users.html deleted file mode 100644 index 8183622..0000000 --- a/public/manage-users.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - - - Manage Users - MatConnect - - - - - - - - -
- - -
-
-
-

System Users

-
- -
- - - - -
- -
-
-
- - - - - - - - - - - - - - -
NameEmailRoleStatusActions
Loading users...
-
-
-
-
- - - - - \ No newline at end of file diff --git a/public/mat-admin-dashboard.html b/public/mat-admin-dashboard.html deleted file mode 100644 index b673cc4..0000000 --- a/public/mat-admin-dashboard.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - Matatu Admin Dashboard - MatConnect - - - - - - - -
- - -
-
-

My Dashboard

-

Welcome back! Here's an overview of your fleet's performance today.

-
- - -
-
-
- directions_bus -
-
-

My Vehicles

- -- -
-
-
-
- social_distance -
-
-

Total Distance Today

- -- km -
-
-
-
- trip_origin -
-
-

Total Trips Today

- -- -
-
-
- - -
-
-
-
-

Live Vehicle Status

- View All Stats -
-
- - - - - - - - - - - - -
VehicleStatusCurrent Location
Loading vehicle data...
-
-
-
-
-
-
- - - \ No newline at end of file diff --git a/public/mat-admin-vehicles.html b/public/mat-admin-vehicles.html deleted file mode 100644 index 3cf188a..0000000 --- a/public/mat-admin-vehicles.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - My Vehicles - MatConnect - - - - - - - - - - -
- - -
-
-

My Vehicles

-

Detailed statistics and feedback for your fleet.

-
- -
-
- - -
-
-
- - - -
- -
-
- -
- -
-

Loading vehicle stats...

-
-
- -
-
- - - - - - \ No newline at end of file diff --git a/public/scripts/admin-dashboard.js b/public/scripts/admin-dashboard.js deleted file mode 100644 index 1777909..0000000 --- a/public/scripts/admin-dashboard.js +++ /dev/null @@ -1,171 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - document.querySelector('.avatar').src = '/images/pfp.jpg'; - - const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); - const mainNav = document.querySelector('.main-nav'); - const navList = mainNav.querySelector('ul'); - const userMenu = document.querySelector('.user-menu'); - const logoutLinkDesktop = document.getElementById('logout-link-desktop'); - - const vehicleTableBody = document.querySelector('.data-table tbody'); - const feedbackList = document.getElementById('feedback-list'); - const accessToken = localStorage.getItem('accessToken'); - - // Stat card elements - const totalVehiclesStat = document.getElementById('total-vehicles-stat'); - const onlineVehiclesStat = document.getElementById('online-vehicles-stat'); - const totalStagesStat = document.getElementById('total-stages-stat'); - - - - const apiFetch = async (url, options = {}) => { - const defaultOptions = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - }; - const response = await fetch(url, { ...defaultOptions, ...options }); - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - window.location.href = '/login.html'; - } - const errorData = await response.json(); - throw new Error(errorData.message || 'API request failed'); - } - return response.json(); - }; - - function createVehicleRow(vehicle) { - const statusClass = vehicle.status.toLowerCase() === 'online' ? 'badge-success' : 'badge-danger'; - let locationCell = vehicle.location || 'N/A'; - - // If we have coordinates, make the location a link to Google Maps - if (vehicle.latitude && vehicle.longitude) { - const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${vehicle.latitude},${vehicle.longitude}`; - locationCell = `${vehicle.location}`; - } - - return ` - - ${vehicle.plateNumber} - ${vehicle.route} - ${vehicle.status} - ${locationCell} - - `; - } - - async function fetchAndDisplayDashboardData() { - try { - const data = await apiFetch('/api/admin/dashboard-data'); - - // Update vehicle table and stats - if (data.vehicles && data.vehicles.length > 0) { - vehicleTableBody.innerHTML = data.vehicles.map(createVehicleRow).join(''); - totalVehiclesStat.textContent = data.vehicles.length; - const onlineCount = data.vehicles.filter(v => v.status.toLowerCase() === 'online').length; - onlineVehiclesStat.textContent = onlineCount; - } else { - vehicleTableBody.innerHTML = 'No vehicles found.'; - totalVehiclesStat.textContent = 0; - onlineVehiclesStat.textContent = 0; - } - - // Update stages stat - const stages = await apiFetch('/api/stages'); - totalStagesStat.textContent = stages.length || 0; - - } catch (error) { - console.error('Error fetching dashboard data:', error); - if (vehicleTableBody) { - vehicleTableBody.innerHTML = `Could not load vehicle data.`; - } - } - } - - async function fetchAndDisplayFeedback() { - try { - const feedbacks = await apiFetch('/api/admin/feedback'); - if (feedbacks.length > 0) { - feedbackList.innerHTML = feedbacks.map(item => { - const stars = Array(5).fill(0).map((_, i) => - `star` - ).join(''); - const timeAgo = new Date(item.createdAt).toLocaleDateString(); - - return ` - - `; - }).join(''); - } else { - feedbackList.innerHTML = '

No recent feedback.

'; - } - } catch (error) { - console.error('Error fetching feedback:', error); - feedbackList.innerHTML = '

Could not load feedback.

'; - } - } - - const logoutUser = async () => { - try { - await apiFetch('/api/auth/logout', { method: 'POST' }); - } catch (error) { - console.error('Logout failed, but clearing session anyway.'); - } - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - }; - - const initializeApp = async () => { - // Auth check - const role = localStorage.getItem('role'); - if (!accessToken || role !== 'Admin') { - window.location.href = '/login.html'; - return; - } - - // --- Mobile Menu Toggle --- - mobileMenuBtn.addEventListener('click', () => { - mainNav.classList.toggle('is-active'); - }); - - // --- Desktop User Menu Toggle --- - if (userMenu) { - userMenu.addEventListener('click', (e) => { - // Prevents the document click listener from firing immediately - e.stopPropagation(); - userMenu.classList.toggle('is-active'); - }); - } - - // --- Add Logout Link to Nav --- - const logoutLi = document.createElement('li'); - logoutLi.innerHTML = `Logout`; - navList.appendChild(logoutLi); - logoutLi.querySelector('.logout-link').addEventListener('click', logoutUser); - logoutLinkDesktop.addEventListener('click', logoutUser); - }; - - initializeApp(); - - // Fetch data immediately and then set an interval to refresh - fetchAndDisplayDashboardData(); - setInterval(fetchAndDisplayDashboardData, 60000); // Refresh every 60 seconds - - fetchAndDisplayFeedback(); - - // Close user menu when clicking outside - document.addEventListener('click', () => { - if (userMenu && userMenu.classList.contains('is-active')) { - userMenu.classList.remove('is-active'); - } - }); -}); \ No newline at end of file diff --git a/public/scripts/feedback-page.js b/public/scripts/feedback-page.js deleted file mode 100644 index 3cf1e4f..0000000 --- a/public/scripts/feedback-page.js +++ /dev/null @@ -1,158 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const accessToken = localStorage.getItem('accessToken'); - const ratingStarsContainer = document.querySelector('.rating-stars'); - const profileIcon = document.querySelector('.profile-icon'); - - const feedbackForm = document.getElementById('feedback-form'); - const vehicleSelect = document.getElementById('vehicle-select'); - // Protect the page: redirect to login if not authenticated - if (!accessToken) { - window.location.href = '/login.html'; - return; - } - - const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); - const mainNav = document.querySelector('.main-nav'); - const navList = mainNav.querySelector('ul'); - const headerActions = document.querySelector('.header-actions'); - - const apiFetch = async (url, options = {}) => { - const defaultOptions = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - }; - const response = await fetch(url, { ...defaultOptions, ...options }); - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - window.location.href = '/login.html'; - } - const errorData = await response.json(); - throw new Error(errorData.message || 'API request failed'); - } - return response.json(); - }; - - // --- Mobile Menu Toggle --- - mobileMenuBtn.addEventListener('click', () => { - mainNav.classList.toggle('is-active'); - }); - - // --- Logout Functionality --- - function logoutUser() { - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - } - - // Add logout link to mobile menu - const logoutLi = document.createElement('li'); - const logoutMobileLink = document.createElement('a'); - logoutMobileLink.href = '#'; - logoutMobileLink.textContent = 'Logout'; - logoutMobileLink.addEventListener('click', (e) => { - e.preventDefault(); - logoutUser(); - }); - logoutLi.appendChild(logoutMobileLink); - navList.appendChild(logoutLi); - - // Add logout button for desktop view - const logoutDesktopBtn = document.createElement('button'); - logoutDesktopBtn.innerHTML = ''; - logoutDesktopBtn.title = 'Logout'; - logoutDesktopBtn.classList.add('logout-btn-desktop'); - logoutDesktopBtn.addEventListener('click', logoutUser); - // Add it before the profile icon - headerActions.insertBefore(logoutDesktopBtn, headerActions.querySelector('.user-profile')); - - // --- Star Rating Functionality --- - if (ratingStarsContainer) { - const stars = ratingStarsContainer.querySelectorAll('label'); - const starInputs = ratingStarsContainer.querySelectorAll('input[type="radio"]'); - - starInputs.forEach(input => { - input.addEventListener('change', () => { - const selectedValue = parseInt(input.value, 10); - stars.forEach((star, index) => { - // The labels are in reverse order in the DOM (5, 4, 3, 2, 1). - // The visual order is (1, 2, 3, 4, 5) due to `flex-direction: row-reverse`. - const starValue = parseInt(star.htmlFor.replace('star', ''), 10); - star.classList.toggle('is-selected', starValue <= selectedValue); - }); - }); - }); - } - - async function populateVehicleDropdown() { - try { - const vehicles = await apiFetch('/api/feedback/vehicles'); - if (vehicles.length > 0) { - vehicleSelect.innerHTML = ''; - vehicles.forEach(vehicle => { - const option = document.createElement('option'); - option.value = vehicle.id; - option.textContent = vehicle.plateNumber; - vehicleSelect.appendChild(option); - }); - } else { - vehicleSelect.innerHTML = ''; - } - } catch (error) { - console.error('Error fetching vehicles:', error); - vehicleSelect.innerHTML = ''; - } - } - - async function handleFeedbackSubmit(event) { - event.preventDefault(); - const formData = new FormData(feedbackForm); - const vehicleId = formData.get('vehicle'); - const rating = formData.get('rating'); - const comment = formData.get('comments'); - - if (!vehicleId) { - alert('Please select a vehicle.'); - return; - } - if (!rating) { - alert('Please provide a star rating.'); - return; - } - - try { - const response = await apiFetch('/api/feedback', { - method: 'POST', - body: JSON.stringify({ vehicleId, rating: parseInt(rating), comment }), - }); - alert(response.message); - feedbackForm.reset(); - } catch (error) { - alert(`Error submitting feedback: ${error.message}`); - } - } - - async function fetchProfileImage() { - if (!profileIcon) return; - try { - const res = await fetch('/api/auth/me/passenger', { - headers: { Authorization: `Bearer ${accessToken}` } - }); - - if (!res.ok) throw new Error('Failed to fetch profile image.'); - - const { profilePictureUrl } = await res.json(); - profileIcon.src = profilePictureUrl || './images/pfp.jpg'; - profileIcon.refferrerPolicy = 'no-referrer'; - } catch (err) { - console.error('Error fetching profile image:', err); - profileIcon.src = './images/pfp.jpg'; - } - } - - fetchProfileImage(); - - feedbackForm.addEventListener('submit', handleFeedbackSubmit); - populateVehicleDropdown(); -}); \ No newline at end of file diff --git a/public/scripts/forgot-password.js b/public/scripts/forgot-password.js deleted file mode 100644 index 666c991..0000000 --- a/public/scripts/forgot-password.js +++ /dev/null @@ -1,102 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const form = document.getElementById('reset-form'); - const emailInput = document.getElementById('email-address'); - const resetFields = document.getElementById('reset-fields'); - const tokenInput = document.getElementById('reset-token'); - const userIdInput = document.getElementById('user-id'); - const passwordInput = document.getElementById('new-password'); - const submitButton = document.getElementById('submit-button'); - const messageArea = document.getElementById('message-area'); - const subtitle = document.getElementById('subtitle-text'); - - // This flag tracks which step of the process we are in. - let isLinkSent = false; - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - showMessage('', 'none'); // Clear previous messages - - if (!isLinkSent) { - // --- Step 1: Request the reset link --- - const email = emailInput.value; - if (!email) { - showMessage('Please enter your email address.', 'error'); - return; - } - - try { - setLoading(true, 'Sending...'); - const response = await fetch('/api/auth/requestPasswordReset', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email }), - }); - - const data = await response.json(); - if (!response.ok) throw new Error(data.message); - - // Transition to the next step - showMessage('A reset link has been sent to your email. Please copy the ID and Token from the link into the fields below.', 'success'); - isLinkSent = true; - emailInput.readOnly = true; // Lock the email field - resetFields.style.display = 'block'; - subtitle.textContent = 'Enter the details from your reset link.'; - setLoading(false, 'Reset Password'); - - } catch (error) { - showMessage(error.message, 'error'); - setLoading(false, 'Send Reset Link'); - } - } else { - // --- Step 2: Submit the ID, token, and new password --- - const id = userIdInput.value; - const token = tokenInput.value; - const password = passwordInput.value; - - if (!id || !token || !password) { - showMessage('Please fill in your User ID, Token, and a new password.', 'error'); - return; - } - - try { - setLoading(true, 'Resetting...'); - // The URL is constructed based on your authRoutes.js - const response = await fetch(`/api/auth/resetpassword/${id}/${token}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password }), - }); - - const data = await response.json(); - if (!response.ok) throw new Error(data.message); - - showMessage('Password reset successfully! Redirecting to login...', 'success'); - - // Redirect to login page after a short delay - setTimeout(() => { - window.location.href = '/login.html'; - }, 2000); - - } catch (error) { - showMessage(error.message, 'error'); - setLoading(false, 'Reset Password'); - } - } - }); - - function setLoading(isLoading, buttonText) { - submitButton.disabled = isLoading; - submitButton.textContent = buttonText; - } - - function showMessage(text, type) { - messageArea.textContent = text; - // Add CSS classes for styling success/error messages - messageArea.className = 'message'; - if (type === 'error') { - messageArea.classList.add('error'); - } else if (type === 'success') { - messageArea.classList.add('success'); - } - } -}); \ No newline at end of file diff --git a/public/scripts/index.js b/public/scripts/index.js deleted file mode 100644 index 18e9d37..0000000 --- a/public/scripts/index.js +++ /dev/null @@ -1,97 +0,0 @@ -const loginBtns = document.querySelectorAll('.login'); -const signupBtns = document.querySelectorAll('.signup'); - -const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); -const mainNav = document.querySelector('.main-nav'); -const navLinks = document.querySelector('.nav-links'); - - -const accessToken = localStorage.getItem('accessToken'); -const role = localStorage.getItem('role'); - -const headerButtonsDiv = document.querySelector('.header-buttons'); -const heroBtnsDiv = document.querySelector('.hero-btns'); - -loginBtns.forEach(btn => { - btn.addEventListener('click', ()=> { - window.location.href = 'login.html' - }) -}) - - -signupBtns.forEach(btn => { - btn.addEventListener('click', ()=> { - window.location.href = 'signup.html' - }) -}) -mobileMenuBtn.addEventListener('click', () => { - mainNav.classList.toggle('is-active'); - }) - -// Only try to fetch user data if a token exists -if (accessToken && role) { - // Use relative paths for API calls to work in any environment - const endpoint = role === 'Admin' ? '/api/auth/me/admin' : '/api/auth/me/passenger'; - - fetch(endpoint, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }) - .then(res => { - if (!res.ok) { - // If token is invalid/expired, clear storage - throw new Error('Invalid token'); - } - return res.json(); - }) - .then(data => { - if (data && data.id) { - console.log('Logged in as:', role); - headerButtonsDiv.innerHTML = ` - - `; - - // Add logout button to mobile nav - const logoutLi = document.createElement('li'); - const logoutMobileBtn = document.createElement('a'); - logoutMobileBtn.href = '#'; - logoutMobileBtn.textContent = 'Logout'; - logoutMobileBtn.classList.add('logout'); - logoutLi.appendChild(logoutMobileBtn); - navLinks.appendChild(logoutLi); - - - const logoutBtn = document.querySelector('.logout'); - logoutBtn.addEventListener('click', () => { - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - alert('Logged out successfully'); - window.location.href = 'index.html'; - }); - - // Add event listener for the new mobile logout button - logoutMobileBtn.addEventListener('click', (e) => { - e.preventDefault(); - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = 'index.html'; - }); - heroBtnsDiv.innerHTML = ` - - `; - const dashboardBtn = document.querySelector('.dashboard'); - dashboardBtn.addEventListener('click', () => { - window.location.href = role === 'Admin' ? 'admin-dashboard.html' : 'select-stage.html'; - }); - } - }) - .catch(() => { - // If fetch fails for any reason (e.g., invalid token), clear localStorage - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - }); -} \ No newline at end of file diff --git a/public/scripts/login.js b/public/scripts/login.js deleted file mode 100644 index 6e4ef36..0000000 --- a/public/scripts/login.js +++ /dev/null @@ -1,137 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const form = document.querySelector('.form'); - const emailInput = document.getElementById('email-address'); - const passwordInput = document.getElementById('password'); - const tokenInput = document.getElementById('2fa-token'); - const twoFaContainer = document.getElementById('2fa-container'); - const messageArea = document.getElementById('message-area'); - const submitButton = document.getElementById('submit-button'); - const subtitle = document.getElementById('subtitle-text'); - const googleButton = document.getElementById('google-signin-button'); - - let tempToken = null; // To store the temporary token for 2FA - - // Check for authentication errors from URL parameters (e.g., from Google OAuth) - const params = new URLSearchParams(window.location.search); - if (params.get('error') === 'auth_failed') { - showMessage('Authentication failed. Please try again.', 'error'); - // Clean up the URL - window.history.replaceState({}, document.title, "/login.html"); - } - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - clearMessage(); - - try { - // If tempToken is present, we are in the second step (2FA verification) - if (tempToken) { - await verifyTwoFactor(); - } else { - // Otherwise, this is the first step (email/password login) - await loginWithPassword(); - } - } catch (error) { - console.error('An unexpected error occurred:', error); - showMessage('An unexpected error occurred. Please try again.', 'error'); - } - }); - - googleButton.addEventListener('click', () => { - // Redirect the user to the backend route that starts the Google OAuth flow - window.location.href = '/api/auth/google'; - }); - - async function loginWithPassword() { - const email = emailInput.value; - const password = passwordInput.value; - - const response = await fetch('/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }); - - const data = await response.json(); - - if (response.ok) { - if (data.twoFactorRequired) { - // 2FA is required, move to the second step - tempToken = data.tempToken; - prepareTwoFactorUI(); - } else { - // Login successful, no 2FA - localStorage.setItem('accessToken', data.accessToken); - localStorage.setItem('role', data.role); - redirectToDashboard(data.role); - } - } else { - // Login failed (e.g., wrong password) - showMessage(data.message || 'Login failed.', 'error'); - } - } - - async function verifyTwoFactor() { - const token = tokenInput.value; - - if (!token || token.length !== 6) { - showMessage('Please enter a valid 6-digit code.', 'error'); - return; - } - - const response = await fetch('/api/auth/2fa/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tempToken, token }), - }); - - const data = await response.json(); - - if (response.ok) { - // 2FA verification successful - localStorage.setItem('accessToken', data.accessToken); - localStorage.setItem('role', data.role); - redirectToDashboard(data.role); - } else { - // 2FA verification failed - showMessage(data.message || 'Invalid 2FA code.', 'error'); - tokenInput.value = ''; // Clear the input for retry - tokenInput.focus(); - } - } - - function redirectToDashboard(role) { - if (role === 'Admin') { - window.location.href = '/admin-dashboard.html'; - } else if (role === 'MatAdmin') { - window.location.href = '/mat-admin-dashboard.html'; - } else { - window.location.href = '/select-stage.html'; - } - } - - function prepareTwoFactorUI() { - // Hide email and password fields - emailInput.parentElement.style.display = 'none'; - passwordInput.parentElement.style.display = 'none'; - - // Show the 2FA token input - twoFaContainer.style.display = 'block'; - tokenInput.focus(); - - // Update UI text - subtitle.textContent = 'Enter the 6-digit code from your authenticator app.'; - submitButton.textContent = 'Verify Code'; - } - - function showMessage(message, type = 'info') { - messageArea.textContent = message; - messageArea.className = `message ${type}`; // Assumes you have .error, .success classes in your CSS - messageArea.style.display = 'block'; - } - - function clearMessage() { - messageArea.textContent = ''; - messageArea.style.display = 'none'; - } -}); \ No newline at end of file diff --git a/public/scripts/manage-stages.js b/public/scripts/manage-stages.js deleted file mode 100644 index 8cb4c50..0000000 --- a/public/scripts/manage-stages.js +++ /dev/null @@ -1,345 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - // --- DOM Elements --- - const stageList = document.getElementById('stage-list'); - const detailsTitle = document.getElementById('details-title'); - const detailsCoords = document.getElementById('details-coords'); - const assignedVehiclesList = document.getElementById('assigned-vehicles-list'); - const assignVehicleSelect = document.getElementById('assign-vehicle-select'); - const assignVehicleBtn = document.getElementById('assign-vehicle-btn'); - const detailsFooter = document.getElementById('details-footer'); - - // Header elements - const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); - const mainNav = document.querySelector('.main-nav'); - const userMenu = document.querySelector('.user-menu'); - const logoutLinkDesktop = document.getElementById('logout-link-desktop'); - - // Stage Modal Elements - const addStageBtn = document.getElementById('add-stage-btn'); - const stageModal = document.getElementById('add-stage-modal'); - const closeStageModalBtn = document.getElementById('close-modal-btn'); - const cancelStageBtn = document.getElementById('cancel-btn'); - const addStageForm = document.getElementById('add-stage-form'); - - // Vehicle Modal Elements - const addVehicleBtn = document.getElementById('add-vehicle-btn'); - const vehicleModal = document.getElementById('add-vehicle-modal'); - const closeVehicleModalBtn = document.getElementById('close-vehicle-modal-btn'); - const cancelVehicleBtn = document.getElementById('cancel-vehicle-btn'); - const addVehicleForm = document.getElementById('add-vehicle-form'); - const vehicleOwnerSelect = document.getElementById('vehicle-owner-select'); - - document.querySelector('.avatar').src = '/images/pfp.jpg'; - - // --- State --- - let stages = []; - let vehicles = []; - let selectedStage = null; - let matAdmins = []; - const accessToken = localStorage.getItem('accessToken'); - - // --- API Helper --- - const apiFetch = async (url, options = {}) => { - const defaultOptions = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - }; - const response = await fetch(url, { ...defaultOptions, ...options }); - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - window.location.href = '/login.html'; - } - const errorData = await response.json(); - throw new Error(errorData.message || 'API request failed'); - } - return response.json(); - }; - - // --- Render Functions --- - const renderStageList = () => { - stageList.innerHTML = ''; - if (stages.length === 0) { - stageList.innerHTML = '

No stages found. Add one to get started.

'; - return; - } - stages.forEach(stage => { - const li = document.createElement('li'); - li.className = 'stage-list-item'; - li.dataset.stageId = stage.id; - li.innerHTML = ` -
flag
-

${stage.name}

- chevron_right - `; - li.addEventListener('click', () => handleStageSelect(stage.id)); - stageList.appendChild(li); - }); - }; - - const renderStageDetails = () => { - if (!selectedStage) { - detailsTitle.textContent = 'Select a Stage'; - detailsCoords.textContent = 'Details will appear here.'; - assignedVehiclesList.innerHTML = '

Select a stage to see assigned vehicles.

'; - detailsFooter.classList.add('hidden'); - return; - } - - detailsFooter.classList.remove('hidden'); - detailsTitle.textContent = selectedStage.name; - detailsCoords.innerHTML = `location_on Lat: ${selectedStage.latitude}, Long: ${selectedStage.longitude}`; - - // Highlight selected stage in the list - document.querySelectorAll('.stage-list-item').forEach(item => { - item.classList.toggle('active', item.dataset.stageId == selectedStage.id); - if (item.dataset.stageId == selectedStage.id) item.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - }); - - // Render assigned vehicles - assignedVehiclesList.innerHTML = ''; - if (selectedStage.Vehicles.length === 0) { - assignedVehiclesList.innerHTML = '

No vehicles assigned to this stage.

'; - } else { - selectedStage.Vehicles.forEach(vehicle => { - const li = document.createElement('li'); - li.className = 'vehicle-list-item'; - li.innerHTML = ` -
- local_shipping -

${vehicle.plateNumber}

-
-
- - -
- `; - li.querySelector('.unassign-btn').addEventListener('click', () => handleRemoveVehicle(vehicle.id)); - li.querySelector('.delete-btn').addEventListener('click', () => handleDeleteVehicle(vehicle.id)); - assignedVehiclesList.appendChild(li); - }); - } - - // Render vehicle assignment dropdown - const assignedVehicleIds = new Set(selectedStage.Vehicles.map(v => v.id)); - const availableVehicles = vehicles.filter(v => !assignedVehicleIds.has(v.id)); - assignVehicleSelect.innerHTML = ''; - availableVehicles.forEach(v => { - assignVehicleSelect.innerHTML += ``; - }); - }; - - // --- Event Handlers --- - const handleStageSelect = (stageId) => { - selectedStage = stages.find(s => s.id === stageId); - renderStageDetails(); - }; - - const handleAddStage = async (event) => { - event.preventDefault(); - const name = document.getElementById('stage-name-input').value; - const latitude = document.getElementById('latitude-input').value; - const longitude = document.getElementById('longitude-input').value; - - try { - const newStage = await apiFetch('/api/admin/stages', { - method: 'POST', - body: JSON.stringify({ name, latitude, longitude }), - }); - stages.push({ ...newStage, Vehicles: [] }); // Add to local state - stages.sort((a, b) => a.name.localeCompare(b.name)); // Keep it sorted - renderStageList(); - closeStageModal(); - } catch (error) { - alert(`Error creating stage: ${error.message}`); - } - }; - - const handleAssignVehicle = async () => { - const vehicleId = assignVehicleSelect.value; - if (!vehicleId || !selectedStage) return; - - try { - await apiFetch('/api/admin/stages/assign-vehicle', { - method: 'POST', - body: JSON.stringify({ stageId: selectedStage.id, vehicleId: parseInt(vehicleId) }), - }); - - // Refresh data to reflect the change - const vehicleToAdd = vehicles.find(v => v.id == vehicleId); - selectedStage.Vehicles.push(vehicleToAdd); - renderStageDetails(); - } catch (error) { - alert(`Error assigning vehicle: ${error.message}`); - } - }; - - const handleRemoveVehicle = async (vehicleId) => { - if (!selectedStage) return; - - try { - await apiFetch(`/api/admin/stages/${selectedStage.id}/vehicles/${vehicleId}`, { - method: 'DELETE', - }); - - // Refresh data to reflect the change - selectedStage.Vehicles = selectedStage.Vehicles.filter(v => v.id !== vehicleId); - renderStageDetails(); - } catch (error) { - alert(`Error removing vehicle: ${error.message}`); - } - }; - - const handleDeleteVehicle = async (vehicleId) => { - const vehicle = vehicles.find(v => v.id === vehicleId); - if (!vehicle) return; - - if (!confirm(`Are you sure you want to permanently delete vehicle ${vehicle.plateNumber}? This action cannot be undone.`)) { - return; - } - - try { - await apiFetch(`/api/admin/vehicles/${vehicleId}`, { - method: 'DELETE', - }); - // Refetch all data to ensure UI consistency - await initializeApp(true); - } catch (error) { - alert(`Error deleting vehicle: ${error.message}`); - } - }; - - const handleAddVehicle = async (event) => { - event.preventDefault(); - const plateNumber = document.getElementById('vehicle-plate-input').value; - const carId = document.getElementById('vehicle-carid-input').value; - const ownerId = vehicleOwnerSelect.value; - - try { - const newVehicle = await apiFetch('/api/admin/vehicles', { - method: 'POST', - body: JSON.stringify({ plateNumber, carId, ownerId: ownerId || null }), - }); - vehicles.push(newVehicle); - vehicles.sort((a, b) => a.plateNumber.localeCompare(b.plateNumber)); - renderStageDetails(); // Re-render dropdown with new vehicle - closeVehicleModal(); - } catch (error) { - alert(`Error creating vehicle: ${error.message}`); - } - }; - - // --- Modal Logic --- - const openStageModal = () => stageModal.classList.remove('hidden'); - const closeStageModal = () => { - stageModal.classList.add('hidden'); - addStageForm.reset(); - }; - - const openVehicleModal = () => vehicleModal.classList.remove('hidden'); - const closeVehicleModal = () => { - vehicleModal.classList.add('hidden'); - addVehicleForm.reset(); - }; - - const populateOwnerDropdown = () => { - vehicleOwnerSelect.innerHTML = ''; - matAdmins.forEach(admin => { - vehicleOwnerSelect.innerHTML += ``; - }); - }; - - addStageBtn.addEventListener('click', openStageModal); - closeStageModalBtn.addEventListener('click', closeStageModal); - cancelStageBtn.addEventListener('click', closeStageModal); - stageModal.addEventListener('click', (e) => { - if (e.target === stageModal) closeStageModal(); - }); - - addVehicleBtn.addEventListener('click', openVehicleModal); - closeVehicleModalBtn.addEventListener('click', closeVehicleModal); - cancelVehicleBtn.addEventListener('click', closeVehicleModal); - vehicleModal.addEventListener('click', (e) => { - if (e.target === vehicleModal) closeVehicleModal(); - }); - - const logoutUser = async () => { - try { - await apiFetch('/api/auth/logout', { method: 'POST' }); - } catch (error) { - console.error('Logout failed, but clearing session anyway.'); - } - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - }; - // --- Initialization --- - const initializeApp = async (isRefresh = false) => { - // Auth check - const role = localStorage.getItem('role'); - if (!accessToken || role !== 'Admin') { - window.location.href = '/login.html'; - return; - } - if (isRefresh) selectedStage = null; // Reset selection on refresh - - // --- Mobile Menu Toggle --- - mobileMenuBtn.addEventListener('click', () => { - mainNav.classList.toggle('is-active'); - }); - - // --- Desktop User Menu Toggle --- - if (userMenu) { - userMenu.addEventListener('click', (e) => { - e.stopPropagation(); - userMenu.classList.toggle('is-active'); - }); - } - - // --- Add Logout Links --- - if (!isRefresh) { - const navList = mainNav.querySelector('ul'); - const logoutLi = document.createElement('li'); - logoutLi.innerHTML = `Logout`; - navList.appendChild(logoutLi); - logoutLi.querySelector('.logout-link').addEventListener('click', logoutUser); - logoutLinkDesktop.addEventListener('click', logoutUser); - - // Add event listeners - addStageForm.addEventListener('submit', handleAddStage); - addVehicleForm.addEventListener('submit', handleAddVehicle); - assignVehicleBtn.addEventListener('click', handleAssignVehicle); - } - - // Initial data fetch - try { - const [stagesData, vehiclesData, matAdminsData] = await Promise.all([ - apiFetch('/api/admin/stages'), - apiFetch('/api/admin/vehicles'), - apiFetch('/api/admin/mat-admins'), - ]); - stages = stagesData; - vehicles = vehiclesData; - matAdmins = matAdminsData; - renderStageList(); - populateOwnerDropdown(); - renderStageDetails(); // Render the initial empty state for details - } catch (error) { - stageList.innerHTML = `

Failed to load data: ${error.message}

`; - } - }; - - // Close user menu when clicking outside - document.addEventListener('click', () => { - if (userMenu && userMenu.classList.contains('is-active')) { - userMenu.classList.remove('is-active'); - } - }); - - initializeApp(); -}); \ No newline at end of file diff --git a/public/scripts/manage-users.js b/public/scripts/manage-users.js deleted file mode 100644 index b280830..0000000 --- a/public/scripts/manage-users.js +++ /dev/null @@ -1,220 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const accessToken = localStorage.getItem('accessToken'); - const userAvatar = document.getElementById('user-avatar'); - const userName = document.querySelector('.user-name'); - const logoutLinkDesktop = document.getElementById('logout-link-desktop'); - const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); - const mainNav = document.querySelector('.main-nav'); - const usersTableBody = document.getElementById('users-table-body'); - const searchInput = document.getElementById('user-search-input'); - const roleFilterButtons = document.getElementById('role-filter-buttons'); - - // Modal elements - const addUserModal = document.getElementById('add-user-modal'); - const addUserBtn = document.getElementById('add-user-btn'); - const closeModalBtn = document.getElementById('close-user-modal-btn'); - const cancelBtn = document.getElementById('cancel-user-btn'); - const addUserForm = document.getElementById('add-user-form'); - const passwordToggleBtn = document.querySelector('.password-toggle-btn'); - - let searchTimeout; - - const apiFetch = async (url, options = {}) => { - const defaultOptions = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - }; - const response = await fetch(url, { ...defaultOptions, ...options }); - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - window.location.href = '/login.html'; - } - const errorData = await response.json(); - throw new Error(errorData.message || 'API request failed'); - } - // Handle responses with no content - const contentType = response.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return response.json(); - } - return; - }; - - const logoutUser = async () => { - try { - await apiFetch('/api/auth/logout', { method: 'POST' }); - } catch (error) { - console.error('Logout failed, but clearing session anyway.'); - } - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - }; - - function createUserRow(user) { - const statusClass = user.disabled ? 'badge-danger' : 'badge-success'; - const statusText = user.disabled ? 'Blocked' : 'Active'; - const blockButtonText = user.disabled ? 'Unblock' : 'Block'; - - return ` - - ${user.name} - ${user.email} - ${user.role} - ${statusText} - - - - - - `; - } - - async function fetchAndDisplayUsers(query = '', role = '') { - try { - const params = new URLSearchParams({ q: query, role: role }); - const users = await apiFetch(`/api/admin/users?${params.toString()}`); - - if (users.length > 0) { - usersTableBody.innerHTML = users.map(createUserRow).join(''); - } else { - usersTableBody.innerHTML = 'No users found.'; - } - } catch (error) { - console.error('Error fetching users:', error); - usersTableBody.innerHTML = 'Could not load users.'; - } - } - - async function handleTableClick(event) { - const target = event.target; - const row = target.closest('tr'); - if (!row) return; - - const userId = row.dataset.userId; - - if (target.classList.contains('btn-block')) { - const shouldBeDisabled = target.dataset.disabled === 'true'; - if (confirm(`Are you sure you want to ${shouldBeDisabled ? 'block' : 'unblock'} this user?`)) { - try { - await apiFetch(`/api/admin/users/${userId}`, { - method: 'PUT', - body: JSON.stringify({ disabled: shouldBeDisabled }), - }); - fetchAndDisplayUsers(searchInput.value); // Refresh list - } catch (error) { - alert(`Error updating user: ${error.message}`); - } - } - } - - if (target.classList.contains('btn-delete')) { - if (confirm('Are you sure you want to permanently delete this user? This action cannot be undone.')) { - try { - await apiFetch(`/api/admin/users/${userId}`, { method: 'DELETE' }); - row.remove(); // Optimistically remove from UI - } catch (error) { - alert(`Error deleting user: ${error.message}`); - } - } - } - } - - async function handleAddUser(event) { - event.preventDefault(); - const name = document.getElementById('user-name-input').value; - const email = document.getElementById('user-email-input').value; - const password = document.getElementById('user-password-input').value; - const role = document.getElementById('user-role-select').value; - - try { - await apiFetch('/api/admin/users', { - method: 'POST', - body: JSON.stringify({ name, email, password, role }), - }); - closeModal(); - fetchAndDisplayUsers(); // Refresh the list - } catch (error) { - alert(`Error creating user: ${error.message}`); - } - } - - function togglePasswordVisibility() { - const passwordInput = document.getElementById('user-password-input'); - const icon = passwordToggleBtn.querySelector('.material-symbols-outlined'); - if (passwordInput.type === 'password') { - passwordInput.type = 'text'; - icon.textContent = 'visibility_off'; - passwordToggleBtn.title = 'Hide password'; - } else { - passwordInput.type = 'password'; - icon.textContent = 'visibility'; - passwordToggleBtn.title = 'Show password'; - } - } - - // Modal handling - const openModal = () => addUserModal.classList.remove('hidden'); - const closeModal = () => { - addUserModal.classList.add('hidden'); - addUserForm.reset(); - }; - - const initializeApp = async () => { - const role = localStorage.getItem('role'); - if (!accessToken || role !== 'Admin') { - window.location.href = '/login.html'; - return; - } - - try { - const user = await apiFetch('/api/auth/me/admin'); - userAvatar.src = user.profilePictureUrl || '/images/pfp.jpg'; - userName.textContent = user.name.split(' ')[0]; - } catch (e) { - console.error("Failed to fetch user profile", e); - } - - // Event Listeners - logoutLinkDesktop.addEventListener('click', logoutUser); - mobileMenuBtn.addEventListener('click', () => mainNav.classList.toggle('is-active')); - usersTableBody.addEventListener('click', handleTableClick); - addUserBtn.addEventListener('click', openModal); - closeModalBtn.addEventListener('click', closeModal); - cancelBtn.addEventListener('click', closeModal); - addUserForm.addEventListener('submit', handleAddUser); - passwordToggleBtn.addEventListener('click', togglePasswordVisibility); - - // Debounced search - searchInput.addEventListener('keyup', () => { - clearTimeout(searchTimeout); - const activeRole = roleFilterButtons.querySelector('button.active').dataset.role; - searchTimeout = setTimeout(() => { - fetchAndDisplayUsers(searchInput.value, activeRole); - }, 300); // 300ms delay - }); - - // Role filter buttons - roleFilterButtons.addEventListener('click', (e) => { - if (e.target.tagName === 'BUTTON') { - roleFilterButtons.querySelector('button.active').classList.remove('active'); - e.target.classList.add('active'); - fetchAndDisplayUsers(searchInput.value, e.target.dataset.role); - } - }); - - // Add mobile logout link - const navList = mainNav.querySelector('ul'); - const logoutLi = document.createElement('li'); - logoutLi.innerHTML = `Logout`; - navList.appendChild(logoutLi); - logoutLi.querySelector('#logout-link-mobile').addEventListener('click', logoutUser); - - // Initial data fetch - fetchAndDisplayUsers(); - }; - - initializeApp(); -}); \ No newline at end of file diff --git a/public/scripts/mat-admin-dashboard.js b/public/scripts/mat-admin-dashboard.js deleted file mode 100644 index f298e45..0000000 --- a/public/scripts/mat-admin-dashboard.js +++ /dev/null @@ -1,119 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const accessToken = localStorage.getItem('accessToken'); - const userAvatar = document.getElementById('user-avatar'); - const userName = document.querySelector('.user-name'); - const logoutLinkDesktop = document.getElementById('logout-link-desktop'); - const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); - const mainNav = document.querySelector('.main-nav'); - - // Stat card elements - const totalVehiclesStat = document.getElementById('total-vehicles-stat'); - const totalDistanceStat = document.getElementById('total-distance-stat'); - const totalTripsStat = document.getElementById('total-trips-stat'); - const vehicleTableBody = document.getElementById('vehicle-table-body'); - - const apiFetch = async (url, options = {}) => { - const defaultOptions = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - }; - const response = await fetch(url, { ...defaultOptions, ...options }); - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - window.location.href = '/login.html'; - } - const errorData = await response.json(); - throw new Error(errorData.message || 'API request failed'); - } - return response.json(); - }; - - const logoutUser = async () => { - try { - await apiFetch('/api/auth/logout', { method: 'POST' }); - } catch (error) { - console.error('Logout failed, but clearing session anyway.'); - } - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - }; - - async function fetchDashboardSummary() { - try { - const summary = await apiFetch('/api/mat-admin/dashboard-summary'); - totalVehiclesStat.textContent = summary.totalVehicles; - totalDistanceStat.textContent = `${(summary.totalDistanceToday / 1000).toFixed(2)} km`; - totalTripsStat.textContent = summary.totalTripsToday; - } catch (error) { - console.error('Error fetching summary:', error); - totalVehiclesStat.textContent = 'Error'; - } - } - - function createVehicleRow(vehicle) { - console.log(vehicle); - const statusClass = vehicle.status.toLowerCase() === 'online' ? 'badge-success' : 'badge-danger'; - let locationCell = vehicle.location || 'N/A'; - - if (vehicle.latitude && vehicle.longitude) { - const mapsUrl = `https://www.google.com/maps/search/?api=1&query=${vehicle.latitude},${vehicle.longitude}`; - locationCell = `${vehicle.location}`; - } - - return ` - - ${vehicle.plateNumber} - ${vehicle.status} - ${locationCell} - - `; - } - - async function fetchLiveVehicleStatus() { - try { - const data = await apiFetch('/api/mat-admin/dashboard-data'); - if (data.vehicles && data.vehicles.length > 0) { - console.log(data); - - vehicleTableBody.innerHTML = data.vehicles.map(createVehicleRow).join(''); - } else { - vehicleTableBody.innerHTML = 'No vehicles found.'; - } - } catch (error) { - console.error('Error fetching live vehicle data:', error); - vehicleTableBody.innerHTML = 'Could not load vehicle data.'; - } - } - - const initializeApp = async () => { - const role = localStorage.getItem('role'); - if (!accessToken || role !== 'MatAdmin') { - window.location.href = '/login.html'; - return; - } - - const user = await apiFetch('/api/auth/me/mat-admin'); - userAvatar.src = user.profilePictureUrl || '/images/pfp.jpg'; - userName.textContent = user.name.split(' ')[0]; - - logoutLinkDesktop.addEventListener('click', logoutUser); - mobileMenuBtn.addEventListener('click', () => mainNav.classList.toggle('is-active')); - - // Add mobile logout link - const navList = mainNav.querySelector('ul'); - const logoutLi = document.createElement('li'); - logoutLi.innerHTML = `Logout`; - navList.appendChild(logoutLi); - logoutLi.querySelector('#logout-link-mobile').addEventListener('click', logoutUser); - - // Fetch data - fetchDashboardSummary(); - fetchLiveVehicleStatus(); - setInterval(fetchLiveVehicleStatus, 30000); // Refresh live status every 30s - }; - - initializeApp(); -}); \ No newline at end of file diff --git a/public/scripts/mat-admin-vehicles.js b/public/scripts/mat-admin-vehicles.js deleted file mode 100644 index 9a6b8d3..0000000 --- a/public/scripts/mat-admin-vehicles.js +++ /dev/null @@ -1,294 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const accessToken = localStorage.getItem('accessToken'); - const userAvatar = document.getElementById('user-avatar'); - const userName = document.querySelector('.user-name'); - const logoutLinkDesktop = document.getElementById('logout-link-desktop'); - const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); - const mainNav = document.querySelector('.main-nav'); - const statsContainer = document.getElementById('vehicle-stats-container'); - const exportFleetBtn = document.getElementById('export-fleet-report-btn'); - - // To keep track of Chart.js instances - const chartInstances = {}; - - const apiFetch = async (url, options = {}) => { - const defaultOptions = { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - }; - const response = await fetch(url, { ...defaultOptions, ...options }); - if (!response.ok) { - if (response.status === 401 || response.status === 403) { - window.location.href = '/login.html'; - } - const errorData = await response.json(); - throw new Error(errorData.message || 'API request failed'); - } - return response.json(); - }; - - const logoutUser = async () => { - try { - await apiFetch('/api/auth/logout', { method: 'POST' }); - } catch (error) { - console.error('Logout failed, but clearing session anyway.'); - } - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - }; - - function createVehicleStatCard(vehicle) { - const distance = vehicle.DailyDistances[0] ? (vehicle.DailyDistances[0].distanceCovered / 1000).toFixed(2) : '0.00'; - const trips = vehicle.DailyTrips[0] ? vehicle.DailyTrips[0].tripCount : 0; - - const feedbackHtml = vehicle.Feedbacks.length > 0 - ? vehicle.Feedbacks.map(fb => ` - - `).join('') - : '

No feedback for this vehicle yet.

'; - - return ` -
-
-

${vehicle.plateNumber}

- ID: ${vehicle.carId} -
-
-
-

Distance Today

-

${distance} km

-
-
-

Trips Today

-

${trips}

-
-
- -
-

Performance Charts

-
-
- - - -
- -
-
- -
-
-
- `; - } - - async function fetchAndDisplayVehicleStats() { - try { - statsContainer.innerHTML = '

Loading vehicle stats...

'; - const vehicles = await apiFetch('/api/mat-admin/vehicle-stats'); - - if (vehicles && vehicles.length > 0) { - statsContainer.innerHTML = vehicles.map(createVehicleStatCard).join(''); - // After rendering, initialize charts and add event listeners - vehicles.forEach(vehicle => { - initializeChartForVehicle(vehicle.id); - }); - } else { - statsContainer.innerHTML = '

You have not added any vehicles yet.

'; - } - } catch (error) { - console.error('Error fetching vehicle stats:', error); - statsContainer.innerHTML = '

Could not load vehicle statistics.

'; - } - } - - function initializeChartForVehicle(vehicleId) { - const chartControls = document.querySelector(`.chart-controls[data-vehicle-id="${vehicleId}"]`); - const periodFilters = chartControls.querySelector('.chart-period-filters'); - const exportBtn = chartControls.querySelector('.export-chart-btn'); - - periodFilters.addEventListener('click', (e) => { - if (e.target.tagName === 'BUTTON') { - const period = e.target.dataset.period; - // Update active button - periodFilters.querySelector('button.active').classList.remove('active'); - e.target.classList.add('active'); - // Fetch new data and update chart - updateChart(vehicleId, period); - } - }); - - exportBtn.addEventListener('click', () => { - const chart = chartInstances[vehicleId]; - if (chart) { - const a = document.createElement('a'); - a.href = chart.toBase64Image(); - a.download = `chart_vehicle_${vehicleId}_${new Date().toISOString().slice(0, 10)}.png`; - a.click(); - } - }); - - // Initial chart load (daily) - updateChart(vehicleId, 'daily'); - } - - async function updateChart(vehicleId, period) { - try { - const data = await apiFetch(`/api/mat-admin/vehicles/${vehicleId}/chart-data?period=${period}`); - renderChart(vehicleId, data, period); - } catch (error) { - console.error(`Error fetching chart data for vehicle ${vehicleId}:`, error); - const canvas = document.getElementById(`chart-${vehicleId}`); - const ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillText('Could not load chart data.', 10, 50); - } - } - - function renderChart(vehicleId, data, period) { - const ctx = document.getElementById(`chart-${vehicleId}`).getContext('2d'); - - // Destroy previous chart instance if it exists - if (chartInstances[vehicleId]) { - chartInstances[vehicleId].destroy(); - } - - const labels = data.distance.map(d => d.period); - - chartInstances[vehicleId] = new Chart(ctx, { - type: 'bar', - data: { - labels: labels, - datasets: [ - { - label: 'Distance (km)', - data: data.distance.map(d => d.total), - backgroundColor: 'rgba(74, 108, 247, 0.6)', // Primary color with opacity - borderColor: 'rgba(74, 108, 247, 1)', - borderWidth: 1, - yAxisID: 'yDistance', - }, - { - label: 'Trips', - data: data.trips.map(d => d.total), - backgroundColor: 'rgba(56, 161, 105, 0.6)', // Green color with opacity - borderColor: 'rgba(56, 161, 105, 1)', - borderWidth: 1, - yAxisID: 'yTrips', - } - ] - }, - options: { - scales: { - yDistance: { - type: 'linear', - display: true, - position: 'left', - title: { display: true, text: 'Distance (km)' } - }, - yTrips: { - type: 'linear', - display: true, - position: 'right', - title: { display: true, text: 'Trips' }, - grid: { drawOnChartArea: false } // only show the grid for the left axis - } - } - } - }); - } - - function generateCSV(data) { - const headers = ['Period', 'Total Distance (km)', 'Total Trips']; - const rows = Object.entries(data).map(([period, values]) => { - const distance = values.distance || 0; - const trips = values.trips || 0; - return [period, distance, trips]; - }); - - // Sort rows by period - rows.sort((a, b) => a[0].localeCompare(b[0])); - - let csvContent = "data:text/csv;charset=utf-8," + headers.join(",") + "\n"; - csvContent += rows.map(row => row.join(",")).join("\n"); - - return encodeURI(csvContent); - } - - async function handleExportFleetReport() { - const filters = document.getElementById('fleet-report-filters'); - const period = filters.querySelector('button.active').dataset.period; - - try { - const data = await apiFetch(`/api/mat-admin/fleet-aggregate-data?period=${period}`); - if (Object.keys(data).length === 0) { - alert('No data available to export for the selected period.'); - return; - } - - const csvUri = generateCSV(data); - const link = document.createElement("a"); - link.setAttribute("href", csvUri); - link.setAttribute("download", `fleet_report_${period}_${new Date().toISOString().slice(0, 10)}.csv`); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - } catch (error) { - console.error('Error exporting fleet report:', error); - alert('Failed to generate the report. Please try again.'); - } - } - - document.getElementById('fleet-report-filters').addEventListener('click', (e) => { - if (e.target.tagName !== 'BUTTON') return; - document.querySelector('#fleet-report-filters button.active').classList.remove('active'); - e.target.classList.add('active'); - }); - - const initializeApp = async () => { - const role = localStorage.getItem('role'); - if (!accessToken || role !== 'MatAdmin') { - window.location.href = '/login.html'; - return; - } - - try { - const user = await apiFetch('/api/auth/me/mat-admin'); - userAvatar.src = user.profilePictureUrl || '/images/pfp.jpg'; - userName.textContent = user.name.split(' ')[0]; - } catch (e) { - console.error("Failed to fetch user profile", e); - // Allow continuing, but with default avatar/name - } - - logoutLinkDesktop.addEventListener('click', logoutUser); - mobileMenuBtn.addEventListener('click', () => mainNav.classList.toggle('is-active')); - - // Add mobile logout link - const navList = mainNav.querySelector('ul'); - const logoutLi = document.createElement('li'); - logoutLi.innerHTML = `Logout`; - navList.appendChild(logoutLi); - logoutLi.querySelector('#logout-link-mobile').addEventListener('click', logoutUser); - - // Set date input to today - document.getElementById('stats-date').valueAsDate = new Date(); - - exportFleetBtn.addEventListener('click', handleExportFleetReport); - - fetchAndDisplayVehicleStats(); - }; - - initializeApp(); -}); \ No newline at end of file diff --git a/public/scripts/select-stage.js b/public/scripts/select-stage.js deleted file mode 100644 index 4f30892..0000000 --- a/public/scripts/select-stage.js +++ /dev/null @@ -1,130 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const stageGrid = document.getElementById('stage-grid'); - const accessToken = localStorage.getItem('accessToken'); - - const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); - const mainNav = document.querySelector('.main-nav'); - const headerActions = document.querySelector('.header-actions'); - - const profileIcon = document.querySelector('.profile-icon'); - - if (!accessToken) { - window.location.href = '/login.html'; - return; - } - - // --- Mobile Menu Toggle --- - mobileMenuBtn.addEventListener('click', () => { - mainNav.classList.toggle('is-active'); - }); - - // --- Add Logout Buttons --- - addLogoutButtons(); - - // A simple function to create a stage card element - function createStageCard(stage) { - const card = document.createElement('a'); - card.className = 'stage-card'; - // Pass the stage ID in the URL to the next page - card.href = `/stage-info.html?stageId=${stage.id}`; - - // Use a placeholder image for now. You can add image URLs to your Stage model later. - const imageNumber = Math.floor(Math.random() * 6) + 1; // Cycle through 6 placeholder images - - card.innerHTML = ` -
- ${stage.name} -
-
-
-

${stage.name}

-

Lat: ${stage.latitude}, Long: ${stage.longitude}

-
- `; - return card; - } - - // Fetches stages from the API and displays them - async function fetchAndDisplayStages() { - try { - const response = await fetch('/api/stages', { - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }); - - if (!response.ok) { - throw new Error('Failed to fetch stages.'); - } - - const stages = await response.json(); - stageGrid.innerHTML = ''; // Clear the "Loading..." message - - if (stages.length > 0) { - stages.forEach(stage => { - const card = createStageCard(stage); - stageGrid.appendChild(card); - }); - } else { - stageGrid.innerHTML = '

No stages are available at the moment.

'; - } - } catch (error) { - console.error('Error:', error); - stageGrid.innerHTML = '

Could not load stages. Please try again later.

'; - } - } - - // --- Logout Functionality --- - function logoutUser() { - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - } - - function addLogoutButtons() { - // 1. Add logout button to mobile navigation - const navList = mainNav.querySelector('ul'); - const logoutLi = document.createElement('li'); - const logoutMobileLink = document.createElement('a'); - logoutMobileLink.href = '#'; - logoutMobileLink.textContent = 'Logout'; - logoutMobileLink.classList.add('logout-link'); - logoutMobileLink.addEventListener('click', (e) => { - e.preventDefault(); - logoutUser(); - }); - logoutLi.appendChild(logoutMobileLink); - navList.appendChild(logoutLi); - - // 2. Add logout button for desktop view - const logoutDesktopBtn = document.createElement('button'); - logoutDesktopBtn.innerHTML = ''; - logoutDesktopBtn.title = 'Logout'; - logoutDesktopBtn.classList.add('logout-btn-desktop'); - logoutDesktopBtn.addEventListener('click', logoutUser); - headerActions.insertBefore(logoutDesktopBtn, headerActions.firstChild); - } - - async function fetchProfileImage() { - if (!profileIcon) return; - try { - const res = await fetch('/api/auth/me/passenger', { - headers: { Authorization: `Bearer ${accessToken}` } - }); - - if (!res.ok) throw new Error('Failed to fetch profile image.'); - - const { profilePictureUrl } = await res.json(); - profileIcon.src = profilePictureUrl || './images/pfp.jpg'; - profileIcon.refferrerPolicy = 'no-referrer'; - } catch (err) { - console.error('Error fetching profile image:', err); - profileIcon.src = './images/pfp.jpg'; - } - } - - // Fetch the profile image when the page loads - fetchProfileImage(); - - fetchAndDisplayStages(); -}); \ No newline at end of file diff --git a/public/scripts/signup.js b/public/scripts/signup.js deleted file mode 100644 index 049cec7..0000000 --- a/public/scripts/signup.js +++ /dev/null @@ -1,43 +0,0 @@ -const fullNameEl = document.getElementById("full-name"); -const emailAddressEl = document.getElementById("email-address"); -const passwordEl = document.getElementById("password"); -const confirmPasswordEl = document.getElementById("confirm-password"); -const signupForm = document.querySelector(".form"); - -signupForm.addEventListener("submit", async (e) => { - e.preventDefault(); - - const fullName = fullNameEl.value.trim(); - const email = emailAddressEl.value.trim(); - const password = passwordEl.value; - const confirmPassword = confirmPasswordEl.value; - - if (!fullName || !email || !password || !confirmPassword) { - alert("Please fill in all fields."); - return; - } - - if (password !== confirmPassword) { - alert("Passwords do not match."); - return; - } - - try { - const response = await fetch("http://localhost:5000/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name: fullName, email, password, role: "Passenger" }), - }); - - const result = await response.json(); - - if (response.ok) { - alert("Signup successful! Please log in."); - window.location.href = "../login.html"; - } else { - alert(result.message || "Signup failed."); - } - } catch (error) { - alert("An error occurred. Please try again."); - } -}); \ No newline at end of file diff --git a/public/scripts/stage-info.js b/public/scripts/stage-info.js deleted file mode 100644 index 4292c93..0000000 --- a/public/scripts/stage-info.js +++ /dev/null @@ -1,167 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const arrivalList = document.querySelector('.arrival-list'); - const avatar = document.querySelector('.avatar'); - const stageHeader = document.querySelector('.page-header h2'); - const accessToken = localStorage.getItem('accessToken'); - const logoutBtn = document.getElementById('logout-btn'); - const notificationBell = document.getElementById('notification-bell'); - const notificationIcon = notificationBell.querySelector('.material-symbols-outlined'); - const profileIcon = document.querySelector('.profile-icon'); - - avatar.addEventListener('click', () => { - window.location.href = '/user-profile.html'; - }); - - if (logoutBtn) { - logoutBtn.addEventListener('click', logoutUser); - } - if (notificationBell) { - notificationBell.addEventListener('click', toggleSubscription); - } - - // Get the stageId from the URL query parameters - const params = new URLSearchParams(window.location.search); - const stageId = params.get('stageId'); - - if (!stageId) { - alert('No stage selected. Redirecting...'); - window.location.href = '/select-stage.html'; - return; - } - - if (!accessToken || localStorage.getItem('role') !== 'Passenger') { - window.location.href = '/login.html'; - return; - } - - function createArrivalItem(vehicle) { - const statusClass = vehicle.status.toLowerCase().replace(' ', '-'); - return ` -
  • -
    -
    - directions_bus -
    -
    -

    ${vehicle.plateNumber}

    -

    ETA: ${vehicle.eta}

    -
    -
    -
    - - - ${vehicle.status} - -
    -
  • - `; - } - - async function fetchAndDisplayEtas() { - try { - const response = await fetch(`/api/eta/${stageId}`, { - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }); - - if (!response.ok) { - throw new Error('Failed to fetch ETA data.'); - } - - const data = await response.json(); - stageHeader.textContent = data.stageName; - arrivalList.innerHTML = data.arrivals.map(createArrivalItem).join(''); - - } catch (error) { - console.error('Error:', error); - arrivalList.innerHTML = '
  • Could not load arrival times. Please try again later.
  • '; - } - } - - async function fetchSubscriptionStatus() { - try { - const response = await fetch(`/api/stages/${stageId}/subscription`, { - headers: { 'Authorization': `Bearer ${accessToken}` } - }); - if (!response.ok) throw new Error('Failed to get subscription status.'); - const data = await response.json(); - updateBellIcon(data.isSubscribed); - } catch (error) { - console.error('Error fetching subscription status:', error); - } - } - - function updateBellIcon(isSubscribed) { - if (isSubscribed) { - notificationIcon.textContent = 'notifications_active'; - notificationBell.classList.add('active'); - notificationBell.title = 'SMS alerts are ON for the next arrival. Click to turn off.'; - } else { - notificationIcon.textContent = 'notifications_off'; - notificationBell.classList.remove('active'); - notificationBell.title = 'SMS alerts are OFF. Click to get an alert for the next arrival.'; - } - } - - async function fetchProfileImage() { - if (!profileIcon) return; - try { - const res = await fetch('/api/auth/me/passenger', { - headers: { Authorization: `Bearer ${accessToken}` } - }); - - if (!res.ok) throw new Error('Failed to fetch profile image.'); - - const { profilePictureUrl } = await res.json(); - profileIcon.src = profilePictureUrl || './images/pfp.jpg'; - profileIcon.refferrerPolicy = 'no-referrer'; - } catch (err) { - console.error('Error fetching profile image:', err); - profileIcon.src = './images/pfp.jpg'; - } - } - - async function toggleSubscription() { - const isSubscribed = notificationBell.classList.contains('active'); - const endpoint = isSubscribed ? 'unsubscribe' : 'subscribe'; - const method = 'POST'; - - try { - notificationBell.disabled = true; // Prevent double-clicking - const response = await fetch(`/api/stages/${stageId}/${endpoint}`, { - method, - headers: { 'Authorization': `Bearer ${accessToken}` } - }); - - const result = await response.json(); - - if (!response.ok) { - throw new Error(result.message || 'Failed to update subscription.'); - } - - alert(result.message); - updateBellIcon(!isSubscribed); - - } catch (error) { - console.error('Error toggling subscription:', error); - alert(`Error: ${error.message}`); - } finally { - notificationBell.disabled = false; - } - } - - // Fetch profile image - fetchProfileImage(); - - // Fetch data immediately and then every 30 seconds - fetchAndDisplayEtas(); - fetchSubscriptionStatus(); - setInterval(fetchAndDisplayEtas, 30000); - - async function logoutUser() { - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - } -}); \ No newline at end of file diff --git a/public/scripts/user-profile.js b/public/scripts/user-profile.js deleted file mode 100644 index 3e4a619..0000000 --- a/public/scripts/user-profile.js +++ /dev/null @@ -1,223 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - // --- DOM Elements --- - const profileForm = document.querySelector('.profile-details-section .form-grid'); - const passwordForm = document.querySelector('.password-reset-section .form-grid'); - - // 2FA Elements - const twoFaStatus = document.getElementById('2fa-status'); - const twoFaToggleBtn = document.getElementById('2fa-toggle-btn'); - const twoFaModal = document.getElementById('2fa-modal'); - const twoFaCloseModalBtn = document.getElementById('2fa-close-modal-btn'); - const twoFaCancelBtn = document.getElementById('2fa-cancel-btn'); - const twoFaVerifyBtn = document.getElementById('2fa-verify-btn'); - const qrCodeContainer = document.getElementById('qr-code-container'); - const twoFaTokenInput = document.getElementById('2fa-verify-token'); - - // Section Elements - const twoFaSection = document.getElementById('2fa-section'); - const passwordResetSection = document.getElementById('password-reset-section'); - const logoutBtn = document.getElementById('logout-btn'); - - // --- State --- - const token = localStorage.getItem('accessToken'); - - // 1. Authentication Check: Redirect if no token is found. - if (!token) { - window.location.href = '/login.html'; - return; - } - - // 2. Fetch and display user data. - fetchUserProfile(token); - - // 3. Add event listeners for the forms. - if (profileForm) { - profileForm.addEventListener('submit', (e) => handleProfileUpdate(e, token)); - } - - // Logout listener - if (logoutBtn) { - logoutBtn.addEventListener('click', logoutUser); - } - - // 2FA Listeners - if (twoFaToggleBtn) { - twoFaToggleBtn.addEventListener('click', () => handle2FaToggle(token)); - } - if (twoFaCloseModalBtn) { - twoFaCloseModalBtn.addEventListener('click', close2FaModal); - } - if (twoFaCancelBtn) { - twoFaCancelBtn.addEventListener('click', close2FaModal); - } - if (twoFaModal) { - twoFaModal.addEventListener('click', (e) => { - if (e.target === twoFaModal) close2FaModal(); - }); - } - if (twoFaVerifyBtn) { - twoFaVerifyBtn.addEventListener('click', () => handle2FaVerify(token)); - } - - /** - * Generic API fetch helper - */ - async function apiFetch(url, options) { - const response = await fetch(url, options); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || 'An API error occurred'); - } - return response.json(); - }; - - /** - * Fetches user data from the server and populates the form. - * @param {string} token The JWT access token. - */ - async function fetchUserProfile(token) { - try { - const user = await apiFetch('/api/auth/me/passenger', { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - }); - populateProfileForm(user); - - } catch (error) { - console.error('Error fetching profile:', error); - alert('Could not load your profile. Please try logging in again.'); - } - } - - /** - * Fills the input fields with the user's data. - * @param {object} user The user object from the API. - */ - function populateProfileForm(user) { - document.getElementById('name').value = user.name || ''; - document.getElementById('email').value = user.email || ''; - document.getElementById('phone').value = user.phoneNumber || ''; - document.getElementById('profile-img').src = user.profilePictureUrl || './images/pfp.jpg'; - document.querySelector('.avatar').style.backgroundImage = `url(${user.profilePictureUrl || './images/pfp.jpg'})`; - - update2FaUI(user.twoFactorEnabled); - - // Conditionally hide sections for Google users - if (user.isGoogleUser) { - if (twoFaSection) twoFaSection.style.display = 'none'; - if (passwordResetSection) passwordResetSection.style.display = 'none'; - } - } - - - /** - * Handles the submission of the profile update form. - * @param {Event} event The form submission event. - * @param {string} token The JWT access token. - */ - async function handleProfileUpdate(event, token) { - event.preventDefault(); - const name = document.getElementById('name').value; - const phone = document.getElementById('phone').value; - - try { - await apiFetch('/api/auth/me', { - method: 'PUT', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, phoneNumber: phone }), - }); - - alert('Profile updated successfully!'); - } catch (error) { - console.error('Profile update error:', error); - alert(`Error: ${error.message}`); - } - } - - /** - * Handles the logic for enabling or disabling 2FA. - * @param {string} token The JWT access token. - */ - async function handle2FaToggle(token) { - const isEnabled = document.getElementById('2fa-status').classList.contains('enabled'); - - if (isEnabled) { - // --- Disable 2FA --- - if (confirm('Are you sure you want to disable Two-Factor Authentication?')) { - try { - await apiFetch('/api/auth/2fa/disable', { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}` }, - }); - alert('2FA has been disabled.'); - update2FaUI(false); - } catch (error) { - alert(`Error disabling 2FA: ${error.message}`); - } - } - } else { - // --- Enable 2FA --- - try { - const data = await apiFetch('/api/auth/2fa/setup', { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}` }, - }); - document.getElementById('qr-code-container').innerHTML = `QR Code for 2FA`; - document.getElementById('2fa-modal').classList.remove('hidden'); - } catch (error) { - alert(`Error setting up 2FA: ${error.message}`); - } - } - } - - /** - * Handles the verification of the 2FA token from the modal. - * @param {string} token The JWT access token. - */ - async function handle2FaVerify(token) { - const twoFaCode = document.getElementById('2fa-verify-token').value; - if (!twoFaCode || twoFaCode.length !== 6) { - alert('Please enter a valid 6-digit code.'); - return; - } - - try { - await apiFetch('/api/auth/2fa/verify', { - method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ token: twoFaCode }), - }); - alert('2FA enabled successfully!'); - close2FaModal(); - update2FaUI(true); - } catch (error) { - alert(`Verification failed: ${error.message}`); - } - } - - function close2FaModal() { - document.getElementById('2fa-modal').classList.add('hidden'); - document.getElementById('qr-code-container').innerHTML = ''; - document.getElementById('2fa-verify-token').value = ''; - } - - function update2FaUI(isEnabled) { - const statusEl = document.getElementById('2fa-status'); - const toggleBtn = document.getElementById('2fa-toggle-btn'); - statusEl.textContent = isEnabled ? 'Enabled' : 'Disabled'; - statusEl.className = `status-badge ${isEnabled ? 'enabled' : 'disabled'}`; - toggleBtn.textContent = isEnabled ? 'Disable 2FA' : 'Enable 2FA'; - } - - /** - * Logs the user out by clearing the token and redirecting. - */ - async function logoutUser() { - localStorage.removeItem('accessToken'); - localStorage.removeItem('role'); - window.location.href = '/login.html'; - } -}); \ No newline at end of file diff --git a/public/select-stage.html b/public/select-stage.html deleted file mode 100644 index ac0a0ba..0000000 --- a/public/select-stage.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - MatConnect - Select Stage - - - - - - - - - - - - - -
    -
    -

    Select Your Stage

    -
    - -

    Loading stages...

    -
    -
    -
    - - - - \ No newline at end of file diff --git a/public/signup.html b/public/signup.html deleted file mode 100644 index f099b73..0000000 --- a/public/signup.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - Sign Up - MatConnect - - - - - - - -
    -
    -
    -

    Create an Account

    -

    - Join MatConnect for real-time matatu updates. -

    -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - -
    -
    - -
    -
    - - \ No newline at end of file diff --git a/public/stage-details.html b/public/stage-details.html deleted file mode 100644 index aeb2ae4..0000000 --- a/public/stage-details.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - Matatu Arrival System - - - - - - - - -
    -
    - -
    -
    -
    -
    -

    Kenyatta Avenue Stage

    -

    Real-time arrivals for your next ride.

    -
    -
    -
    -

    Upcoming Matatus

    -
    -
      -
    • -
      -
      - directions_bus -
      -
      -

      KDH 420X

      -

      ETA: 2 mins

      -
      -
      -
      - - - At Stage - -
      -
    • -
    • -
      -
      - directions_bus -
      -
      -

      KCL 112A

      -

      ETA: 5 mins

      -
      -
      -
      - - - Approaching - -
      -
    • -
    • -
      -
      - directions_bus -
      -
      -

      KDJ 899B

      -

      ETA: 8 mins

      -
      -
      -
      - - - Approaching - -
      -
    • -
    -
    -
    -

    Passenger Feedback

    -

    Help us improve your experience.

    - -
    -
    -
    -
    -
    -

    © 2024 Matatu Tracker. All rights reserved.

    -
    -
    -
    - - \ No newline at end of file diff --git a/public/stage-info.html b/public/stage-info.html deleted file mode 100644 index e653d0b..0000000 --- a/public/stage-info.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - Matatu Arrival System - - - - - - - - - - -
    - - -
    - - -
    -
    -

    Upcoming Matatus

    -
    -
      -
    • -
      -
      -

      Loading arrival times...

      -
      -
    • -
    -
    - - -
    - -
    -
    -

    © 2025 MatConnect. All rights reserved.

    -
    -
    -
    - - - \ No newline at end of file diff --git a/public/styles/admin-dashboard.css b/public/styles/admin-dashboard.css deleted file mode 100644 index c21d1f6..0000000 --- a/public/styles/admin-dashboard.css +++ /dev/null @@ -1,498 +0,0 @@ -/* ------------------- */ -/* Variables & Globals */ -/* ------------------- */ -:root { - --primary: #607afb; - --primary-light: #eef2ff; - --bg: #f5f6f8; - --card-bg: #ffffff; - --border-light: #e5e7eb; - --text-light: #111827; - --text-muted-light: #6b7280; - --green-light: #dcfce7; - --green-dark: #166534; - --red-light: #fee2e2; - --red-dark: #991b1b; - --orange-light: #ffedd5; - --orange-dark: #9a3412; - --yellow: #facc15; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: "Public Sans", sans-serif; - background-color: var(--bg); - color: var(--text-light); -} - -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -/* Header */ -.site-header { - border-bottom: 1px solid var(--border-light); - background-color: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(4px); - height: 8vh; - min-height: 60px; - position: sticky; - top: 0; - z-index: 20; -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - height: 100%; - max-width: 1400px; - margin: 0 auto; - padding: 0 1.5rem; -} - -.logo { - font-size: 1.6rem; - font-weight: 700; - text-decoration: none; - color: var(--text-light); -} - -.main-nav { - display: none; -} - -.main-nav ul { - list-style: none; - display: flex; - gap: 1.5rem; -} - -.main-nav a { - text-decoration: none; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-muted-light); - transition: all 0.2s; -} - -.main-nav a:hover, -.main-nav a.active { - color: var(--primary); -} - -.header-actions { - display: flex; - align-items: center; - gap: 1rem; -} - -.user-menu { - position: relative; - display: none; - /* Hidden on mobile */ -} - -.user-menu-btn { - display: flex; - align-items: center; - gap: 0.5rem; - background: none; - border: none; - cursor: pointer; - padding: 0.25rem; - border-radius: 6px; - transition: background-color 0.2s; -} - -.user-menu-btn:hover { - background-color: var(--bg); -} - -.avatar { - width: 2.25rem; - height: 2.25rem; - border-radius: 50%; - object-fit: cover; -} - -.user-name { - font-weight: 600; -} - -.dropdown-icon { - color: var(--text-muted-light); -} - -.user-menu-dropdown { - display: none; - position: absolute; - top: calc(100% + 10px); - right: 0; - background-color: var(--card-bg); - border: 1px solid var(--border-light); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - width: 160px; - z-index: 30; -} - -.user-menu.is-active .user-menu-dropdown { - display: block; -} - -.user-menu-dropdown a { - display: block; - padding: 0.75rem 1rem; - color: var(--text-light); - text-decoration: none; - font-size: 0.875rem; -} - -.user-menu-dropdown a:hover { - background-color: var(--bg); -} - -.mobile-menu-btn { - background: none; - border: none; - cursor: pointer; - padding: 0.5rem; -} - -/* Dashboard Layout */ -.dashboard-container { - flex: 1; - width: 100%; - max-width: 1400px; - margin: 0 auto; - padding: 1.5rem; -} - -.dashboard-header { - margin-bottom: 1.5rem; -} - -.dashboard-header h1 { - font-size: 1.875rem; - font-weight: 700; -} - -.dashboard-header p { - color: var(--text-muted-light); - font-size: 1rem; - margin-top: 0.25rem; -} - -/* Stats Grid */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.stat-card { - background-color: var(--card-bg); - border: 1px solid var(--border-light); - border-radius: 0.75rem; - padding: 1.5rem; - display: flex; - align-items: center; - gap: 1rem; -} - -.stat-icon-wrapper { - width: 48px; - height: 48px; - border-radius: 50%; - display: grid; - place-items: center; -} - -.stat-icon-wrapper .material-symbols-outlined { - font-size: 24px; -} - -.icon-blue { - background-color: var(--primary-light); - color: var(--primary); -} - -.icon-green { - background-color: var(--green-light); - color: var(--green-dark); -} - -.icon-orange { - background-color: var(--orange-light); - color: var(--orange-dark); -} - -.icon-red { - background-color: var(--red-light); - color: var(--red-dark); -} - -.stat-info p { - color: var(--text-muted-light); - font-size: 0.875rem; - margin-bottom: 0.25rem; -} - -.stat-info span { - font-size: 1.75rem; - font-weight: 700; -} - -/* Main Content Grid */ -.dashboard-grid { - display: flex; - flex-direction: column; - /* Stack items vertically on mobile by default */ - gap: 2rem; -} - -.grid-main { - display: flex; - flex-direction: column; - gap: 2rem; - min-width: 0; - /* Allow flex item to shrink below its content size */ -} - -.grid-sidebar { - min-width: 0; - /* Allow flex item to shrink below its content size */ -} - -/* General Panel Style */ -.panel { - background-color: var(--card-bg); - border: 1px solid var(--border-light); - border-radius: 0.75rem; - padding: 1.5rem; - min-width: 0; -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; -} - -.panel h3 { - font-size: 1.125rem; - font-weight: 600; -} - -.btn { - text-decoration: none; - font-size: 0.875rem; - font-weight: 500; - padding: 0.5rem 1rem; - border-radius: 0.5rem; - background-color: var(--bg); - color: var(--text-light); - border: none; - cursor: pointer; - transition: background-color 0.2s; -} - -.btn:hover { - background-color: var(--border-light); -} - -/* Vehicle Table */ -.table-wrapper { - overflow-x: auto; -} - -.data-table { - width: 100%; - border-collapse: collapse; -} - -.data-table th, -.data-table td { - padding: 1rem; - text-align: left; - white-space: nowrap; -} - -.data-table thead { - border-bottom: 1px solid var(--border-light); -} - -.data-table th { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted-light); -} - -.data-table tbody tr:not(:last-child) { - border-bottom: 1px solid var(--border-light); -} - -.data-table td { - font-size: 0.875rem; - color: var(--text-muted-light); -} - -.data-table td:first-child { - font-weight: 500; - color: var(--text-light); -} - -/* Status Badges */ -.badge { - padding: 0.25rem 0.75rem; - font-size: 0.75rem; - font-weight: 500; - border-radius: 9999px; -} - -.badge-success { - background-color: var(--green-light); - color: var(--green-dark); -} - -.badge-danger { - background-color: var(--red-light); - color: var(--red-dark); -} - -/* Sidebar */ -.grid-sidebar { - display: flex; - flex-direction: column; - gap: 2rem; -} - -/* Feedback Card */ -#feedback-list { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.feedback-item { - display: flex; - gap: 1rem; -} - -.feedback-item .avatar { - width: 36px; - height: 36px; -} - -.feedback-body { - flex: 1; -} - -.feedback-meta { - display: flex; - justify-content: space-between; - font-size: 0.875rem; -} - -.feedback-meta p { - font-weight: 500; -} - -.feedback-meta time { - font-size: 0.75rem; - color: var(--text-muted-light); -} - -.star-rating { - display: flex; - margin: 0.25rem 0; -} - -.star-rating .material-symbols-outlined { - font-size: 1.125rem; - color: var(--yellow); -} - -.star-rating .star-empty { - color: var(--border-light); -} - -.feedback-comment { - font-size: 0.875rem; - color: var(--text-muted-light); -} - -/* Responsive */ -@media (min-width: 768px) { - .main-nav { - display: block; - } - - .user-menu { - display: block; - } - - .mobile-menu-btn { - display: none; - } - - .dashboard-container { - padding: 2rem; - } -} - -@media (min-width: 1024px) { - .dashboard-grid { - flex-direction: row; - /* Side-by-side layout on desktop */ - } - - .grid-main { - flex: 2; - } - - /* Takes up 2/3 of the space */ - .grid-sidebar { - flex: 1; - } - - /* Takes up 1/3 of the space */ -} - -/* Mobile Nav Menu (when active) */ -@media (max-width: 767px) { - .main-nav.is-active { - display: block; - position: absolute; - top: 100%; - left: 0; - width: 100%; - background-color: var(--bg); - border-bottom: 1px solid var(--border-light); - padding: 1rem 0; - z-index: 10; - } - - .main-nav.is-active ul { - flex-direction: column; - align-items: center; - gap: 1.5rem; - } - - .main-nav.is-active .logout-link { - color: var(--red-dark); - font-weight: 500; - } -} \ No newline at end of file diff --git a/public/styles/admin-settings.css b/public/styles/admin-settings.css deleted file mode 100644 index 2f6cd93..0000000 --- a/public/styles/admin-settings.css +++ /dev/null @@ -1,220 +0,0 @@ -/* ------------------- */ -/* Variables & Globals */ -/* ------------------- */ -:root { - --primary: #607afb; - --bg: #f5f6f8; - --card-bg: #ffffff; - --border: rgba(0, 0, 0, 0.1); - --text-main: #000000; - --text-muted: rgba(0, 0, 0, 0.6); - --green-light: #dcfce7; - --green-dark: #166534; - --red-light: #fee2e2; - --red-dark: #991b1b; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: "Public Sans", sans-serif; - background-color: var(--bg); - color: var(--text-main); -} - -a { - color: var(--primary); - text-decoration: none; -} -a:hover { - opacity: 0.8; -} - -/* Layout */ -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} -.main-content { - flex: 1; - max-width: 896px; /* max-w-4xl */ - width: 100%; - margin: 0 auto; - padding: 2rem 1rem; -} - -/* Header */ -.site-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 2.5rem; - border-bottom: 1px solid var(--border); -} -.logo { - display: flex; - align-items: center; - gap: 1rem; -} -.logo-icon { - width: 1.75rem; - height: 1.75rem; - color: var(--primary); -} -.logo h2 { - font-size: 1.25rem; - font-weight: 700; -} -.main-nav { - display: flex; - align-items: center; - gap: 1.5rem; -} -.main-nav a { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-muted); -} -.avatar { - width: 2.5rem; - height: 2.5rem; - border-radius: 50%; - background-size: cover; - background-position: center; -} - -/* Page Header */ -.page-header { - margin-bottom: 2rem; -} -.page-header h1 { - font-size: 1.875rem; - font-weight: 700; -} -.page-header p { - color: var(--text-muted); - margin-top: 0.25rem; -} - -/* CSS-Only Tabs */ -.tabs { - border-bottom: 1px solid var(--border); -} -.tabs input[type="radio"] { - display: none; /* Hide the actual radio buttons */ -} -.tabs label { - display: inline-block; - padding: 1rem 0.25rem; - margin-right: 2rem; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-muted); - border-bottom: 2px solid transparent; - cursor: pointer; - transition: all 0.2s; -} -.tabs label:hover { - color: var(--text-main); - border-bottom-color: rgba(0, 0, 0, 0.2); -} -.tabs input:checked + label { - color: var(--primary); - border-bottom-color: var(--primary); -} -.tab-panel { - display: none; /* Hide all panels by default */ - padding-top: 2rem; -} -.tabs input:checked + label + .tab-panel { - display: block; /* Show only the active panel */ -} - -/* Table & Content Panel */ -.panel-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; -} -.panel-header h3 { - font-size: 1.25rem; - font-weight: 700; -} - -.btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - background-color: var(--primary); - color: white; - padding: 0.5rem 1rem; - border: none; - border-radius: 0.25rem; - font-size: 0.875rem; - font-weight: 700; - cursor: pointer; - transition: background-color 0.2s; -} -.btn .material-symbols-outlined { font-size: 1rem; } -.btn:hover { background-color: rgba(96, 122, 251, 0.8); } - -.table-wrapper { - overflow-x: auto; - border: 1px solid var(--border); - border-radius: 0.25rem; - background-color: var(--card-bg); -} -.data-table { - width: 100%; - border-collapse: collapse; -} -.data-table thead { - background-color: rgba(0, 0, 0, 0.05); -} -.data-table th, .data-table td { - padding: 1rem 1.5rem; - text-align: left; - white-space: nowrap; -} -.data-table th { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); -} -.data-table tbody tr:not(:last-child) { - border-bottom: 1px solid var(--border); -} -.data-table td { - font-size: 0.875rem; -} -.data-table td:first-child { - font-weight: 500; -} -.text-right { - text-align: right; -} - -/* Badges */ -.badge { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - font-size: 0.75rem; - font-weight: 500; - border-radius: 9999px; -} -.badge-success { - background-color: var(--green-light); - color: var(--green-dark); -} -.badge-danger { - background-color: var(--red-light); - color: var(--red-dark); -} \ No newline at end of file diff --git a/public/styles/feedback-page.css b/public/styles/feedback-page.css deleted file mode 100644 index ced38f0..0000000 --- a/public/styles/feedback-page.css +++ /dev/null @@ -1,301 +0,0 @@ -/* ------------------- */ -/* Variables & Globals */ -/* ------------------- */ -:root { - --primary: #607afb; - --bg: #f5f6f8; - --card-bg: #ffffff; - --border: #e5e7eb; - --text-main: #111827; - --text-muted: #6b7280; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: "Public Sans", sans-serif; - background-color: var(--bg); - color: var(--text-main); -} - -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.container { - width: 100%; - max-width: 1280px; - margin: 0 auto; - padding: 0 1.5rem; -} - -/* Header & Footer */ -.site-header { - background-color: rgba(245, 246, 248, 0.8); - backdrop-filter: blur(4px); - border-bottom: 1px solid var(--border); - position: sticky; - top: 0; - z-index: 20; - height: 8vh; -} - -.header-content { - display: flex; - align-items: center; - justify-content: space-between; - height: 100%; -} - -.logo { - font-size: 1.6rem; - font-weight: 700; - color: var(--text-main); - text-decoration: none; -} - -.main-nav { - display: none; /* Hidden on mobile */ -} - -.main-nav ul { - list-style: none; - display: flex; - align-items: center; - gap: 2rem; - margin: 0; - padding: 0; -} - -.main-nav a { - text-decoration: none; - font-weight: 500; - color: var(--text-muted); - transition: color 0.2s; -} - -.main-nav a:hover, -.main-nav a.active { - color: var(--primary); -} - -.header-actions { - display: flex; - align-items: center; - gap: 1rem; -} - -.mobile-menu-btn { - background: none; - border: none; - padding: 0.5rem; - cursor: pointer; -} - -.user-profile { - display: flex; - align-items: center; -} - -.profile-icon { - height: 2.5rem; - width: 2.5rem; - border-radius: 50%; - object-fit: cover; - border: 2px solid var(--border); - cursor: pointer; -} - -.logout-btn-desktop { - display: none; /* Hidden on mobile, shown on desktop */ - padding: 10px 15px; - border-radius: 8px; - border: none; - font-size: 1rem; - background-color: var(--border); - color: var(--text-main); - cursor: pointer; -} - -.site-footer { - border-top: 1px solid var(--border); - text-align: center; - padding: 2rem 1.5rem; - font-size: 0.875rem; - color: var(--text-muted); -} - -/* Main Content & Feedback Card */ -.main-content { - flex-grow: 1; - padding: 2rem 1.5rem; -} - -.feedback-card { - max-width: 672px; /* max-w-2xl */ - margin: 0 auto; - background-color: var(--card-bg); - border-radius: 0.75rem; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); - padding: 2rem; -} - -.card-header { - text-align: center; - margin-bottom: 2rem; -} - -.card-header h2 { - font-size: 1.875rem; - font-weight: 900; -} - -.card-header p { - margin-top: 0.5rem; - color: var(--text-muted); -} - -/* Form Styling */ -.feedback-form { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.form-group { - display: flex; - flex-direction: column; -} - -.form-group label { - font-size: 0.875rem; - font-weight: 500; - margin-bottom: 0.5rem; -} - -.form-select { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--border); - background-color: var(--bg); - border-radius: 0.5rem; - font-family: inherit; - font-size: 1rem; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right 0.5rem center; - background-repeat: no-repeat; - background-size: 1.5em 1.5em; -} - -.form-textarea { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--border); - background-color: var(--bg); - border-radius: 0.5rem; - font-family: inherit; - font-size: 1rem; - transition: border-color 0.2s, box-shadow 0.2s; -} -.form-textarea:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgba(96, 122, 251, 0.3); -} -.form-textarea::placeholder { - color: var(--text-muted); -} - -.btn { - width: 100%; - padding: 0.75rem; - border: none; - border-radius: 0.5rem; - background-color: var(--primary); - color: white; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease-in-out; -} - -.btn:hover { - opacity: 0.9; - transform: scale(1.02); -} - -/* Star Rating */ -.rating-stars { - display: flex; - justify-content: center; - /* Stars are in reverse order in HTML, so we reverse them back for display */ - flex-direction: row-reverse; - border: none; - padding: 0; -} -.rating-stars input { - display: none; /* Hide the radio buttons */ -} -.rating-stars label { - font-family: 'Material Symbols Outlined'; - font-size: 2.5rem; - color: var(--text-muted); - cursor: pointer; - transition: color 0.2s; -} - -/* Color stars on hover */ -.rating-stars:hover label { - color: var(--primary); -} -/* Keep stars after the hovered one gray */ -.rating-stars label:hover ~ label { - color: var(--text-muted); -} - -/* Color stars permanently when selected via JavaScript */ -.rating-stars label.is-selected { - color: var(--primary); -} - -/* Responsive */ -@media (min-width: 768px) { - .main-nav { - display: flex; - } - .mobile-menu-btn { - display: none; - } - .logout-btn-desktop { - display: block; - } -} - -/* Mobile Nav Menu (when active) */ -@media (max-width: 767px) { - .main-nav.is-active { - display: block; - position: absolute; - top: 8vh; - left: 0; - width: 100%; - background-color: var(--bg); - border-bottom: 1px solid var(--border); - padding: 1rem 0; - z-index: 10; - } - .main-nav.is-active ul { - flex-direction: column; - align-items: center; - gap: 1.5rem; - } -} \ No newline at end of file diff --git a/public/styles/general.css b/public/styles/general.css deleted file mode 100644 index 98729eb..0000000 --- a/public/styles/general.css +++ /dev/null @@ -1,155 +0,0 @@ -:root { - --primary: #607AFB; - --background-light: #f5f6f8; - --background-dark: #0f1323; - --foreground-light: #101922; - --foreground-dark: #f6f7f8; - --card-light: #ffffff; - --card-dark: #1b2734; - --subtle-light: #e5e7eb; - --subtle-dark: #374151; - - --white: #ffffff; - --black-transparent: rgba(0, 0, 0, 0.6); - --gray-200: #e5e7eb; - --gray-400: #9ca3af; - --gray-500: #6b7280; - --gray-600: #4b5563; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - font-family: 'Public Sans', sans-serif; - color: var(--foreground-light); -} - -body { - background-color: var(--background-light); -} - -.header { - height: 8vh; - width: 100vw; - display: flex; - justify-content: space-between; - align-items: center; - padding-left: 2vw; - border-bottom: 1px solid var(--subtle-light); -} - -.header-logo { - font-weight: bold; - font-size: 1.6rem; -} - -.header-links-container { - width: 10vw; -} - -.header-links{ - list-style: none; - display: flex; - align-items: center; - justify-content: space-evenly; -} - -.header-links a{ - text-decoration: none; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-muted); - transition: color 0.2s; -} - -.header-links a:hover { - cursor: pointer; - color: var(--primary); -} - -.header-buttons { - width: fit-content; - height: 100%; - display: flex; - align-items: center; - justify-content: space-evenly; -} - -.primary-btn { - padding: 10px 15px; - border-radius: 8px; - color: white; - border: none; -} - -.primary-btn:hover { - transform: scale(1.1); - cursor: pointer; - transition: all ease-in-out 0.2s; -} - -.btn-2 { - background-color: var(--subtle-light); - color: var(--foreground-light); -} - -.btn-2:hover { - background-color: var(--gray-400); -} - -.btn-1 { - background-color: var(--primary); -} - -.footer { - width: 100%; - height: 8vh; - background-color: var(--card-light); - display: flex; - justify-content: space-evenly; - align-items: center; - border-top: 1px solid var(--subtle-light); -} - -.footer-section { - flex: 0.8; - height: 100%; - display: flex; - align-items: center; -} - -.footlogo { - padding-left: 2vw; - font-size: 1.5rem; - font-weight: bold; -} - -.footlinks { - display: flex; - justify-content: center; -} - -.footer-links { - list-style: none; - display: flex; - align-items: center; - justify-content: space-evenly; - width: 60%; -} - -.footer-links a { - text-decoration: none; - color: var(--gray-500); -} - -.footer-links a:hover { - cursor: pointer; - text-decoration: underline; -} - -.copyright { - justify-content: end; - padding-right: 2vw; - color: var(--gray-500); -} \ No newline at end of file diff --git a/public/styles/index.css b/public/styles/index.css deleted file mode 100644 index 716d562..0000000 --- a/public/styles/index.css +++ /dev/null @@ -1,334 +0,0 @@ -/* ------------------- */ -/* Variables & Globals */ -/* ------------------- */ -:root { - --primary: #607AFB; - --background: #f5f6f8; - --card-bg: #ffffff; - --text-main: #101922; - --text-muted: #6b7280; - --border: #e5e7eb; - --white: #ffffff; - --black-transparent: rgba(0, 0, 0, 0.6); -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Public Sans', sans-serif; - background-color: var(--background); - color: var(--text-main); - font-size: 16px; -} - -a { - color: var(--primary); - text-decoration: none; -} - -/* ------------------- */ -/* Layout Containers */ -/* ------------------- */ -.container { - width: 100%; - max-width: 1280px; - margin: 0 auto; - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -main { - display: flex; - flex-direction: column; - width: 100%; - align-items: center; -} - -/* ------------------- */ -/* Header & Navigation */ -/* ------------------- */ -.site-header { - position: sticky; - top: 0; - z-index: 20; - background-color: rgba(245, 246, 248, 0.8); - backdrop-filter: blur(4px); - border-bottom: 1px solid var(--border); - height: 8vh; -} - -.nav-container { - display: flex; - justify-content: space-between; - align-items: center; - height: 100%; -} - -.logo { - font-weight: bold; - font-size: 1.6rem; - color: var(--text-main); - text-decoration: none; -} - -.main-nav { - display: none; /* Hidden on mobile */ -} - -.nav-links { - list-style: none; - display: flex; - align-items: center; - gap: 2rem; -} - -.nav-links a { - text-decoration: none; - font-size: 1rem; - font-weight: 500; - color: var(--text-muted); - transition: color 0.2s; -} - -.nav-links a:hover { - color: var(--primary); -} - -.header-buttons { - display: none; /* Hidden on mobile, shown on desktop */ - align-items: center; - gap: 1rem; -} - -.mobile-menu-btn { - display: block; /* Visible on mobile */ - background: none; - border: none; - cursor: pointer; - padding: 0.5rem; -} - -/* ------------------- */ -/* Buttons */ -/* ------------------- */ -.primary-btn { - padding: 10px 15px; - border-radius: 8px; - border: none; - font-size: 1rem; - font-weight: 500; - cursor: pointer; - transition: transform 0.2s ease-in-out, background-color 0.2s; -} - -.primary-btn:hover { - transform: scale(1.05); -} - -.btn-1 { - background-color: var(--primary); - color: var(--white); -} - -.btn-2 { - background-color: var(--border); - color: var(--text-main); -} - -.btn-2:hover { - background-color: #d1d5db; /* gray-300 */ -} - -/* ------------------- */ -/* Hero Section */ -/* ------------------- */ -.hero-section { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 65vh; - padding: 2rem; - position: relative; - overflow: hidden; - text-align: center; -} - -.hero-section::before { - content: ''; - position: absolute; - inset: 0; - width: 100%; - height: 100%; - background-image: url(https://publish.eastleighvoice.co.ke/mugera_lock/uploads/2024/08/matatu2.jpg); - background-repeat: no-repeat; - background-size: cover; - background-position: center; - filter: blur(5px); - transform: scale(1.1); - z-index: -1; -} - -.hero-msg { - font-size: clamp(2.5rem, 5vw, 4rem); /* Responsive font size */ - margin-bottom: 1.5rem; - color: var(--white); - text-shadow: 2px 2px 8px var(--black-transparent); -} - -.hero-desc { - font-size: clamp(1.1rem, 2.5vw, 1.4rem); - max-width: 65ch; - margin-bottom: 2rem; - color: var(--white); - text-shadow: 1px 1px 4px var(--black-transparent); -} - -.hero-btns { - display: flex; - gap: 1rem; - justify-content: center; -} - -/* ------------------- */ -/* Features Section */ -/* ------------------- */ -.features { - width: 100%; - padding: 5rem 1.5rem; - text-align: center; -} - -.features-info { - margin-bottom: 3rem; -} - -.features-cards { - display: grid; - grid-template-columns: 1fr; - gap: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -.features-card { - background-color: var(--card-bg); - border: 1px solid var(--border); - border-radius: 10px; - padding: 2rem; - text-align: left; -} - -.card-logo { - background-color: var(--border); - border-radius: 10px; - width: 50px; - height: 50px; - display: grid; - place-items: center; - font-size: 1.5rem; - margin-bottom: 1.5rem; - color: var(--primary); -} - -.card-title { - font-size: 1.5rem; - margin-bottom: 0.5rem; -} - -.card-desc { - color: var(--text-muted); - font-size: 1.1rem; - line-height: 1.6; -} - -/* ------------------- */ -/* Footer */ -/* ------------------- */ -.footer { - width: 100%; - background-color: var(--card-bg); - display: flex; - flex-direction: column; - justify-content: space-evenly; - align-items: center; - border-top: 1px solid var(--border); - padding: 1.5rem; - gap: 1.5rem; -} - -.footlogo { - font-size: 1.5rem; - font-weight: bold; -} - -.footer-links { - list-style: none; - display: flex; - align-items: center; - justify-content: center; - gap: 2rem; - flex-wrap: wrap; -} - -.footer-links a { - text-decoration: none; - color: var(--text-muted); -} - -.footer-links a:hover { - cursor: pointer; - text-decoration: underline; -} - -.copyright { - color: var(--text-muted); -} - -/* ------------------- */ -/* Responsive Design */ -/* ------------------- */ -@media (min-width: 768px) { - .main-nav { - display: block; - } - .header-buttons { - display: flex; - } - .mobile-menu-btn { - display: none; - } - .features-cards { - grid-template-columns: repeat(3, 1fr); - } - .footer { - flex-direction: row; - justify-content: space-between; - height: 8vh; - padding: 0 2rem; - } -} - -/* Mobile Nav Menu (when active) */ -@media (max-width: 767px) { - .main-nav.is-active { - display: block; - position: absolute; - top: 8vh; - left: 0; - width: 100%; - background-color: var(--background); - border-bottom: 1px solid var(--border); - padding: 1rem 0; - } - .main-nav.is-active .nav-links { - flex-direction: column; - align-items: center; - gap: 1.5rem; - } -} \ No newline at end of file diff --git a/public/styles/login.css b/public/styles/login.css deleted file mode 100644 index 39e3d1e..0000000 --- a/public/styles/login.css +++ /dev/null @@ -1,260 +0,0 @@ -:root { - --primary: #137fec; - --background-light: #f6f7f8; - --background-dark: #101922; - --gray-900: #111827; - --gray-800: #1f2937; - --gray-700: #374151; - --gray-600: #4b5563; - --gray-500: #6b7280; - --gray-400: #9ca3af; - --gray-300: #d1d5db; - --gray-200: #e5e7eb; - --white: #ffffff; -} - -body { - background-color: var(--background-light); - font-family: 'Public Sans', sans-serif; - color: var(--gray-800); -} - -.dark body { - background-color: var(--background-dark); - color: var(--gray-200); -} - -.container { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -} - -.form-wrapper { - width: 100%; - max-width: 28rem; - padding: 2rem; - background-color: #ffffff; - border-radius: 0.75rem; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - margin: 1rem; -} - -.dark .form-wrapper { - background-color: var(--background-dark); -} - -.form-wrapper>*+* { - margin-top: 2rem; -} - -.text-center { - text-align: center; -} - -.title { - font-size: 1.875rem; - font-weight: 700; - color: var(--gray-900); -} - -.dark .title { - color: var(--white); -} - -.subtitle { - margin-top: 0.5rem; - font-size: 0.875rem; - color: var(--gray-600); -} - -.dark .subtitle { - color: var(--gray-400); -} - -.form { - margin-top: 2rem; -} - -.form>*+* { - margin-top: 1.5rem; -} - -.input-container { - border-radius: 0.375rem; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.input-container>div+div { - margin-top: -1px; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.input { - appearance: none; - position: relative; - display: block; - width: 95%; - padding: 0.75rem; - border: 1px solid var(--gray-300); - color: var(--gray-900); - background-color: var(--background-light); - font-size: 0.875rem; -} - -.input::placeholder { - color: var(--gray-500); -} - -.input:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px var(--primary); - z-index: 10; -} - -.dark .input { - border-color: var(--gray-700); - color: var(--white); - background-color: var(--gray-800); -} - -.dark .input::placeholder { - color: var(--gray-400); -} - -.input-top { - border-top-left-radius: 0.5rem; - border-top-right-radius: 0.5rem; -} - -.input-bottom { - border-bottom-left-radius: 0.5rem; - border-bottom-right-radius: 0.5rem; -} - -.flex-between { - display: flex; - align-items: center; - justify-content: space-between; -} - -.text-sm { - font-size: 0.875rem; -} - -.link { - font-weight: 500; - color: var(--primary); -} - -.link:hover { - color: rgb(19 127 236 / 0.8); -} - -.button { - display: flex; - justify-content: center; - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid transparent; - font-size: 0.875rem; - font-weight: 500; - border-radius: 0.5rem; - color: var(--white); - background-color: var(--primary); -} - -.button:hover { - background-color: rgb(19 127 236 / 0.9); -} - -.button:focus { - outline: none; - box-shadow: 0 0 0 2px var(--primary); -} - -.footer-text { - margin-top: 0.5rem; - text-align: center; - font-size: 0.875rem; - color: var(--gray-600); -} - -.dark .footer-text { - color: var(--gray-400); -} - -.message { - text-align: center; - margin-bottom: 1rem; - padding: 0.5rem; - border-radius: 4px; - font-size: 0.9rem; - display: none; /* Hide by default */ -} - -.message.error { - display: block; - color: #721c24; - background-color: #f8d7da; - border: 1px solid #f5c6cb; -} - -.message.success { - display: block; - color: #155724; - background-color: #d4edda; - border: 1px solid #c3e6cb; -} - -.divider { - margin-top: 1.5rem; - margin-bottom: 1.5rem; - text-align: center; - position: relative; - color: var(--gray-400); -} - -.divider::before, -.divider::after { - content: ''; - position: absolute; - top: 50%; - width: 40%; - height: 1px; - background-color: var(--gray-300); -} - -.divider::before { left: 0; } -.divider::after { right: 0; } - -.google-button { - background-color: var(--white); - color: var(--gray-700); - border: 1px solid var(--gray-300); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.google-button:hover { - background-color: var(--background-light); -} - -.google-button img { - width: 1.25rem; - height: 1.25rem; -} \ No newline at end of file diff --git a/public/styles/main.css b/public/styles/main.css deleted file mode 100644 index 805672d..0000000 --- a/public/styles/main.css +++ /dev/null @@ -1,167 +0,0 @@ -/* ------------------- */ -/* Variables & Globals */ -/* ------------------- */ -:root { - --primary: #137fec; - --primary-light: rgba(19, 127, 236, 0.1); - --primary-dark: #106ac4; - --background: #f6f7f8; - --card-bg: #ffffff; - --text-main: #111827; - --text-muted: #6b7280; - --border: #e5e7eb; - --green: #22c55e; - --red: #ef4444; - --active: #607AFB; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: "Public Sans", sans-serif; - background-color: var(--background); - color: var(--text-main); - font-size: 16px; -} - -a { - color: var(--primary); - text-decoration: none; -} - -.container { - width: 100%; - max-width: 1280px; - margin: 0 auto; - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -.nav-container { - display: flex; - justify-content: space-between; - align-items: center; - height: 8vh; -} - -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -main { - flex-grow: 1; -} - -/* ------------------- */ -/* Header & Footer */ -/* ------------------- */ -.site-header { - position: sticky; - top: 0; - z-index: 20; - background-color: rgba(246, 247, 248, 0.8); - backdrop-filter: blur(4px); - border-bottom: 1px solid var(--border); - height: 8vh; - display: flex; - justify-content: space-between; - align-items: center; -} - -.header-content { - display: flex; - justify-content: space-between; - align-items: center; - height: 4rem; -} - -.logo { - display: flex; - align-items: center; - gap: 0.75rem; - font-size: 1.25rem; - font-weight: 700; - color: var(--text-main); - cursor: pointer; -} - -.logo .material-symbols-outlined { - color: var(--primary); - font-size: 2rem; -} - -.main-nav { - display: none; /* Mobile first */ - gap: 1.5rem; -} - -.main-nav a { - text-decoration: none; - font-weight: 500; - color: var(--text-muted); - transition: color 0.2s; -} -.main-nav a:hover, .main-nav a.active { - color: var(--active); -} - -.header-actions { - display: flex; - align-items: center; - gap: 1rem; -} - -.avatar { - width: 2.5rem; - height: 2.5rem; - border-radius: 9999px; - background-size: cover; - background-position: center; - border: 2px solid var(--border); -} - -.site-footer { - border-top: 1px solid var(--border); - padding: 2rem 0; - text-align: center; - font-size: 0.875rem; - color: var(--text-muted); -} - - -.primary-btn { - padding: 10px 15px; - border-radius: 8px; - color: white; - border: none; -} - -.primary-btn:hover { - transform: scale(1.1); - cursor: pointer; - transition: all ease-in-out 0.2s; -} - -.btn-2 { - color: var(--foreground-light); -} - - -.btn-1 { - background-color: var(--primary); -} - -/* ------------------- */ -/* Responsive Nav */ -/* ------------------- */ -@media (min-width: 768px) { - .main-nav { - display: flex; - } -} \ No newline at end of file diff --git a/public/styles/manage-stages.css b/public/styles/manage-stages.css deleted file mode 100644 index ca25043..0000000 --- a/public/styles/manage-stages.css +++ /dev/null @@ -1,534 +0,0 @@ -/* ------------------- */ -/* Variables & Globals */ -/* ------------------- */ -:root { - --primary: #607afb; - --primary-light: #eef2ff; - --bg: #f6f7f8; - --card-bg: #ffffff; - --border-light: #e5e7eb; - --text-light: #111827; - --text-muted-light: #6b7280; - --red: #ef4444; - --red-light: #fee2e2; - --red-dark: #991b1b; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: "Public Sans", sans-serif; - background-color: var(--bg); - color: var(--text-light); -} - -.hidden { - display: none !important; -} - -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.dashboard-container { - display: flex; - flex-direction: column; - width: 100%; - max-width: 1400px; - margin: 0 auto; - flex-grow: 1; - padding: 1.5rem; - gap: 1.5rem; -} - -/* ------------------- */ -/* Header & Navigation */ -/* ------------------- */ -.site-header { - border-bottom: 1px solid var(--border-light); - background-color: rgba(255, 255, 255, 0.8); - backdrop-filter: blur(4px); - height: 8vh; - min-height: 60px; - position: sticky; - top: 0; - z-index: 20; -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - height: 100%; - max-width: 1400px; - margin: 0 auto; - padding: 0 1.5rem; -} - -.logo { - font-size: 1.6rem; - font-weight: 700; - text-decoration: none; - color: var(--text-light); -} - -.main-nav { - display: none; -} - -.main-nav ul { - list-style: none; - display: flex; - gap: 1.5rem; -} - -.main-nav a { - text-decoration: none; - font-size: 0.875rem; - font-weight: 500; - color: var(--text-muted-light); - transition: color 0.2s; -} - -.main-nav a:hover, -.main-nav a.active { - color: var(--primary); -} - -.header-actions { - display: flex; - align-items: center; - gap: 1rem; -} - -.user-menu { - position: relative; - display: none; /* Hidden on mobile */ -} - -.user-menu-btn { - display: flex; - align-items: center; - gap: 0.5rem; - background: none; - border: none; - cursor: pointer; - padding: 0.25rem; - border-radius: 6px; - transition: background-color 0.2s; -} - -.user-menu-btn:hover { - background-color: var(--bg); -} - -.avatar { - width: 2.25rem; - height: 2.25rem; - border-radius: 50%; - object-fit: cover; -} - -.user-name { - font-weight: 600; -} - -.dropdown-icon { - color: var(--text-muted-light); -} - -.user-menu-dropdown { - display: none; - position: absolute; - top: calc(100% + 10px); - right: 0; - background-color: var(--card-bg); - border: 1px solid var(--border-light); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - width: 160px; - z-index: 30; -} - -.user-menu.is-active .user-menu-dropdown { - display: block; -} - -.user-menu-dropdown a { - display: block; - padding: 0.75rem 1rem; - color: var(--text-light); - text-decoration: none; - font-size: 0.875rem; -} - -.user-menu-dropdown a:hover { - background-color: var(--bg); -} - -.mobile-menu-btn { - background: none; - border: none; - cursor: pointer; - padding: 0.5rem; -} - -/* ------------------- */ -/* Panels */ -/* ------------------- */ -.panel { - display: flex; - flex-direction: column; - background-color: var(--card-bg); - border-radius: 0.75rem; - overflow: hidden; - border: 1px solid var(--border-light); - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - min-width: 0; -} - -.panel-header { - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border-light); -} - -.panel-header h2 { - font-size: 1.125rem; - font-weight: 600; -} - -.panel-header .sub-header { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - color: var(--text-muted-light); - margin-top: 0.25rem; -} - -.sub-header .material-symbols-outlined { - font-size: 1.125rem; -} - -.panel-body { - flex: 1; - padding: 1.5rem; -} - -.panel-body.scrollable { - overflow-y: auto; -} - -.panel-body h3 { - font-size: 1rem; - font-weight: 600; - margin-bottom: 1rem; -} - -.panel-footer { - padding: 1rem; - border-top: 1px solid var(--border-light); - background-color: var(--bg); -} - -/* ------------------- */ -/* Lists */ -/* ------------------- */ -.stage-list, -.vehicle-list { - list-style: none; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.stage-list-item { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem 1rem; - border-radius: 0.5rem; - cursor: pointer; - transition: background-color 0.2s; - border: 2px solid transparent; -} - -.stage-list-item:hover { - background-color: var(--primary-light); -} - -.stage-list-item.active { - background-color: var(--primary-light); - border-color: var(--primary); - color: var(--primary); -} - -.stage-list-item p { - flex: 1; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.item-icon { - display: flex; - align-items: center; - justify-content: center; -} - -.chevron { - color: var(--text-muted-light); -} - -.vehicle-list-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - border: 1px solid var(--border-light); - border-radius: 0.5rem; -} - -.vehicle-actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.vehicle-info { - display: flex; - align-items: center; - gap: 1rem; -} - -.vehicle-info .material-symbols-outlined { - color: var(--primary); -} - -.vehicle-info p { - font-weight: 500; -} - -.vehicle-list-item .delete-btn:hover { - color: var(--red-dark); - background-color: var(--red-light); -} - -.vehicle-list-item .remove-btn:hover { - color: var(--red-dark); -} - -/* ------------------- */ -/* Buttons & Forms */ -/* ------------------- */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.625rem 1rem; - border-radius: 0.5rem; - border: none; - font-weight: 600; - font-size: 0.875rem; - cursor: pointer; - transition: all 0.2s; -} - -.btn:hover { - opacity: 0.9; -} - -.btn-primary { - background-color: var(--primary); - color: white; -} - -.btn-primary:hover { - background-color: var(--primary); -} - -.btn-secondary { - background-color: var(--border-light); - color: var(--text-light); -} - -.btn-secondary:hover { - background-color: #d1d5db; /* gray-300 */ -} - -.assign-form { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - width: 100%; -} - -.form-select { - flex-grow: 1; - padding: 0.75rem; - background-color: var(--bg); - border: 1px solid var(--border-light); - border-radius: 0.5rem; - font-family: inherit; - font-size: 0.875rem; -} - -.icon-button { - background: none; - border: none; - color: var(--text-muted-light); - cursor: pointer; - padding: 0.5rem; - border-radius: 999px; - display: inline-flex; - align-items: center; - justify-content: center; - transition: color 0.2s, background-color 0.2s; -} - -.icon-button:hover { - color: var(--text-light); - background-color: var(--border-light); -} - -/* ------------------- */ -/* Modal */ -/* ------------------- */ -.modal-overlay { - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 50; -} - -.modal-content { - background-color: var(--card-bg); - border-radius: 0.75rem; - width: 100%; - max-width: 500px; - margin: 1rem; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border-light); -} - -.modal-header h3 { - font-size: 1.125rem; - font-weight: 600; -} - -.modal-body { - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.form-group { - display: flex; - flex-direction: column; -} - -.form-group label { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-muted-light); - margin-bottom: 0.5rem; -} - -.form-group input { - width: 100%; - padding: 0.625rem 1rem; - background-color: var(--bg); - border: 1px solid var(--border-light); - border-radius: 0.5rem; - font-size: 0.875rem; -} - -.form-group input:focus { - outline: none; - box-shadow: 0 0 0 2px var(--primary-light); - border-color: transparent; -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - padding: 1rem 1.5rem; - background-color: var(--bg); - border-bottom-left-radius: 0.75rem; - border-bottom-right-radius: 0.75rem; -} - -/* ------------------- */ -/* Responsive */ -/* ------------------- */ -@media (min-width: 768px) { - .main-nav { display: block; } - .user-menu { display: block; } - .mobile-menu-btn { display: none; } - .dashboard-container { padding: 2rem; } -} - -@media (min-width: 1024px) { - .dashboard-container { - flex-direction: row; - } - #stages-panel { - flex: 1; - max-width: 400px; - } - #details-panel { - flex: 2; - } -} - -/* Mobile Nav Menu (when active) */ -@media (max-width: 767px) { - .main-nav.is-active { - display: block; - position: absolute; - top: 100%; - left: 0; - width: 100%; - background-color: var(--bg); - border-bottom: 1px solid var(--border-light); - padding: 1rem 0; - z-index: 10; - } - .main-nav.is-active ul { - flex-direction: column; - align-items: center; - gap: 1.5rem; - } - .main-nav.is-active .logout-link { - color: var(--red-dark); - font-weight: 500; - } -} \ No newline at end of file diff --git a/public/styles/manage-users.css b/public/styles/manage-users.css deleted file mode 100644 index 8c3f35c..0000000 --- a/public/styles/manage-users.css +++ /dev/null @@ -1,224 +0,0 @@ -:root { - --primary: #607afb; - --primary-light: #eef2ff; - --bg: #f5f6f8; - --card-bg: #ffffff; - --border-light: #e5e7eb; - --text-light: #111827; - --text-muted-light: #6b7280; - --green-light: #dcfce7; - --green-dark: #166534; - --red-light: #fee2e2; - --red-dark: #991b1b; - --orange-light: #ffedd5; - --orange-dark: #9a3412; - --yellow: #facc15; -} - -/* Header Controls */ -.header-controls { - display: flex; - align-items: center; - gap: 1rem; -} - -.search-bar { - display: flex; - align-items: center; - background-color: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 0 0.75rem; -} - -.search-bar .material-symbols-outlined { - color: var(--text-secondary); -} - -.search-bar input { - border: none; - background: none; - padding: 0.625rem 0.5rem; - outline: none; - font-family: inherit; -} - -.filter-buttons { - display: flex; - gap: 0.5rem; - background-color: var(--text-muted-light); - padding: 0.25rem; - border-radius: 8px; - border: 1px solid var(--border-color); -} - -.filter-buttons button { - background: none; - border: none; - padding: 0.5rem 0.75rem; - border-radius: 6px; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s, color 0.2s; -} - -.filter-buttons button.active { - background-color: var(--primary-color); - color: white; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -/* Table Styles */ -.actions-cell { - display: flex; - gap: 0.5rem; -} - -.btn-action { - padding: 0.375rem 0.75rem; - border-radius: 6px; - border: 1px solid var(--border-color); - background-color: transparent; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s; -} - -.btn-block { - color: var(--warning-color); -} - -.btn-block:hover { - background-color: #feebc8; /* orange-100 */ -} - -.btn-delete { - color: var(--danger-color); -} - -.btn-delete:hover { - background-color: #fed7d7; /* red-200 */ -} - -.badge-role { - padding: 0.25rem 0.75rem; - border-radius: 99px; - font-size: 0.8rem; - font-weight: 600; -} - -.role-admin { - background-color: #e9d8fd; /* purple-200 */ - color: #6b46c1; /* purple-700 */ -} - -.role-matadmin { - background-color: #bee3f8; /* blue-200 */ - color: #2c5282; /* blue-800 */ -} - -.role-passenger { - background-color: #c6f6d5; /* green-200 */ - color: #2f855a; /* green-700 */ -} - -.hidden { - display: none !important; -} - -/* ------------------- */ -/* Modal Styles */ -/* ------------------- */ -.modal-overlay { - position: fixed; - inset: 0; - background-color: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 50; -} - -.modal-content { - background-color: var(--card-bg); - border-radius: 0.75rem; - width: 100%; - max-width: 500px; - margin: 1rem; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border-color); -} - -.modal-header h3 { - font-size: 1.125rem; - font-weight: 600; -} - -.modal-body { - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.form-group { - display: flex; - flex-direction: column; -} - -.form-group label { - font-size: 0.875rem; - font-weight: 500; - color: var(--text-secondary); - margin-bottom: 0.5rem; -} - -.form-group input, .form-group select { - width: 100%; - padding: 0.75rem 1rem; - background-color: #fff; /* Use white background to rely on border for definition */ - border: 1px solid #d1d5db; /* A slightly darker border for better contrast */ - border-radius: 0.5rem; - font-size: 0.9rem; - transition: border-color 0.2s, box-shadow 0.2s; -} - -.form-group input:focus, .form-group select:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.2); -} - -.password-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.password-wrapper input { - padding-right: 3rem; /* Make space for the icon */ -} - -.password-toggle-btn { - position: absolute; - right: 0.5rem; - color: var(--text-secondary); -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - padding: 1rem 1.5rem; - background-color: var(--bg-color); - border-bottom-left-radius: 0.75rem; - border-bottom-right-radius: 0.75rem; -} \ No newline at end of file diff --git a/public/styles/mat-admin-vehicles.css b/public/styles/mat-admin-vehicles.css deleted file mode 100644 index 48109c1..0000000 --- a/public/styles/mat-admin-vehicles.css +++ /dev/null @@ -1,168 +0,0 @@ -/* Controls Bar */ -.controls-bar { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 1rem; - margin-bottom: 2rem; - padding: 1rem; - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 12px; -} - -.date-filter { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.date-filter label { - font-weight: 500; - color: var(--text-secondary); -} - -.date-filter input[type="date"] { - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-color); - border-radius: 8px; - background-color: var(--bg-color); - font-family: inherit; -} - -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.625rem 1rem; - border-radius: 0.5rem; - border: none; - font-weight: 600; - font-size: 0.875rem; - cursor: pointer; - transition: all 0.2s; -} - -.btn-primary { - background-color: var(--primary-color); - color: white; -} - -/* Vehicle Stats Container */ -#vehicle-stats-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); - gap: 1.5rem; -} - -.loading-placeholder { - grid-column: 1 / -1; /* Span full width */ - text-align: center; - padding: 3rem; - color: var(--text-secondary); -} - -/* Vehicle Card Styling */ -.vehicle-card { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.vehicle-card .card-header .vehicle-id { - font-size: 0.8rem; - color: var(--text-secondary); - font-weight: 500; -} - -.vehicle-stats-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - text-align: center; -} - -.mini-stat { - background-color: var(--bg-color); - padding: 1rem; - border-radius: 8px; -} - -.mini-stat h4 { - margin: 0 0 0.5rem; - font-size: 0.875rem; - color: var(--text-secondary); - font-weight: 500; -} - -.mini-stat p { - margin: 0; - font-size: 1.5rem; - font-weight: 700; - color: var(--primary-color); -} - -.feedback-section, .charts-section { - border-top: 1px solid var(--border-color); - padding-top: 1.5rem; -} - -.feedback-section h4, .charts-section h4 { - margin: 0 0 1rem; - font-size: 1rem; - font-weight: 600; -} - -.feedback-item { - padding: 0.75rem; - border-bottom: 1px solid var(--border-color); -} -.feedback-item:last-child { border-bottom: none; } - -/* Chart Section */ -.chart-controls { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 1rem; - margin-bottom: 1.5rem; -} - -.chart-period-filters { - display: flex; - gap: 0.5rem; - background-color: var(--bg-color); - padding: 0.25rem; - border-radius: 8px; -} - -.chart-period-filters button { - background: none; - border: none; - padding: 0.5rem 0.75rem; - border-radius: 6px; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s, color 0.2s; -} - -.chart-period-filters button.active { - background-color: var(--primary-color); - color: white; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.btn-outline { - background-color: transparent; - border: 1px solid var(--border-color); - color: var(--text-secondary); -} - -.fleet-report-section { - display: flex; - align-items: center; - gap: 1rem; -} \ No newline at end of file diff --git a/public/styles/mat-admin.css b/public/styles/mat-admin.css deleted file mode 100644 index 9a43527..0000000 --- a/public/styles/mat-admin.css +++ /dev/null @@ -1,252 +0,0 @@ -/* General Styles from admin-dashboard.css */ -:root { - --primary-color: #4a6cf7; - --primary-hover: #3b57d1; - --text-primary: #1a202c; - --text-secondary: #4a5568; - --bg-color: #f7fafc; - --border-color: #e2e8f0; - --card-bg: #ffffff; - --success-color: #38a169; - --danger-color: #e53e3e; - --warning-color: #dd6b20; - --font-family: "Public Sans", sans-serif; -} - -body { - font-family: var(--font-family); - background-color: var(--bg-color); - color: var(--text-primary); - margin: 0; - line-height: 1.6; -} - -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -/* Header */ -.site-header { - background-color: var(--card-bg); - border-bottom: 1px solid var(--border-color); - padding: 0 2rem; - position: sticky; - top: 0; - z-index: 100; -} - -.header-container { - max-width: 1280px; - margin: 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - height: 70px; -} - -.logo { - font-size: 1.5rem; - font-weight: 700; - color: var(--primary-color); - text-decoration: none; -} - -.main-nav ul { - margin: 0; - padding: 0; - list-style: none; - display: flex; - gap: 1.5rem; -} - -.main-nav a { - text-decoration: none; - color: var(--text-secondary); - font-weight: 500; - padding: 0.5rem 0; - border-bottom: 2px solid transparent; - transition: color 0.2s, border-color 0.2s; -} - -.main-nav a:hover, -.main-nav a.active { - color: var(--primary-color); - border-bottom-color: var(--primary-color); -} - -.header-actions { - display: flex; - align-items: center; - gap: 1rem; -} - -.user-menu { - position: relative; -} - -.user-menu-btn { - display: flex; - align-items: center; - gap: 0.5rem; - background: none; - border: none; - cursor: pointer; - padding: 0.25rem; - border-radius: 99px; -} - -.user-menu .avatar { - width: 36px; - height: 36px; - border-radius: 50%; - object-fit: cover; -} - -.user-menu-dropdown { - display: none; - position: absolute; - right: 0; - top: 120%; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - min-width: 150px; -} - -.user-menu.is-active .user-menu-dropdown { - display: block; -} - -.user-menu-dropdown a { - display: block; - padding: 0.75rem 1rem; - color: var(--text-primary); - text-decoration: none; -} - -.mobile-menu-btn { - display: none; -} - -/* Main Dashboard Content */ -.dashboard-container { - max-width: 1280px; - width: 100%; - margin: 0 auto; - padding: 2rem; - box-sizing: border-box; -} - -.dashboard-header { - margin-bottom: 2rem; -} - -.dashboard-header h1 { - font-size: 2rem; - font-weight: 700; - margin: 0 0 0.5rem; -} - -.dashboard-header p { - color: var(--text-secondary); - margin: 0; -} - -/* Stats Grid */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.stat-card { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 1.5rem; - display: flex; - align-items: center; - gap: 1rem; -} - -.stat-icon-wrapper { - width: 50px; - height: 50px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - color: #fff; -} -.icon-blue { background-color: #4a6cf7; } -.icon-green { background-color: #38a169; } -.icon-purple { background-color: #805ad5; } - -.stat-info p { - margin: 0; - color: var(--text-secondary); -} -.stat-info span { - font-size: 1.75rem; - font-weight: 700; -} - -/* Panels and Tables */ -.dashboard-grid-full { - display: grid; - gap: 1.5rem; -} - -.panel { - background-color: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 1.5rem; -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.card-header h3 { - margin: 0; - font-size: 1.25rem; -} - -.table-wrapper { - overflow-x: auto; -} - -.data-table { - width: 100%; - border-collapse: collapse; -} - -.data-table th, .data-table td { - padding: 1rem; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.data-table th { - font-weight: 600; - color: var(--text-secondary); -} - -.badge { - padding: 0.25rem 0.75rem; - border-radius: 99px; - font-size: 0.8rem; - font-weight: 600; - text-transform: capitalize; -} - -.badge-success { background-color: #c6f6d5; color: #2f855a; } -.badge-danger { background-color: #fed7d7; color: #c53030; } \ No newline at end of file diff --git a/public/styles/select-stage.css b/public/styles/select-stage.css deleted file mode 100644 index 88f682f..0000000 --- a/public/styles/select-stage.css +++ /dev/null @@ -1,241 +0,0 @@ -:root { - --primary: #607AFB; - --background: #f5f6f8; - --card-bg: #ffffff; - --text-main: #101922; - --text-muted: #6b7280; - --border: #e5e7eb; - --white: #ffffff; -} - -body { - margin: 0; - background-color: var(--background); - font-family: 'Public Sans', sans-serif; - color: var(--text-main); -} - -a { - text-decoration: none; - font-weight: 500; - color: var(--text-muted); - transition: color 0.3s; -} - -.container { - width: 100%; - margin-left: auto; - margin-right: auto; - max-width: 1280px; - padding-left: 1.5rem; - padding-right: 1.5rem; - box-sizing: border-box; -} - -.site-header { - background-color: rgba(245, 246, 248, 0.8); - backdrop-filter: blur(4px); - border-bottom: 1px solid var(--border); - position: sticky; - top: 0; - z-index: 20; - height: 8vh; -} - -.nav-container { - display: flex; - justify-content: space-between; - align-items: center; - height: 100%; -} - -.logo { - font-size: 1.6rem; - font-weight: 700; - color: var(--text-main); -} - -.main-nav { - display: none; /* Hidden on mobile */ -} - -.main-nav ul { - list-style: none; - display: flex; - align-items: center; - gap: 2rem; - margin: 0; - padding: 0; -} - -.main-nav a, .main-nav .logout-link { - font-weight: 500; - color: var(--text-muted); - transition: color 0.3s; -} - -.main-nav a:hover, -.main-nav a.active { - color: var(--primary); -} - -.header-actions { - display: flex; - align-items: center; - gap: 1rem; -} - -.mobile-menu-btn { - display: block; /* Visible on mobile */ - background: none; - border: none; - cursor: pointer; - padding: 0.5rem; -} - -.user-profile { - display: flex; - align-items: center; -} - -.profile-icon { - height: 2.5rem; - width: 2.5rem; - border-radius: 50%; - object-fit: cover; - border: 2px solid var(--border); - cursor: pointer; -} - -.logout-btn-desktop { - display: none; /* Hidden on mobile, shown on desktop */ - padding: 10px 15px; - border-radius: 8px; - border: none; - font-size: 1rem; - background-color: var(--border); - color: var(--text-main); -} - -main.container { - padding-top: 3rem; - padding-bottom: 3rem; -} - -.main-title { - font-size: 2.25rem; - font-weight: 900; - text-align: center; - margin-bottom: 2rem; -} - -.stage-grid { - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; -} - -.stage-card { - display: block; - background-color: var(--card-bg); - border-radius: 0.5rem; - overflow: hidden; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - transition: box-shadow 0.3s ease; -} - -.stage-card:hover { - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); -} - -.card-image { - position: relative; - height: 10rem; -} - -.card-image img { - width: 100%; - height: 100%; - object-fit: cover; - background-color: var(--border); -} - -.card-image .overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.2); - transition: background-color 0.3s ease; -} - -.stage-card:hover .overlay { - background-color: rgba(0, 0, 0, 0.1); -} - -.card-content { - padding: 1rem; -} - -.card-content h3 { - font-size: 1.125rem; - font-weight: 700; - margin: 0; -} - -.card-content p { - font-size: 0.875rem; - color: var(--text-muted); - margin: 0.25rem 0 0; -} - -.stage-selector { - max-width: 60vw; - margin-left: auto; - margin-right: auto; -} - -@media (min-width: 640px) { - .stage-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (min-width: 768px) { - .main-nav { - display: flex; - } - .mobile-menu-btn { - display: none; - } - .logout-btn-desktop { - display: block; - } -} - -@media (min-width: 1024px) { - .stage-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -/* Mobile Nav Menu (when active) */ -@media (max-width: 767px) { - .main-nav.is-active { - display: block; - position: absolute; - top: 8vh; - left: 0; - width: 100%; - background-color: var(--background); - border-bottom: 1px solid var(--border); - padding: 1rem 0; - z-index: 10; - } - .main-nav.is-active ul { - flex-direction: column; - align-items: center; - gap: 1.5rem; - } -} \ No newline at end of file diff --git a/public/styles/signup.css b/public/styles/signup.css deleted file mode 100644 index e7002a6..0000000 --- a/public/styles/signup.css +++ /dev/null @@ -1,164 +0,0 @@ -:root { - --primary: #137fec; - --background-light: #f6f7f8; - --background-dark: #101922; -} - -body { - background-color: var(--background-light); - font-family: 'Public Sans', sans-serif; -} - -.dark body { - background-color: var(--background-dark); -} - -.container { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -} - -.form-wrapper { - width: 100%; - max-width: 28rem; - padding: 2rem; - background-color: #ffffff; - border-radius: 0.75rem; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - margin: 1rem; -} - -.dark .form-wrapper { - background-color: rgba(16, 25, 34, 0.5); -} - -.form-wrapper>*+* { - margin-top: 2rem; -} - -.text-center { - text-align: center; -} - -.title { - font-size: 2.25rem; - font-weight: 700; - color: #111827; -} - -.dark .title { - color: #ffffff; -} - -.subtitle { - margin-top: 0.5rem; - font-size: 0.875rem; - color: #4b5563; -} - -.dark .subtitle { - color: #9ca3af; -} - -.form { - margin-top: 2rem; -} - -.form>*+* { - margin-top: 1.5rem; -} - -.input-group { - display: flex; - flex-direction: column; -} - -.input-group>*+* { - margin-top: 1rem; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.input { - position: relative; - display: block; - width: 95%; - padding-left: 0.75rem; - padding-right: 0.75rem; - padding-top: 0.75rem; - padding-bottom: 0.75rem; - background-color: var(--background-light); - border: 1px solid #d1d5db; - color: #111827; - border-radius: 0.5rem; - font-size: 0.875rem; -} - -.input::placeholder { - color: #6b7280; -} - -.input:focus { - outline: none; - border-color: var(--primary); -} - -.dark .input { - background-color: rgba(16, 25, 34, 0.7); - border-color: #374151; - color: #ffffff; -} - -.dark .input::placeholder { - color: #9ca3af; -} - -.button { - display: flex; - justify-content: center; - width: 100%; - padding-top: 0.75rem; - padding-bottom: 0.75rem; - padding-left: 1rem; - padding-right: 1rem; - border: 1px solid transparent; - font-size: 0.875rem; - font-weight: 500; - border-radius: 0.5rem; - color: #ffffff; - background-color: var(--primary); -} - -.button:hover { - background-color: rgba(19, 127, 236, 0.9); -} - -.button:focus { - outline: none; -} - -.link { - font-size: 0.875rem; - text-align: center; -} - -.link a { - font-weight: 500; - color: var(--primary); -} - -.link a:hover { - color: rgba(19, 127, 236, 0.9); -} \ No newline at end of file diff --git a/public/styles/stage-details.css b/public/styles/stage-details.css deleted file mode 100644 index 0ed0ac6..0000000 --- a/public/styles/stage-details.css +++ /dev/null @@ -1,440 +0,0 @@ -:root { - --primary: #137fec; - --background-light: #f6f7f8; - --background-dark: #101922; - --gray-900: #111827; - --gray-800: #1f2937; - --gray-500: #6b7280; - --gray-400: #9ca3af; - --gray-200: #e5e7eb; - --white: #ffffff; - --green-100: #f0fdf4; - --green-800: #166534; - --green-900: #14532d; - --green-300: #86efac; - --green-500: #22c55e; - --blue-100: #eff6ff; - --blue-800: #1e40af; - --blue-900: #1e3a8a; - --blue-300: #93c5fd; - --blue-500: #3b82f6; -} - -body { - background-color: var(--background-light); - font-family: 'Public Sans', sans-serif; - color: var(--gray-800); -} - -.dark body { - background-color: var(--background-dark); - color: var(--gray-200); -} - -.material-symbols-outlined { - font-variation-settings: - 'FILL' 0, - 'wght' 400, - 'GRAD' 0, - 'opsz' 24; -} - -.flex { - display: flex; -} - -.flex-col { - flex-direction: column; -} - -.min-h-screen { - min-height: 100vh; -} - -header { - background-color: rgba(246, 247, 248, 0.8); - backdrop-filter: blur(4px); - position: sticky; - top: 0; - z-index: 10; - border-bottom: 1px solid rgba(19, 127, 236, 0.2); -} - -.dark header { - background-color: rgba(16, 25, 34, 0.8); - border-bottom-color: rgba(19, 127, 236, 0.3); -} - -.container { - width: 100%; - margin-left: auto; - margin-right: auto; - padding-left: 1rem; - padding-right: 1rem; -} - -@media (min-width: 640px) { - .container { - padding-left: 1.5rem; - padding-right: 1.5rem; - } -} - -@media (min-width: 1024px) { - .container { - padding-left: 2rem; - padding-right: 2rem; - } -} - -.items-center { - align-items: center; -} - -.justify-between { - justify-content: space-between; -} - -.h-16 { - height: 4rem; -} - -.gap-4 { - gap: 1rem; -} - -.text-primary { - color: var(--primary); -} - -.text-3xl { - font-size: 1.875rem; -} - -.text-xl { - font-size: 1.25rem; -} - -.font-bold { - font-weight: 700; -} - -.text-gray-900 { - color: var(--gray-900); -} - -.dark .text-gray-900 { - color: var(--white); -} - -.hidden { - display: none; -} - -@media (min-width: 640px) { - .sm-flex { - display: flex; - } -} - -.gap-6 { - gap: 1.5rem; -} - -.text-sm { - font-size: 0.875rem; -} - -.font-medium { - font-weight: 500; -} - -.hover-text-primary:hover { - color: var(--primary); -} - -.dark .hover-text-primary:hover { - color: var(--primary); -} - -.p-2 { - padding: 0.5rem; -} - -.rounded-full { - border-radius: 9999px; -} - -.hover-bg-primary-20:hover { - background-color: rgba(19, 127, 236, 0.2); -} - -.dark .hover-bg-primary-20:hover { - background-color: rgba(19, 127, 236, 0.3); -} - -.h-10 { - height: 2.5rem; -} - -.w-10 { - width: 2.5rem; -} - -.object-cover { - object-fit: cover; -} - -.flex-grow { - flex-grow: 1; -} - -.py-8 { - padding-top: 2rem; - padding-bottom: 2rem; -} - -.max-w-4xl { - max-width: 64rem; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.mb-8 { - margin-bottom: 2rem; -} - -.text-4xl { - font-size: 2.25rem; -} - -.text-gray-500 { - color: var(--gray-500); -} - -.dark .text-gray-500 { - color: var(--gray-400); -} - -.mt-2 { - margin-top: 0.5rem; -} - -.bg-background-light { - background-color: var(--background-light); -} - -.dark .bg-background-light { - background-color: var(--background-dark); -} - -.rounded-lg { - border-radius: 0.5rem; -} - -.shadow-sm { - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.border { - border-width: 1px; -} - -.border-primary-20 { - border-color: rgba(19, 127, 236, 0.2); -} - -.dark .border-primary-20 { - border-color: rgba(19, 127, 236, 0.3); -} - -.p-6 { - padding: 1.5rem; -} - -.border-b { - border-bottom-width: 1px; -} - -.text-lg { - font-size: 1.125rem; -} - -.font-semibold { - font-weight: 600; -} - -.divide-y>li+li { - border-top-width: 1px; -} - -.divide-primary-20 { - border-color: rgba(19, 127, 236, 0.2); -} - -.dark .divide-primary-20 { - border-color: rgba(19, 127, 236, 0.3); -} - -.p-4 { - padding: 1rem; -} - -.hover-bg-primary-10:hover { - background-color: rgba(19, 127, 236, 0.1); -} - -.dark .hover-bg-primary-10:hover { - background-color: rgba(19, 127, 236, 0.2); -} - -.transition-colors { - transition: background-color 0.2s, color 0.2s; -} - -.bg-primary-20 { - background-color: rgba(19, 127, 236, 0.2); -} - -.dark .bg-primary-20 { - background-color: rgba(19, 127, 236, 0.3); -} - -.p-3 { - padding: 0.75rem; -} - -.text-gray-800 { - color: var(--gray-800); -} - -.dark .text-gray-800 { - color: var(--gray-200); -} - -.gap-2 { - gap: 0.5rem; -} - -.inline-flex { - display: inline-flex; -} - -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} - -.py-1 { - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - -.bg-green-100 { - background-color: var(--green-100); -} - -.text-green-800 { - color: var(--green-800); -} - -.dark .bg-green-100 { - background-color: var(--green-900); -} - -.dark .text-green-800 { - color: var(--green-300); -} - -.w-2 { - width: 0.5rem; -} - -.h-2 { - height: 0.5rem; -} - -.mr-2 { - margin-right: 0.5rem; -} - -.bg-green-500 { - background-color: var(--green-500); -} - -.bg-blue-100 { - background-color: var(--blue-100); -} - -.text-blue-800 { - color: var(--blue-800); -} - -.dark .bg-blue-100 { - background-color: var(--blue-900); -} - -.dark .text-blue-800 { - color: var(--blue-300); -} - -.bg-blue-500 { - background-color: var(--blue-500); -} - -.mt-8 { - margin-top: 2rem; -} - -.text-center { - text-align: center; -} - -.mt-1 { - margin-top: 0.25rem; -} - -.mb-4 { - margin-bottom: 1rem; -} - -.bg-primary { - background-color: var(--primary); -} - -.text-white { - color: var(--white); -} - -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} - -.hover-bg-primary-90:hover { - background-color: rgba(19, 127, 236, 0.9); -} - -footer { - background-color: var(--background-light); - border-top: 1px solid rgba(19, 127, 236, 0.2); - margin-top: 2rem; -} - -.dark footer { - background-color: var(--background-dark); - border-top-color: rgba(19, 127, 236, 0.3); -} - -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} \ No newline at end of file diff --git a/public/styles/stage-info.css b/public/styles/stage-info.css deleted file mode 100644 index 6dd8931..0000000 --- a/public/styles/stage-info.css +++ /dev/null @@ -1,262 +0,0 @@ -/* ------------------- */ -/* Variables & Globals */ -/* ------------------- */ -:root { - --primary: #137fec; - --background-light: #f6f7f8; - --gray-400: #9ca3af; - --gray-500: #6b7280; - --gray-800: #1f2937; - --gray-900: #111827; - --white: #ffffff; - --green-100: #dcfce7; - --green-500: #22c55e; - --green-800: #166534; - --blue-100: #dbeafe; - --blue-500: #3b82f6; - --blue-800: #1e40af; - --active: #607AFB; - --text-muted: #6b7280; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: "Public Sans", sans-serif; - background-color: var(--background-light); - color: var(--gray-800); -} - -.material-symbols-outlined { - font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; -} - -.page-wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} -.container { - width: 100%; - max-width: 1280px; - margin: 0 auto; - padding: 0 1.5rem; -} - -/* Header & Navigation */ -.site-header { - position: sticky; - top: 0; - z-index: 10; - background-color: rgba(246, 247, 248, 0.8); - backdrop-filter: blur(4px); - border-bottom: 1px solid rgba(19, 127, 236, 0.2); -} - -.navbar { - display: flex; - justify-content: space-between; - align-items: center; - height: 8vh; -} - -.main-nav .active { - color: var(--active); -} - -.logo, .header-actions, .vehicle-info { - display: flex; - align-items: center; - gap: 1rem; -} - -.logo .material-symbols-outlined { - color: var(--primary); - font-size: 2rem; -} -.logo h1 { - font-size: 1.25rem; - font-weight: 700; - color: var(--gray-900); -} - -.main-nav { - display: none; /* Hidden on mobile */ - gap: 1.5rem; -} -.main-nav a { - text-decoration: none; - font-weight: 500; - font-weight: 500; - color: var(--text-muted); - transition: color 0.3s; -} -.main-nav a:hover { - color: var(--active); -} - -.icon-button { - background: none; - border: none; - color: inherit; - padding: 0.5rem; - border-radius: 9999px; - cursor: pointer; - transition: background-color 0.2s; -} -.icon-button:hover { background-color: rgba(19, 127, 236, 0.2); } - -.avatar { - height: 2.5rem; - width: 2.5rem; - border-radius: 9999px; - object-fit: cover; -} - -/* Main Content */ -main { - flex-grow: 1; - padding: 2rem 0; -} -.page-header { - margin-bottom: 2rem; - max-width: 896px; - margin: 0 auto 2rem; -} -.page-header h2 { - font-size: 2.25rem; - font-weight: 700; - color: var(--gray-900); -} -.page-header p { - color: var(--gray-500); - margin-top: 0.5rem; -} - -.card { - max-width: 896px; - margin: 0 auto; - background-color: var(--background-light); - border: 1px solid rgba(19, 127, 236, 0.2); - border-radius: 0.5rem; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.arrival-card .card-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(19, 127, 236, 0.2); -} - -.card-header h3 { - font-size: 1.125rem; - font-weight: 600; - color: var(--gray-900); -} - -.arrival-list { list-style: none; } -.arrival-list li:not(:last-child) { - border-bottom: 1px solid rgba(19, 127, 236, 0.2); -} - -.arrival-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - transition: background-color 0.2s; -} -.arrival-item:hover { background-color: rgba(19, 127, 236, 0.1); } - -.vehicle-icon-wrapper { - background-color: rgba(19, 127, 236, 0.2); - padding: 0.75rem; - border-radius: 0.5rem; - color: var(--primary); -} - -.plate-number { font-weight: 600; } -.eta { - font-size: 0.875rem; - color: var(--gray-500); -} - -/* Status Badges */ -.status-badge { - display: inline-flex; - align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; -} -.status-dot { - width: 0.5rem; - height: 0.5rem; - margin-right: 0.5rem; - border-radius: 50%; -} -.status-at-stage { background-color: var(--green-100); color: var(--green-800); } -.status-at-stage .status-dot { background-color: var(--green-500); } - -.status-arrived { background-color: var(--green-100); color: var(--green-800); } -.status-arrived .status-dot { background-color: var(--green-500); } - -.status-approaching { background-color: var(--blue-100); color: var(--blue-800); } -.status-approaching .status-dot { background-color: var(--blue-500); } - -/* Feedback Card */ -.feedback-card { - margin-top: 2rem; - padding: 1.5rem; - text-align: center; -} -.feedback-card h3 { - font-size: 1.125rem; - font-weight: 600; - color: var(--gray-900); -} -.feedback-card p { - font-size: 0.875rem; - color: var(--gray-500); - margin: 0.25rem 0 1rem; -} - -.btn { - background-color: var(--primary); - color: var(--white); - font-weight: 700; - padding: 0.5rem 1rem; - border: none; - border-radius: 0.5rem; - cursor: pointer; - transition: background-color 0.2s; - display: inline-flex; - align-items: center; - gap: 0.5rem; -} -.btn:hover { background-color: #106ac4; } - -/* Footer */ -.site-footer { - margin-top: 2rem; - border-top: 1px solid rgba(19, 127, 236, 0.2); -} -.site-footer .container { - padding: 1rem 1.5rem; - text-align: center; - font-size: 0.875rem; - color: var(--gray-500); -} - -/* Responsive */ -@media (min-width: 640px) { - .main-nav { display: flex; } -} - -.avatar:hover { - cursor: pointer; -} \ No newline at end of file diff --git a/public/styles/user-profile.css b/public/styles/user-profile.css deleted file mode 100644 index 91d7cc9..0000000 --- a/public/styles/user-profile.css +++ /dev/null @@ -1,238 +0,0 @@ -.hidden { display: none !important; } - -.main-content { - padding-top: 3rem; - padding-bottom: 3rem; -} - -.profile-card { - max-width: 896px; - margin: 0 auto; - background-color: var(--card-bg); - border: 1px solid var(--border); - border-radius: 0.75rem; - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - padding: 2rem; - overflow: hidden; -} - -.card-header h2 { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 0.5rem; -} - -.card-header p { - color: var(--text-muted); -} - -.profile-details-section { - display: flex; - flex-direction: column; - gap: 2rem; - margin-top: 2rem; - margin-bottom: 2rem; -} - -.profile-picture-container { - flex-shrink: 0; - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -.profile-picture, #profile-img { - width: 8rem; - height: 8rem; - border-radius: 50%; - object-fit: cover; -} - -.hidden { display: none; } - -.divider { - border: 0; - border-top: 1px solid var(--border); - margin: 2rem 0; -} - -.form-grid { - flex-grow: 1; - display: grid; - grid-template-columns: 1fr; - gap: 1.5rem; -} - -.form-group { - display: flex; - flex-direction: column; -} - -label { - font-size: 0.875rem; - font-weight: 500; - margin-bottom: 0.5rem; -} - -.form-input { - width: 100%; - padding: 0.75rem 1rem; - background-color: var(--background); - border: 1px solid var(--border); - border-radius: 0.5rem; -} - -.form-input:focus { - outline: 2px solid transparent; - outline-offset: 2px; - border-color: var(--primary); - box-shadow: 0 0 0 2px var(--primary-light); -} - -.form-actions { - display: flex; - justify-content: flex-end; - grid-column: 1 / -1; -} - -.password-reset-section, .two-factor-section { - padding: 1.5rem 0; -} - -.two-factor-section { - padding: 1.5rem 0; -} - -.two-factor-section h3 { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 1rem; -} - -.two-factor-content { - display: grid; - grid-template-columns: 1fr auto; - gap: 1rem; - align-items: center; -} - -.two-factor-content .description { - color: var(--text-muted); - font-size: 0.875rem; - max-width: 60ch; -} - -.two-factor-content .status-badge { - font-weight: 600; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; -} - -.status-badge.enabled { - background-color: var(--green-100); - color: var(--green-800); -} - -.status-badge.disabled { - background-color: var(--gray-200); - color: var(--gray-800); -} - -.two-factor-content .btn { - justify-self: end; -} - -.password-reset-section h3 { - font-size: 1.25rem; - font-weight: 600; - margin-bottom: 1rem; -} - -.modal-overlay { - position: fixed; - inset: 0; - background-color: rgba(17, 24, 39, 0.6); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 50; -} - -.modal-content { - background-color: white; - border-radius: 0.75rem; - width: 100%; - max-width: 500px; - margin: 1rem; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border); -} - -.modal-header h3 { font-size: 1.125rem; font-weight: 600; } -.modal-body { padding: 1.5rem; display: flex; flex-direction: column; gap: 1.5rem; text-align: center; } -.modal-body .qr-code-container { margin: 0 auto; } -.modal-body .qr-code-container img { border: 4px solid var(--border); border-radius: 0.5rem; } - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - padding: 1rem 1.5rem; - background-color: var(--bg); - border-bottom-left-radius: 0.75rem; - border-bottom-right-radius: 0.75rem; -} - -.btn { - font-weight: 700; - padding: 0.5rem 1rem; - border-radius: 0.5rem; - border: none; - cursor: pointer; - transition: background-color 0.2s; -} - -.btn-primary { - background-color: var(--primary); - color: #fff; -} - -.btn-primary:hover { - background-color: var(--primary-dark); -} - -.btn-secondary { - background-color: var(--primary-light); - color: var(--text-main); -} - -.btn-secondary:hover { - background-color: rgba(19, 127, 236, 0.2); -} - -@media (min-width: 640px) { - .form-grid { - grid-template-columns: repeat(2, 1fr); - } - - .two-factor-content { - grid-template-columns: 1fr auto; - } - .two-factor-content .btn { justify-self: end; } -} - -@media (min-width: 768px) { - .profile-details-section { - flex-direction: row; - align-items: flex-start; - } -} \ No newline at end of file diff --git a/public/user-profile.html b/public/user-profile.html deleted file mode 100644 index eceb895..0000000 --- a/public/user-profile.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - User Profile - - - - - - - - - - - -
    - -
    -
    -
    -

    Profile

    -

    Manage your personal details and preferences.

    -
    - -
    -
    -
    - User profile picture -
    - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    - -
    - -
    -

    Two-Factor Authentication (2FA)

    -
    -
    -

    - Status: Loading... -

    -

    - Add an extra layer of security to your account by requiring a second verification step when you sign in. -

    -
    -
    -
    -
    - -
    - -
    -

    Password Reset

    -

    To change your password, please use the "Forgot Password" - link on the login page.

    - Go to Password Reset -
    -
    -
    - - - -
    - - - \ No newline at end of file diff --git a/server/config/database.js b/server/config/database.js deleted file mode 100644 index 22818d9..0000000 --- a/server/config/database.js +++ /dev/null @@ -1,20 +0,0 @@ -const { Sequelize } = require("sequelize"); -require("dotenv").config(); - -const sequelize = new Sequelize( - process.env.DB_NAME, - process.env.DB_USER, - process.env.DB_PASSWORD, - { - host: process.env.DB_HOST, - dialect: "mariadb", - logging: false, - } -); - -sequelize - .authenticate() - .then(() => console.log("✅ MariaDB connected successfully")) - .catch((err) => console.error("❌ Database connection failed:", err)); - -module.exports = sequelize; \ No newline at end of file diff --git a/server/config/passport.js b/server/config/passport.js deleted file mode 100644 index 31efb69..0000000 --- a/server/config/passport.js +++ /dev/null @@ -1,56 +0,0 @@ -const passport = require("passport"); -const GoogleStrategy = require("passport-google-oauth20").Strategy; -const { User } = require("../models"); -require("dotenv").config(); - -// Construct the absolute callback URL. This is crucial for Google OAuth. -const callbackURL = `${process.env.SERVER_BASE_URL || `http://localhost:${process.env.PORT || 5000}`}/api/auth/google/callback`; - -passport.use( - new GoogleStrategy( - { - clientID: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackURL: callbackURL, - // Pass the request object to the callback to access session info, etc. - passReqToCallback: true, - }, - async (req, accessToken, refreshToken, profile, done) => { - try { - const email = profile.emails[0].value; - let user = await User.findOne({ where: { email: email } }); - - if (user) { - // User exists. If they don't have a googleId, link the account. - if (!user.googleId) { - user.googleId = profile.id; - user.profilePictureUrl = - user.profilePictureUrl || - (profile.photos && profile.photos.length > 0 - ? profile.photos[0].value - : null); - await user.save(); - } - } else { - // User does not exist, create a new one. - user = await User.create({ - googleId: profile.id, - name: profile.displayName, - email: email, - profilePictureUrl: - profile.photos && profile.photos.length > 0 - ? profile.photos[0].value - : null, - role: "Passenger", // Default role for new Google sign-ups - }); - } - - // The 'done' callback attaches the user to req.user - return done(null, user); - } catch (err) { - console.error(" ❌ Error in Google OAuth strategy:", err); - return done(err, null); - } - }, - ), -); diff --git a/server/controllers/adminController.js b/server/controllers/adminController.js deleted file mode 100644 index 92612e7..0000000 --- a/server/controllers/adminController.js +++ /dev/null @@ -1,367 +0,0 @@ -const { Stage, Vehicle, User, Feedback } = require("../models"); -const bcrypt = require("bcrypt"); -const { Op } = require("sequelize"); -const { validationResult } = require("express-validator"); -const { getLocationName } = require("../services/googleMapsService"); -const { getLocations } = require("../services/locationCache"); - -// --- Stage Management --- - -const getAllStages = async (req, res, next) => { - try { - const rawLimit = parseInt(req.query.limit, 10); - const rawOffset = parseInt(req.query.offset, 10); - - const limit = Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : 25; - const offset = - Number.isInteger(rawOffset) && rawOffset >= 0 ? rawOffset : 0; - const { count, rows } = await Stage.findAndCountAll({ - include: { model: Vehicle, through: { attributes: [] } }, - order: [["name", "ASC"]], - limit, - offset, - }); - res.status(200).json({ stages: rows, total: count }); - } catch (error) { - next(error); - } -}; - -const createStage = async (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - try { - const { name, latitude, longitude } = req.body; - - const newStage = await Stage.create({ name, latitude, longitude }); - res.status(201).json(newStage); - } catch (error) { - next(error); - } -}; - -// --- Vehicle Management --- - -const getAllVehicles = async (req, res, next) => { - try { - const limit = parseInt(req.query.limit, 10) || 25; - const offset = parseInt(req.query.offset, 10) || 0; - const { count, rows } = await Vehicle.findAndCountAll({ - order: [["plateNumber", "ASC"]], - limit, - offset, - }); - res.status(200).json({ vehicles: rows, total: count }); - } catch (error) { - next(error); - } -}; - -const createVehicle = async (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - try { - const { carId, plateNumber, ownerId } = req.body; - const newVehicle = await Vehicle.create({ - carId, - plateNumber, - ownerId: ownerId || null, - }); - res.status(201).json(newVehicle); - } catch (error) { - next(error); - } -}; - -const deleteVehicle = async (req, res, next) => { - try { - const { id } = req.params; - const vehicle = await Vehicle.findByPk(id); - - if (!vehicle) { - return res.status(404).json({ message: "Vehicle not found" }); - } - - await vehicle.destroy(); - res.status(200).json({ message: "Vehicle deleted successfully." }); - } catch (error) { - next(error); - } -}; - -// --- Association Management --- - -const assignVehicleToStage = async (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - try { - const { stageId, vehicleId } = req.body; - const stage = await Stage.findByPk(stageId); - const vehicle = await Vehicle.findByPk(vehicleId); - - if (!stage || !vehicle) { - return res.status(404).json({ message: "Stage or Vehicle not found" }); - } - - await stage.addVehicle(vehicle); - res.status(200).json({ message: "Vehicle assigned to stage successfully" }); - } catch (error) { - next(error); - } -}; - -const removeVehicleFromStage = async (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - try { - const { stageId, vehicleId } = req.params; - const stage = await Stage.findByPk(stageId); - const vehicle = await Vehicle.findByPk(vehicleId); - if (!stage || !vehicle) - return res.status(404).json({ message: "Stage or Vehicle not found" }); - await stage.removeVehicle(vehicle); - res.status(200).json({ message: "Vehicle unassigned successfully" }); - } catch (error) { - next(error); - } -}; - -const getDashboardData = async (req, res, next) => { - try { - const vehicles = await Vehicle.findAll({ - order: [["plateNumber", "ASC"]], - include: { - model: Stage, - attributes: ["name"], - through: { attributes: [] }, // Don't include the join table attributes - }, - }); - const vehicleLocations = await getLocations(); - - const vehicleDetails = await Promise.all( - vehicles.map(async (vehicle) => { - const locationData = vehicleLocations.get(vehicle.id); - - let locationName = "Offline"; - let latitude = null; - let longitude = null; - if (locationData && locationData.latitude && locationData.longitude) { - locationName = await getLocationName(locationData); - latitude = locationData.latitude; - longitude = locationData.longitude; - } - - // Dynamically construct the route from the eager-loaded stages - let route = "Not Assigned"; - if (vehicle.Stages && vehicle.Stages.length > 0) { - // Simple route: first stage to last stage - if (vehicle.Stages.length > 1) { - // Sort stages by name to get a consistent route order, though a dedicated `order` column would be better. - const sortedStages = [...vehicle.Stages].sort((a, b) => - a.name.localeCompare(b.name), - ); - route = `${sortedStages[0].name} - ${sortedStages[sortedStages.length - 1].name}`; - } else { - route = vehicle.Stages[0].name; - } - } - - return { - id: vehicle.id, - plateNumber: vehicle.plateNumber, - status: locationData ? locationData.gpsStatus : "Offline", - location: locationName, - latitude, - longitude, - route: route, - }; - }), - ); - res.status(200).json({ vehicles: vehicleDetails }); - } catch (error) { - next(error); - } -}; - -const getAllFeedback = async (req, res, next) => { - try { - const limit = parseInt(req.query.limit, 10) || 10; - const offset = parseInt(req.query.offset, 10) || 0; - - const { count, rows } = await Feedback.findAndCountAll({ - order: [["createdAt", "DESC"]], - include: [ - { model: User, attributes: ["name"] }, - { model: Vehicle, attributes: ["plateNumber"] }, - ], - limit, - offset, - }); - res.status(200).json({ feedback: rows, total: count }); - } catch (error) { - next(error); - } -}; - -const getAllUsers = async (req, res, next) => { - try { - const { q = "", role = "" } = req.query; - const rawLimit = parseInt(req.query.limit, 10); - const rawOffset = parseInt(req.query.offset, 10); - - const limit = Number.isInteger(rawLimit) && rawLimit > 0 ? rawLimit : 25; - const offset = - Number.isInteger(rawOffset) && rawOffset >= 0 ? rawOffset : 0; - - const whereClause = { - id: { [Op.ne]: req.user.id }, // Exclude the current admin - }; - - if (q) { - whereClause[Op.or] = [ - { name: { [Op.like]: `%${q}%` } }, - { email: { [Op.like]: `%${q}%` } }, - ]; - } - - if (role && ["Admin", "MatAdmin", "Passenger"].includes(role)) { - whereClause.role = role; - } - - const { count, rows } = await User.findAndCountAll({ - where: whereClause, - attributes: [ - "id", - "name", - "email", - "role", - "twoFactorEnabled", - "disabled", - ], // 'disabled' is a virtual field - order: [["name", "ASC"]], - limit: limit, - offset: offset, - }); - res.status(200).json({ users: rows, total: count }); - } catch (error) { - next(error); - } -}; - -const updateUserByAdmin = async (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - try { - const { id } = req.params; - const { disabled } = req.body; - - const user = await User.findByPk(id); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - const updatePayload = { disabled: !!disabled }; - - // If disabling the user, invalidate all their existing tokens by updating the timestamp. - if (disabled) { - updatePayload.tokensValidFrom = new Date(); - } - - await user.update(updatePayload); - res.status(200).json({ - message: `User has been ${disabled ? "blocked" : "unblocked"}.`, - }); - } catch (error) { - next(error); - } -}; - -const deleteUserByAdmin = async (req, res, next) => { - try { - const { id } = req.params; - const user = await User.findByPk(id); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - await user.destroy(); - res.status(200).json({ message: "User deleted successfully." }); - } catch (error) { - next(error); - } -}; - -const getAllMatAdmins = async (req, res, next) => { - try { - const matAdmins = await User.findAll({ - where: { role: "MatAdmin" }, - attributes: ["id", "name"], // Only need id and name for the dropdown - order: [["name", "ASC"]], - }); - res.status(200).json(matAdmins); - } catch (error) { - next(error); - } -}; - -const createUserByAdmin = async (req, res, next) => { - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - const { name, email, password, role } = req.body; - - if (!["Admin", "MatAdmin"].includes(role)) { - return res.status(400).json({ message: "Invalid role specified." }); - } - - // Hash password - const hashedPassword = await bcrypt.hash(password, 10); - - // Create user - const user = await User.create({ - name, - email, - password: hashedPassword, - role, - }); - delete user.dataValues.password; - - res.status(201).json({ message: "User created successfully", user }); - } catch (error) { - next(error); - } -}; - -module.exports = { - getAllStages, - createStage, - getAllVehicles, - createVehicle, - deleteVehicle, - assignVehicleToStage, - removeVehicleFromStage, - getDashboardData, - getAllMatAdmins, - getAllFeedback, - getAllUsers, - updateUserByAdmin, - deleteUserByAdmin, - createUserByAdmin, -}; diff --git a/server/controllers/authController.js b/server/controllers/authController.js deleted file mode 100644 index 2bc3de2..0000000 --- a/server/controllers/authController.js +++ /dev/null @@ -1,659 +0,0 @@ -const bcrypt = require("bcrypt"); -const jwt = require("jsonwebtoken"); -const { User } = require("../models"); -const nodemailer = require("nodemailer"); -const speakeasy = require("speakeasy"); -const qrcode = require("qrcode"); -const { validationResult } = require("express-validator"); -const { v4: uuidv4 } = require("uuid"); -const { addToBlocklist } = require("../services/tokenBlocklistService"); - -// Login user -const loginUser = async (req, res, next) => { - // ValidationResult check is done in authRoutes.js - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - const { email, password } = req.body; - const user = await User.findOne({ where: { email } }); - - if (!user) return res.status(404).json({ message: "User not found" }); - - if (user.disabled) - return res.status(403).json({ - message: - "Your account has been disabled. Please contact an administrator.", - }); - - // If user has no password, they likely registered via OAuth. - // Prevent them from logging in with a password. - if (!user.password) { - return res.status(400).json({ - message: "Please sign in using Google.", - }); - } - - const validPassword = await bcrypt.compare(password, user.password); - if (!validPassword) - return res.status(401).json({ message: "Invalid password" }); - - // Check if 2FA is enabled for the user - if (user.twoFactorEnabled) { - // If 2FA is enabled, generate a temporary token - const tempToken = jwt.sign( - { id: user.id, role: user.role }, - process.env.JWT_SECRET, - { expiresIn: "5m" }, // Short-lived token - ); - return res.status(200).json({ - twoFactorRequired: true, - tempToken: tempToken, - }); - } - - // Generate tokens - const accessToken = jwt.sign( - { - id: user.id, - role: user.role, - jti: uuidv4(), - disabled: user.disabled, // Include disabled status in token - // Include tokensValidFrom timestamp in token for quick validation without DB query - // Convert to milliseconds for consistent comparison with iat - tokensValidFrom: user.tokensValidFrom.getTime(), - }, - process.env.JWT_SECRET, - { expiresIn: "15m" }, - ); - - const refreshToken = jwt.sign( - { id: user.id, jti: uuidv4() }, // JTI for refresh token blocklisting - process.env.JWT_REFRESH_SECRET, - { expiresIn: "7d" }, - ); - - // Set refresh token in a secure, HTTP-only cookie - res.cookie("refreshToken", refreshToken, { - httpOnly: true, - secure: true, // Must be true if sameSite is 'none' - sameSite: "none", - }); - - res - .status(200) - .json({ message: "Login successful", accessToken, role: user.role }); - } catch (error) { - next(error); - } -}; - -const refreshAccessToken = (req, res, next) => { - try { - // ValidationResult check is done in authRoutes.js - const refreshToken = req.cookies.refreshToken; - if (!refreshToken) - return res.status(401).json({ message: "Refresh token missing" }); - - // CSRF Protection: Require the (potentially expired) access token in the Authorization header. - // This proves the client possesses both tokens, which a CSRF attacker cannot do. - const authHeader = req.header("Authorization"); - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return res - .status(401) - .json({ message: "Access token missing for refresh" }); - } - const accessToken = authHeader.split(" ")[1]; - - // Verify the refresh token first - jwt.verify( - refreshToken, - process.env.JWT_REFRESH_SECRET, - async (err, oldDecodedRefresh) => { - if (err) - return res.status(403).json({ message: "Invalid refresh token" }); - - // Check if the refresh token has already been used/invalidated. - if ( - oldDecodedRefresh.jti && - (await isBlocklisted(oldDecodedRefresh.jti)) - ) { - return res - .status(401) - .json({ message: "Token has been invalidated" }); - } - - try { - // Verify the access token's signature (ignoring expiration) to prove it was issued by us. - const decodedAccess = jwt.verify( - accessToken, - process.env.JWT_SECRET, - { ignoreExpiration: true }, - ); - if (!decodedAccess || decodedAccess.id !== oldDecodedRefresh.id) { - return res.status(403).json({ message: "Token mismatch" }); - } - } catch (verifyError) { - // Catches malformed tokens that would otherwise crash the server. - return res - .status(401) - .json({ message: "Invalid access token for refresh" }); - } - - const user = await User.findByPk(oldDecodedRefresh.id); - // Also check if the user is disabled before issuing a new token. - if (!user || user.disabled) { - return res - .status(403) - .json({ message: "User account is disabled or not found" }); - } - // Check if tokens have been invalidated since this refresh token was issued. - if (oldDecodedRefresh.iat * 1000 < user.tokensValidFrom.getTime()) { - return res - .status(401) - .json({ message: "Token has been invalidated" }); - } - - const newAccessToken = jwt.sign( - { - id: user.id, - role: user.role, - jti: uuidv4(), - disabled: user.disabled, - tokensValidFrom: user.tokensValidFrom.getTime(), - }, - process.env.JWT_SECRET, - { expiresIn: "15m" }, - ); - - // Implement Refresh Token Rotation: issue a new refresh token. - const newRefreshToken = jwt.sign( - { id: user.id, jti: uuidv4() }, // JTI for refresh token blocklisting - process.env.JWT_REFRESH_SECRET, - { expiresIn: "7d" }, - ); - - // Invalidate the old refresh token by adding it to the blocklist. - if (oldDecodedRefresh.jti && oldDecodedRefresh.exp) { - await addToBlocklist(oldDecodedRefresh.jti, oldDecodedRefresh.exp); - } - - res.cookie("refreshToken", newRefreshToken, { - httpOnly: true, - secure: true, // Must be true if sameSite is 'none' - sameSite: "none", - }); - - res.status(200).json({ - accessToken: newAccessToken, - role: user.role, - }); - }, - ); - } catch (error) { - next(error); - } -}; - -// Create a new user -const createUser = async (req, res, next) => { - // ValidationResult check is done in authRoutes.js - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - const { name, email, password } = req.body; - let role = "Passenger"; // Default role - - // --- Bootstrap Logic: Make the first user an Admin --- - const userCount = await User.count(); - if (userCount === 0) { - role = "Admin"; - console.log( - "✨ No users found. Promoting first registered user to Admin.", - ); - } - // --- End Bootstrap Logic --- - - // Hash password - const hashedPassword = await bcrypt.hash(password, 10); - - // Create user - const user = await User.create({ - name, - email, - password: hashedPassword, - role: role, - }); - delete user.dataValues.password; - - res.status(201).json({ message: "User created successfully", user }); - } catch (error) { - next(error); - } -}; - -const logoutUser = async (req, res, next) => { - try { - // Invalidate the access token by adding it to a blocklist. - const authHeader = req.header("Authorization"); - if (authHeader && authHeader.startsWith("Bearer ")) { - const token = authHeader.split(" ")[1]; - const decoded = jwt.decode(token); - if (decoded && decoded.jti && decoded.exp) { - await addToBlocklist(decoded.jti, decoded.exp); - } - } - - // Also invalidate the refresh token. - const refreshToken = req.cookies.refreshToken; - if (refreshToken) { - try { - // Verify the signature to prevent DoS attacks with forged tokens. - const decoded = jwt.verify( - refreshToken, - process.env.JWT_REFRESH_SECRET, - ); - if (decoded && decoded.jti && decoded.exp) { - await addToBlocklist(decoded.jti, decoded.exp); - } - } catch (e) { - // Ignore invalid/malformed refresh tokens during logout. - } - } - - // Clear the refresh token cookie. - res.clearCookie("refreshToken", { - httpOnly: true, - secure: true, // Must be true if sameSite is 'none' - sameSite: "none", - }); - - res.status(200).json({ message: "Logged out successfully." }); - } catch (error) { - next(error); - } -}; - -const me = async (req, res, next) => { - try { - if (!req.user) { - return res.status(401).json({ message: "Not Authenticated" }); - } - - const user = await User.findByPk(req.user.id, { - attributes: { exclude: ["password"] }, - }); - - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - // Convert to a plain object to add custom properties - const userObject = user.get({ plain: true }); - - // Add a flag to indicate if the user is a Google user - userObject.isGoogleUser = !!user.googleId; - // Don't send the googleId itself to the client - delete userObject.googleId; - - res.status(200).json(userObject); - } catch (error) { - next(error); - } -}; - -const updateProfile = async (req, res, next) => { - // ValidationResult check is done in authRoutes.js - try { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - const { name, phoneNumber } = req.body; - const userId = req.user.id; - - const user = await User.findByPk(userId); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - const fieldsToUpdate = {}; - if (name) { - fieldsToUpdate.name = name; - } - - // Normalize and validate phone number if provided - if (phoneNumber !== undefined) { - // Basic validation and normalization (can be improved) - let normalizedPhone = phoneNumber.replace(/\s+/g, ""); // Remove spaces - // Example: If it starts with 0, and is not already international, assume Kenyan and prepend +254 - if (normalizedPhone.startsWith("0") && !normalizedPhone.startsWith("+")) { - normalizedPhone = `+254${normalizedPhone.substring(1)}`; // Basic assumption for Kenyan numbers - } // For full internationalization, use a library like libphonenumber-js - fieldsToUpdate.phoneNumber = normalizedPhone; - } - - await user.update(fieldsToUpdate); - - res.status(200).json({ message: "Profile updated successfully" }); - } catch (error) { - next(error); - } -}; - -const requestPasswordReset = async (req, res, next) => { - const { email } = req.body; - - try { - // Sequelize-style lookup - const user = await User.findOne({ where: { email } }); - - // To prevent user enumeration and timing attacks, and to handle OAuth-only users, - // we proceed only if a user with a password exists. Otherwise, we perform a dummy - // operation and return a generic message. - if (!user || !user.password) { - // To prevent timing attacks, perform a dummy hash operation - // that takes a similar amount of time as the JWT signing. - await bcrypt.hash(uuidv4(), 1); - return res.status(200).json({ - message: - "If an account with that email exists, a password reset link has been sent.", - }); - } - - const secret = process.env.JWT_SECRET + user.password; - const token = jwt.sign({ id: user.id, email: user.email }, secret, { - expiresIn: "1h", - }); - - // Use env vars for mail credentials. For Gmail use an App Password or OAuth2. - let transporter; - if (process.env.EMAIL_USER && process.env.EMAIL_PASS) { - transporter = nodemailer.createTransport({ - host: process.env.EMAIL_HOST || "smtp.gmail.com", - port: process.env.EMAIL_PORT ? Number(process.env.EMAIL_PORT) : 465, - secure: process.env.EMAIL_SECURE !== "false", - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASS, - }, - }); - - // verify SMTP credentials early, fall back to Ethereal on failure - try { - await transporter.verify(); - } catch (err) { - console.error( - "SMTP verify failed, falling back to Ethereal test account:", - err.message || err, - ); - const testAccount = await nodemailer.createTestAccount(); - transporter = nodemailer.createTransport({ - host: "smtp.ethereal.email", - port: 587, - secure: false, - auth: { user: testAccount.user, pass: testAccount.pass }, - }); - console.warn( - "Using Ethereal test account. Set correct SMTP env vars for production.", - ); - } - } else { - // Dev fallback - const testAccount = await nodemailer.createTestAccount(); - transporter = nodemailer.createTransport({ - host: "smtp.ethereal.email", - port: 587, - secure: false, - auth: { user: testAccount.user, pass: testAccount.pass }, - }); - console.warn( - "EMAIL_USER / EMAIL_PASS not set — using Ethereal test account for email delivery.", - ); - } - - const mailOptions = { - to: user.email, - from: - process.env.EMAIL_FROM || - process.env.EMAIL_USER || - "no-reply@example.com", - subject: "Password Reset Request", - text: `If you didn't request this, ignore this email.\n\n Reset Token: ${token}\n\n User ID: ${user.id}`, - }; - - // Send email in a separate try/catch to prevent leaking user existence on SMTP failure. - try { - const info = await transporter.sendMail(mailOptions); - // if using Ethereal, log preview URL - const preview = nodemailer.getTestMessageUrl(info); - if (preview) console.log("Preview email URL:", preview); - } catch (emailError) { - console.error("❌ Failed to send password reset email:", emailError); - // Do not throw an error to the client, just log it. - } - - res.status(200).json({ - message: - "If an account with that email exists, a password reset link has been sent.", - }); - } catch (error) { - next(error); - } -}; - -const resetPassword = async (req, res, next) => { - const { id, token } = req.params; - const { password } = req.body; - - // ValidationResult check is done in authRoutes.js - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - try { - const user = await User.findByPk(id); - if (!user) { - return res.status(400).json({ message: "User not exists!" }); - } - - const secret = process.env.JWT_SECRET + user.password; - - // verify token - jwt.verify(token, secret); - - const encryptedPassword = await bcrypt.hash(password, 10); - - // Sequelize update - await user.update({ - password: encryptedPassword, - tokensValidFrom: new Date(), - }); // Invalidate existing sessions - - res.status(200).json({ message: "Password has been reset" }); - } catch (error) { - next(error); - } -}; - -// --- Two-Factor Authentication Controllers --- - -const generateTwoFactorSecret = async (req, res, next) => { - try { - const user = await User.findByPk(req.user.id); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - const secret = speakeasy.generateSecret({ - name: `Realtime Matatu (${user.email})`, - }); - - // Temporarily store the un-verified secret. - // In a production app, you might want a separate field for a pending secret to not overwrite an existing enabled one. - await user.update({ twoFactorSecret: secret.base32 }); - - qrcode.toDataURL(secret.otpauth_url, (err, data_url) => { - if (err) { - console.error("Error generating QR code:", err); - return res.status(500).json({ message: "Error generating QR code" }); - } - res.json({ secret: secret.base32, qrCodeUrl: data_url }); - }); - } catch (error) { - next(error); - } -}; - -const verifyTwoFactorSecret = async (req, res, next) => { - const { token } = req.body; - try { - const user = await User.findByPk(req.user.id); - if (!user || !user.twoFactorSecret) { - return res - .status(400) - .json({ message: "2FA not set up or user not found." }); - } - - const verified = speakeasy.totp.verify({ - secret: user.twoFactorSecret, - encoding: "base32", - token: token, - }); - - if (verified) { - await user.update({ twoFactorEnabled: true }); - res.json({ message: "2FA enabled successfully" }); - } else { - res.status(400).json({ message: "Invalid 2FA token" }); - } - } catch (error) { - next(error); - } -}; - -const verifyLoginTwoFactor = async (req, res, next) => { - const { tempToken, token } = req.body; - try { - const decoded = jwt.verify(tempToken, process.env.JWT_SECRET); - const user = await User.findByPk(decoded.id); - - if (!user || !user.twoFactorEnabled || !user.twoFactorSecret) { - return res - .status(400) - .json({ message: "2FA is not enabled for this user." }); - } - - const verified = speakeasy.totp.verify({ - secret: user.twoFactorSecret, - encoding: "base32", - token: token, - window: 1, // Allow for a 30-second window of tolerance - }); - - if (verified) { - // 2FA code is correct, issue the final access token - const accessToken = jwt.sign( - { - id: user.id, - role: user.role, - jti: uuidv4(), - disabled: user.disabled, - tokensValidFrom: user.tokensValidFrom.getTime(), - }, - process.env.JWT_SECRET, - { expiresIn: "15m" }, - ); - const refreshToken = jwt.sign( - { id: user.id, jti: uuidv4() }, - process.env.JWT_REFRESH_SECRET, - { expiresIn: "7d" }, - ); // JTI for refresh token blocklisting - - res.cookie("refreshToken", refreshToken, { - httpOnly: true, - secure: true, // Must be true if sameSite is 'none' - sameSite: "none", - }); - res.json({ message: "Login successful", accessToken, role: user.role }); - } else { - res.status(400).json({ message: "Invalid 2FA token" }); - } - } catch (error) { - if (error instanceof jwt.TokenExpiredError) { - return res - .status(401) - .json({ message: "2FA session expired, please log in again." }); - } else if (error instanceof jwt.JsonWebTokenError) { - return res.status(401).json({ message: "Invalid 2FA session token." }); - } else { - // Pass to error handler for other unexpected errors. - next(error); - } - } -}; - -// This function is called after passport successfully authenticates the user. -const googleCallback = (req, res) => { - const user = req.user; - const accessToken = jwt.sign( - { - id: user.id, - role: user.role, - jti: uuidv4(), - disabled: user.disabled, - tokensValidFrom: user.tokensValidFrom.getTime(), - }, - process.env.JWT_SECRET, - { expiresIn: "15m" }, - ); - // Use a URL fragment (#) instead of a query string (?) to pass the token. - // This is more secure as fragments are not sent to the server and are less likely to be logged. - // The frontend (auth-success.html) will need to be updated to parse the token from the URL fragment. - res.redirect( - `/auth-success.html#accessToken=${accessToken}&role=${user.role}`, - ); -}; - -const disableTwoFactor = async (req, res, next) => { - try { - const user = await User.findByPk(req.user.id); - if (!user) { - return res.status(404).json({ message: "User not found" }); - } - - // For enhanced security, you might require a password or 2FA token to disable. - // For simplicity here, we'll just disable it. - await user.update({ - twoFactorEnabled: false, - twoFactorSecret: null, // Clear the secret for security - }); - - res.json({ message: "2FA disabled successfully" }); - } catch (error) { - next(error); - } -}; - -module.exports = { - loginUser, - createUser, - me, - updateProfile, - refreshAccessToken, - logoutUser, - requestPasswordReset, - resetPassword, - generateTwoFactorSecret, - verifyTwoFactorSecret, - verifyLoginTwoFactor, - googleCallback, - disableTwoFactor, -}; diff --git a/server/controllers/etaController.js b/server/controllers/etaController.js deleted file mode 100644 index 717bf50..0000000 --- a/server/controllers/etaController.js +++ /dev/null @@ -1,31 +0,0 @@ -const { Stage, Vehicle } = require("../models"); -const { calculateEtasForStage } = require("../services/etaCalculationService"); -const { getLocations } = require("../services/locationCache"); - -const getStageEta = async (req, res, next) => { - const { stageId } = req.params; - - try { - // Find the stage by its primary key and include its associated vehicles - const stage = await Stage.findByPk(stageId, { - include: { - model: Vehicle, - through: { attributes: [] }, // Don't include the join table attributes - }, - }); - - if (!stage) { - return res.status(404).json({ message: "Stage not found" }); - } - - // Get all vehicle locations from the central cache - const allVehicleLocations = await getLocations(); - - const vehicleEtas = await calculateEtasForStage(stage, allVehicleLocations); - res.status(200).json({ stageName: stage.name, arrivals: vehicleEtas }); - } catch (error) { - next(error); - } -}; - -module.exports = { getStageEta }; diff --git a/server/controllers/feedbackController.js b/server/controllers/feedbackController.js deleted file mode 100644 index 8405f7e..0000000 --- a/server/controllers/feedbackController.js +++ /dev/null @@ -1,51 +0,0 @@ -const { Feedback, Vehicle } = require("../models"); -const { validationResult } = require("express-validator"); - -/** - * Allows a passenger to submit feedback for a specific vehicle. - */ -const submitFeedback = async (req, res, next) => { - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - - try { - const { vehicleId, rating, comment } = req.body; - const userId = req.user.id; - - // Check if vehicle exists - const vehicle = await Vehicle.findByPk(vehicleId); - if (!vehicle) { - return res.status(404).json({ message: "Vehicle not found." }); - } - - await Feedback.create({ - vehicleId, - userId, - rating, - comment, - }); - - res.status(201).json({ message: "Thank you for your feedback!" }); - } catch (error) { - next(error); - } -}; - -/** - * Fetches all vehicles so passengers can select one for feedback. - */ -const getAllVehiclesForFeedback = async (req, res, next) => { - try { - const vehicles = await Vehicle.findAll({ - attributes: ["id", "plateNumber"], - order: [["plateNumber", "ASC"]], - }); - res.status(200).json(vehicles); - } catch (error) { - next(error); - } -}; - -module.exports = { submitFeedback, getAllVehiclesForFeedback }; diff --git a/server/controllers/matAdminController.js b/server/controllers/matAdminController.js deleted file mode 100644 index 938f59b..0000000 --- a/server/controllers/matAdminController.js +++ /dev/null @@ -1,434 +0,0 @@ -const { - Vehicle, - DailyDistance, - DailyTrip, - Feedback, - Stage, // Import Stage model - User, -} = require("../models"); -const { Op, fn, col } = require("sequelize"); -const { validationResult } = require("express-validator"); -const sequelize = require("../config/database"); -const { getVehicleLocation } = require("../services/firebaseService"); -const { getLocations } = require("../services/locationCache"); -const { getLocationName } = require("../services/googleMapsService"); - -const createVehicle = async (req, res, next) => { - // ValidationResult check is done in matAdminRoutes.js - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(400).json({ errors: errors.array() }); - } - try { - const { carId, plateNumber } = req.body; - const ownerId = req.user.id; // Vehicle is owned by the logged-in MatAdmin - - const newVehicle = await Vehicle.create({ carId, plateNumber, ownerId }); - res.status(201).json(newVehicle); - } catch (error) { - next(error); - } -}; - -const getDashboardData = async (req, res, next) => { - try { - const ownerId = req.user.id; - // Fetch locations once before the loop to avoid N Redis/cache calls - const allVehicleLocations = await getLocations(); - - const vehicles = await Vehicle.findAll({ - where: { ownerId }, - order: [["plateNumber", "ASC"]], - include: { - attributes: ["name"], - through: { attributes: [] }, - }, - }); - - const vehicleDetails = await Promise.all( - vehicles.map(async (vehicle) => { - const locationData = allVehicleLocations.get(vehicle.id); - - let locationName = "Offline"; - let latitude = null; - let longitude = null; - if (locationData && locationData.latitude && locationData.longitude) { - locationName = await getLocationName(locationData); - latitude = locationData.latitude; - longitude = locationData.longitude; - } - - // Dynamically construct the route from associated stages - let route = "Not Assigned"; - if (vehicle.Stages && vehicle.Stages.length > 0) { - if (vehicle.Stages.length > 1) { - route = `${vehicle.Stages[0].name} - ${vehicle.Stages[vehicle.Stages.length - 1].name}`; - } else { - route = vehicle.Stages[0].name; - } - } - - return { - id: vehicle.id, - plateNumber: vehicle.plateNumber, - status: locationData ? "Online" : "Offline", - location: locationName, - latitude, - longitude, - route: route, - }; - }), - ); - res.status(200).json({ vehicles: vehicleDetails }); - } catch (error) { - next(error); - } -}; - -const getDashboardSummary = async (req, res, next) => { - try { - const ownerId = req.user.id; - const today = new Date().toISOString().slice(0, 10); - - const ownedVehicles = await Vehicle.findAll({ where: { ownerId } }); - const vehicleIds = ownedVehicles.map((v) => v.id); - - const totalVehicles = ownedVehicles.length; - - const totalDistanceResult = await DailyDistance.sum("distanceCovered", { - where: { vehicleId: { [Op.in]: vehicleIds }, date: today }, - }); - - const totalTripsResult = await DailyTrip.sum("tripCount", { - where: { vehicleId: { [Op.in]: vehicleIds }, date: today }, - }); - - res.status(200).json({ - totalVehicles, - totalDistanceToday: totalDistanceResult || 0, - totalTripsToday: totalTripsResult || 0, - }); - } catch (error) { - next(error); - } -}; - -// Helper to fetch latest feedback for a vehicle -const getLatestFeedbackForVehicle = async (vehicleId) => { - try { - const feedback = await Feedback.findAll({ - where: { vehicleId }, - include: [{ model: User, attributes: ["name"] }], - limit: 5, - order: [["createdAt", "DESC"]], - }); - return feedback; - } catch (error) { - console.error(`Error fetching feedback for vehicle ${vehicleId}:`, error); - return []; - } -}; - -const getVehicleChartData = async (req, res, next) => { - try { - const { vehicleId } = req.params; - const { period = "daily" } = req.query; // 'daily', 'weekly', 'monthly' - const ownerId = req.user.id; - - // Security check: Ensure the vehicle belongs to the logged-in MatAdmin - const vehicle = await Vehicle.findOne({ - where: { id: vehicleId, ownerId }, - }); - if (!vehicle) { - return res.status(404).json({ - message: "Vehicle not found or you do not have permission to view it.", - }); - } - - let distanceData, tripData; - const today = new Date(); - // Helper to format date to YYYY-MM-DD to avoid timezone issues with DATEONLY fields - const toYYYYMMDD = (date) => date.toISOString().slice(0, 10); - - switch (period) { - case "weekly": - const fourWeeksAgo = new Date(new Date().setDate(today.getDate() - 27)); // Go back 27 days to include today in the 4th week - // Use toYYYYMMDD to ensure correct date comparison - distanceData = await DailyDistance.findAll({ - where: { vehicleId, date: { [Op.gte]: fourWeeksAgo } }, - attributes: [ - [fn("YEARWEEK", col("date")), "period"], - [fn("SUM", col("distanceCovered")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - tripData = await DailyTrip.findAll({ - // Use toYYYYMMDD for tripData as well - where: { vehicleId, date: { [Op.gte]: fourWeeksAgo } }, - attributes: [ - [fn("YEARWEEK", col("date")), "period"], - [fn("SUM", col("tripCount")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - break; - - case "monthly": - const twelveMonthsAgo = new Date( - new Date().setMonth(today.getMonth() - 11), // Go back 11 months to include the current month - ); - distanceData = await DailyDistance.findAll({ - // Use toYYYYMMDD to ensure correct date comparison - where: { vehicleId, date: { [Op.gte]: toYYYYMMDD(twelveMonthsAgo) } }, - attributes: [ - [fn("DATE_FORMAT", col("date"), "%Y-%m"), "period"], - [fn("SUM", col("distanceCovered")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - tripData = await DailyTrip.findAll({ - // Use toYYYYMMDD for tripData as well - where: { vehicleId, date: { [Op.gte]: twelveMonthsAgo } }, - attributes: [ - [fn("DATE_FORMAT", col("date"), "%Y-%m"), "period"], - [fn("SUM", col("tripCount")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - break; - - case "daily": - default: - const sevenDaysAgo = new Date(new Date().setDate(today.getDate() - 6)); // Go back 6 days to get 7 total days - // Use toYYYYMMDD to ensure correct date comparison - distanceData = await DailyDistance.findAll({ - where: { vehicleId, date: { [Op.gte]: sevenDaysAgo } }, - attributes: [ - ["date", "period"], - ["distanceCovered", "total"], - ], - order: [["date", "ASC"]], - }); - tripData = await DailyTrip.findAll({ - // Use toYYYYMMDD for tripData as well - where: { vehicleId, date: { [Op.gte]: sevenDaysAgo } }, - attributes: [ - ["date", "period"], - ["tripCount", "total"], - ], - order: [["date", "ASC"]], - }); - break; - } - - // Convert raw data to a format Chart.js can easily use - const formatData = (data, isDistance = false) => { - return data.map((d) => ({ - period: d.get("period"), - total: isDistance - ? parseFloat((d.get("total") / 1000).toFixed(2)) - : parseInt(d.get("total"), 10), - })); - }; - - res.status(200).json({ - distance: formatData(distanceData, true), - trips: formatData(tripData, false), - }); - } catch (error) { - next(error); - } -}; - -const getFleetAggregateData = async (req, res, next) => { - try { - const { period = "daily" } = req.query; - const ownerId = req.user.id; - - const ownedVehicles = await Vehicle.findAll({ - where: { ownerId }, - attributes: ["id"], - }); - const vehicleIds = ownedVehicles.map((v) => v.id); - - if (vehicleIds.length === 0) { - return res.status(200).json({ distance: [], trips: [] }); - } - - let distanceData, tripData; - const today = new Date(); - const whereClause = { vehicleId: { [Op.in]: vehicleIds } }; - - switch (period) { - case "weekly": - whereClause.date = { - [Op.gte]: new Date(new Date().setDate(today.getDate() - 28)), - }; - distanceData = await DailyDistance.findAll({ - where: whereClause, - attributes: [ - [fn("YEARWEEK", col("date")), "period"], - [fn("SUM", col("distanceCovered")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - tripData = await DailyTrip.findAll({ - where: whereClause, - attributes: [ - [fn("YEARWEEK", col("date")), "period"], - [fn("SUM", col("tripCount")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - break; - - case "monthly": - const twelveMonthsAgo = new Date( - new Date().setMonth(today.getMonth() - 11), - ); - whereClause.date = { [Op.gte]: toYYYYMMDD(twelveMonthsAgo) }; - distanceData = await DailyDistance.findAll({ - where: whereClause, - attributes: [ - [fn("DATE_FORMAT", col("date"), "%Y-%m"), "period"], - [fn("SUM", col("distanceCovered")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - tripData = await DailyTrip.findAll({ - where: whereClause, - attributes: [ - [fn("DATE_FORMAT", col("date"), "%Y-%m"), "period"], - [fn("SUM", col("tripCount")), "total"], - ], - group: ["period"], - order: [[col("period"), "ASC"]], - }); - break; - - case "daily": - default: // Use toYYYYMMDD to ensure correct date comparison - const sevenDaysAgo = new Date(new Date().setDate(today.getDate() - 6)); - whereClause.date = { [Op.gte]: toYYYYMMDD(sevenDaysAgo) }; - distanceData = await DailyDistance.findAll({ - where: whereClause, - attributes: [ - ["date", "period"], - [fn("SUM", col("distanceCovered")), "total"], - ], - group: ["date"], - order: [["date", "ASC"]], - }); - tripData = await DailyTrip.findAll({ - where: whereClause, - attributes: [ - ["date", "period"], - [fn("SUM", col("tripCount")), "total"], - ], - group: ["date"], - order: [["date", "ASC"]], - }); - break; - } - - const formatData = (data, isDistance = false) => { - return data.map((d) => ({ - period: d.get("period"), - total: isDistance - ? parseFloat((d.get("total") / 1000).toFixed(2)) - : parseInt(d.get("total") || 0, 10), - })); - }; - - const formattedDistance = formatData(distanceData, true); - const formattedTrips = formatData(tripData); - - // Combine the data into a single structure, ensuring all periods are covered - const combinedData = {}; - const allPeriods = new Set([ - ...formattedDistance.map((d) => d.period), - ...formattedTrips.map((t) => t.period), - ]); - - allPeriods.forEach((period) => { - const dist = formattedDistance.find((d) => d.period === period); - const trip = formattedTrips.find((t) => t.period === period); - combinedData[period] = { - distance: dist ? dist.total : 0, - trips: trip ? trip.total : 0, - }; - }); - - res.status(200).json(combinedData); - } catch (error) { - next(error); - } -}; - -const getMyVehicles = async (req, res, next) => { - try { - const vehicles = await Vehicle.findAll({ - where: { ownerId: req.user.id }, - order: [["plateNumber", "ASC"]], - }); - res.status(200).json(vehicles); - } catch (error) { - next(error); - } -}; - -const getVehicleStats = async (req, res, next) => { - try { - const ownerId = req.user.id; - const today = new Date().toISOString().slice(0, 10); - - // Fetch vehicles owned by the MatAdmin - const vehicles = await Vehicle.findAll({ where: { ownerId } }); - - // For each vehicle, fetch its stats and latest feedback separately - const vehicleStats = await Promise.all( - vehicles.map(async (vehicle) => { - const dailyDistance = await DailyDistance.findOne({ - where: { vehicleId: vehicle.id, date: today }, - }); - const dailyTrip = await DailyTrip.findOne({ - where: { vehicleId: vehicle.id, date: today }, - }); - const latestFeedback = await Feedback.findAll({ - where: { vehicleId: vehicle.id }, - include: [{ model: User, attributes: ["name"] }], - limit: 5, - order: [["createdAt", "DESC"]], - }); - - return { - ...vehicle.toJSON(), // Convert Sequelize instance to plain object - DailyDistances: dailyDistance ? [dailyDistance.toJSON()] : [], - DailyTrips: dailyTrip ? [dailyTrip.toJSON()] : [], - Feedbacks: latestFeedback.map((f) => f.toJSON()), - }; - }), - ); - - res.status(200).json(vehicleStats); - } catch (error) { - next(error); - } -}; - -module.exports = { - createVehicle, - getDashboardData, - getMyVehicles, - getDashboardSummary, - getVehicleStats, - getVehicleChartData, - getFleetAggregateData, -}; diff --git a/server/controllers/stageController.js b/server/controllers/stageController.js deleted file mode 100644 index 4408602..0000000 --- a/server/controllers/stageController.js +++ /dev/null @@ -1,92 +0,0 @@ -const { Stage, User } = require("../models"); - -/** - * Fetches all stages from the database. - */ -const getAllStages = async (req, res, next) => { - try { - const stages = await Stage.findAll({ - order: [["name", "ASC"]], - }); - res.status(200).json(stages); - } catch (error) { - next(error); - } -}; - -const getSubscriptionStatus = async (req, res, next) => { - try { - const stageId = req.params.stageId; - const userId = req.user.id; - - const user = await User.findByPk(userId); - if (!user) return res.status(404).json({ message: "User not found" }); - - const isSubscribed = await user.hasAlertSubscription(stageId); - res.status(200).json({ isSubscribed }); - } catch (error) { - next(error); - } -}; - -const subscribeToStage = async (req, res, next) => { - try { - const stageId = req.params.stageId; - const userId = req.user.id; - - const user = await User.findByPk(userId); - if (!user) return res.status(404).json({ message: "User not found" }); - - if (!user.phoneNumber) { - return res - .status(400) - .json({ - message: - "Please add a phone number to your profile to subscribe for alerts.", - }); - } - - const stage = await Stage.findByPk(stageId); - if (!stage) return res.status(404).json({ message: "Stage not found" }); - - await user.addAlertSubscription(stage); - - res - .status(200) - .json({ - message: `Successfully subscribed to alerts for ${stage.name}.`, - }); - } catch (error) { - next(error); - } -}; - -const unsubscribeFromStage = async (req, res, next) => { - try { - const stageId = req.params.stageId; - const userId = req.user.id; - - const user = await User.findByPk(userId); - if (!user) return res.status(404).json({ message: "User not found" }); - - const stage = await Stage.findByPk(stageId); - if (!stage) return res.status(404).json({ message: "Stage not found" }); - - await user.removeAlertSubscription(stage); - - res - .status(200) - .json({ - message: `Successfully unsubscribed from alerts for ${stage.name}.`, - }); - } catch (error) { - next(error); - } -}; - -module.exports = { - getAllStages, - getSubscriptionStatus, - subscribeToStage, - unsubscribeFromStage, -}; diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js deleted file mode 100644 index 0bf9321..0000000 --- a/server/middleware/authMiddleware.js +++ /dev/null @@ -1,50 +0,0 @@ -const jwt = require("jsonwebtoken"); -const { User } = require("../models"); - -const authMiddleware = (role) => { - return async (req, res, next) => { - const authHeader = req.header("Authorization"); - - if (!authHeader) { - return res - .status(401) - .json({ message: "Access denied. No token provided." }); - } - - const tokenParts = authHeader.split(" "); - if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer") { - return res.status(400).json({ message: "Invalid token format" }); - } - - const token = tokenParts[1]; - - try { - const verified = jwt.verify(token, process.env.JWT_SECRET); - req.user = verified; - - // Check claims directly from the token to avoid DB lookups on every request. - if (verified.disabled) { - return res.status(403).json({ message: "Account disabled" }); - } - - // Check if the token was issued before the user's tokens were last invalidated. - if (verified.iat * 1000 < verified.tokensValidFrom) { - // tokensValidFrom is a timestamp in milliseconds - return res.status(401).json({ message: "Token has been invalidated" }); - } - - if (role && req.user.role !== role) { - return res - .status(403) - .json({ message: "Forbidden: Insufficient permissions" }); - } - - next(); - } catch (error) { - console.log("Error in token verification:", error); - res.status(401).json({ message: "Invalid token" }); - } - }; -}; - -module.exports = authMiddleware; diff --git a/server/middleware/errorHandler.js b/server/middleware/errorHandler.js deleted file mode 100644 index 19eba48..0000000 --- a/server/middleware/errorHandler.js +++ /dev/null @@ -1,31 +0,0 @@ -const { ValidationError, UniqueConstraintError } = require("sequelize"); - -const errorHandler = (err, req, res, next) => { - // Log the full error for debugging purposes on the server - console.error(err); - - // Handle malformed JSON request bodies - if (err instanceof SyntaxError && err.status === 400 && "body" in err) { - return res.status(400).json({ message: "Malformed JSON in request body" }); - } - - // Handle Sequelize validation errors specifically - if (err instanceof ValidationError || err instanceof UniqueConstraintError) { - const errors = err.errors.map((e) => ({ - message: e.message, - field: e.path, - })); - return res.status(400).json({ - message: "Validation failed", - errors, - }); - } - - // Send a generic, user-friendly message to the client - // to avoid leaking implementation details. - res - .status(500) - .json({ message: "An unexpected internal server error occurred." }); -}; - -module.exports = errorHandler; diff --git a/server/middleware/rateLimiters.js b/server/middleware/rateLimiters.js deleted file mode 100644 index bb5a4b6..0000000 --- a/server/middleware/rateLimiters.js +++ /dev/null @@ -1,82 +0,0 @@ -const rateLimit = require("express-rate-limit"); -const { redis } = require("../services/locationCache"); - -let store; - -// Only try to use RedisStore if Redis is configured and the package is installed. -if (redis) { - try { - const { RedisStore } = require("rate-limit-redis"); - store = new RedisStore({ - // @ts-ignore - sendCommand: (...args) => redis.call(...args), - }); - console.log("✅ Rate limiter connected to Redis."); - } catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - console.warn( - "⚠️ `rate-limit-redis` package not found. Rate limiting will use in-memory store.", - ); - } else { - throw e; - } - } -} - -// General limiter for authentication actions -const authLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per windowMs - store, - standardHeaders: true, - legacyHeaders: false, - message: "Too many requests from this IP, please try again after 15 minutes", -}); - -// A stricter limiter for potentially costly operations like SMS -const subscriptionLimiter = rateLimit({ - windowMs: 10 * 60 * 1000, // 10 minutes - max: 30, // Limit each IP to 30 subscription changes per 10 minutes - store, - standardHeaders: true, - legacyHeaders: false, - message: "Too many subscription requests. Please try again later.", -}); - -// General limiter for API endpoints with authorization -const apiLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per windowMs - store, - standardHeaders: true, - legacyHeaders: false, - message: "Too many requests from this IP, please try again after 15 minutes", -}); - -// Stricter limiter for admin operations -const adminLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 50, // Limit admin operations to 50 requests per 15 minutes - store, - standardHeaders: true, - legacyHeaders: false, - message: "Too many admin requests. Please try again later.", -}); - -// Stricter limiter for dashboard/data retrieval -const dashboardLimiter = rateLimit({ - windowMs: 1 * 60 * 1000, // 1 minute - max: 30, // Limit to 30 requests per minute - store, - standardHeaders: true, - legacyHeaders: false, - message: "Too many dashboard requests. Please try again later.", -}); - -module.exports = { - authLimiter, - subscriptionLimiter, - apiLimiter, - adminLimiter, - dashboardLimiter, -}; diff --git a/server/models/DailyDistance.js b/server/models/DailyDistance.js deleted file mode 100644 index 66005ce..0000000 --- a/server/models/DailyDistance.js +++ /dev/null @@ -1,41 +0,0 @@ -const { DataTypes } = require("sequelize"); -const sequelize = require("../config/database"); - -const DailyDistance = sequelize.define( - "DailyDistance", - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - vehicleId: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: "Vehicles", - key: "id", - }, - }, - date: { - type: DataTypes.DATEONLY, // Stores date as 'YYYY-MM-DD' - allowNull: false, - }, - distanceCovered: { - type: DataTypes.FLOAT, // Distance in meters - allowNull: false, - defaultValue: 0, - }, - }, - { - indexes: [ - { - unique: true, - fields: ["vehicleId", "date"], - }, - { fields: ["vehicleId"] }, - ], - }, -); - -module.exports = DailyDistance; diff --git a/server/models/DailyTrip.js b/server/models/DailyTrip.js deleted file mode 100644 index 95a0240..0000000 --- a/server/models/DailyTrip.js +++ /dev/null @@ -1,41 +0,0 @@ -const { DataTypes } = require("sequelize"); -const sequelize = require("../config/database"); - -const DailyTrip = sequelize.define( - "DailyTrip", - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - vehicleId: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: "Vehicles", - key: "id", - }, - }, - date: { - type: DataTypes.DATEONLY, // Stores date as 'YYYY-MM-DD' - allowNull: false, - }, - tripCount: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - }, - }, - { - indexes: [ - { - unique: true, - fields: ["vehicleId", "date"], - }, - { fields: ["vehicleId"] }, - ], - }, -); - -module.exports = DailyTrip; diff --git a/server/models/Feedback.js b/server/models/Feedback.js deleted file mode 100644 index fb4502d..0000000 --- a/server/models/Feedback.js +++ /dev/null @@ -1,38 +0,0 @@ -const { DataTypes } = require("sequelize"); -const sequelize = require("../config/database"); - -const Feedback = sequelize.define( - "Feedback", - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - vehicleId: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "Vehicles", key: "id" }, - }, - userId: { - type: DataTypes.INTEGER, - allowNull: false, - references: { model: "Users", key: "id" }, - }, - rating: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { min: 1, max: 5 }, - }, - comment: { - type: DataTypes.TEXT, - allowNull: true, - }, - }, - { - timestamps: true, - indexes: [{ fields: ["vehicleId"] }, { fields: ["userId"] }], - }, -); - -module.exports = Feedback; diff --git a/server/models/Stage.js b/server/models/Stage.js deleted file mode 100644 index a053352..0000000 --- a/server/models/Stage.js +++ /dev/null @@ -1,22 +0,0 @@ -const { DataTypes } = require("sequelize"); -const sequelize = require("../config/database"); - -const Stage = sequelize.define( - "Stage", - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - name: { - type: DataTypes.STRING, - allowNull: false, - unique: true, - }, - latitude: { type: DataTypes.DOUBLE, allowNull: false }, - longitude: { type: DataTypes.DOUBLE, allowNull: false }, - } -); - -module.exports = Stage; \ No newline at end of file diff --git a/server/models/User.js b/server/models/User.js deleted file mode 100644 index 5b139dd..0000000 --- a/server/models/User.js +++ /dev/null @@ -1,67 +0,0 @@ -const { DataTypes } = require("sequelize"); -const sequelize = require("../config/database"); - -const User = sequelize.define( - "User", - { - id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, - name: { type: DataTypes.STRING, allowNull: false }, - email: { type: DataTypes.STRING, allowNull: false }, - password: { - type: DataTypes.STRING, - allowNull: true, // Allow null for Google-based signups - }, - role: { - type: DataTypes.ENUM("Passenger", "Admin", "MatAdmin"), - allowNull: false, - }, - googleId: { - type: DataTypes.STRING, - allowNull: true, - }, - phoneNumber: { - type: DataTypes.STRING, - allowNull: true, - }, - profilePictureUrl: { - type: DataTypes.STRING, - allowNull: true, - }, - twoFactorSecret: { - type: DataTypes.STRING, - allowNull: true, - }, - twoFactorEnabled: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false, - }, - disabled: { - type: DataTypes.BOOLEAN, - defaultValue: false, - allowNull: false, - }, - tokensValidFrom: { - type: DataTypes.DATE, - allowNull: false, - defaultValue: DataTypes.NOW, // Default to current timestamp - }, - }, - { - timestamps: true, - indexes: [ - { - unique: true, - fields: ["email"], - name: "idx_users_email", - }, - { - unique: true, - fields: ["googleId"], - name: "idx_users_googleid", - }, - ], - }, -); - -module.exports = User; diff --git a/server/models/Vehicle.js b/server/models/Vehicle.js deleted file mode 100644 index bfef40d..0000000 --- a/server/models/Vehicle.js +++ /dev/null @@ -1,59 +0,0 @@ -const { DataTypes } = require("sequelize"); -const sequelize = require("../config/database"); - -const Vehicle = sequelize.define( - "Vehicle", - { - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, - }, - carId: { - type: DataTypes.STRING, - allowNull: false, - }, - plateNumber: { - type: DataTypes.STRING, - allowNull: false, - }, - ownerId: { - type: DataTypes.INTEGER, - allowNull: true, // Can be null if created by Admin and not yet assigned - references: { - model: "Users", // 'Users' is the table name - key: "id", - }, - }, - lastLatitude: { - type: DataTypes.DOUBLE, - allowNull: true, - }, - lastLongitude: { - type: DataTypes.DOUBLE, - allowNull: true, - }, - isAtStage: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false, - }, - }, - { - paranoid: true, // Enable soft deletes - indexes: [ - // Create a unique index on carId - { unique: true, fields: ["carId"] }, - // Create a unique index on plateNumber - { unique: true, fields: ["plateNumber"] }, - // Add an index on the foreign key for performance - { fields: ["ownerId"] }, - ], - }, -); - -// Note: The above is the standard way to define indexes. If you still face issues, -// you could try a composite unique index on both fields if that fits your logic: -// { unique: true, fields: ['carId', 'plateNumber'] } - -module.exports = Vehicle; diff --git a/server/models/index.js b/server/models/index.js deleted file mode 100644 index f75971b..0000000 --- a/server/models/index.js +++ /dev/null @@ -1,78 +0,0 @@ -const sequelize = require("../config/database"); -const User = require("./User"); -const Stage = require("./Stage"); -const Vehicle = require("./Vehicle"); -const DailyDistance = require("./DailyDistance"); -const DailyTrip = require("./DailyTrip"); -const Feedback = require("./Feedback"); - -// A Stage can have many vehicles, and a Vehicle can belong to many stages (in different routes) -// This creates a join table `StageVehicles` -Stage.belongsToMany(Vehicle, { through: "StageVehicles" }); -Vehicle.belongsToMany(Stage, { through: "StageVehicles" }); - -// A User can subscribe to many stages for alerts, and a Stage can have many subscribers. -// This creates a join table `UserStageSubscriptions` -User.belongsToMany(Stage, { - through: "UserStageSubscriptions", - as: "AlertSubscriptions", -}); -Stage.belongsToMany(User, { - through: "UserStageSubscriptions", - as: "Subscribers", -}); - -// A User (MatAdmin) can own many Vehicles. -User.hasMany(Vehicle, { - foreignKey: "ownerId", - as: "OwnedVehicles", - onDelete: "SET NULL", -}); -Vehicle.belongsTo(User, { foreignKey: "ownerId", as: "Owner" }); - -// A Vehicle can have many DailyDistance records. -Vehicle.hasMany(DailyDistance, { - foreignKey: "vehicleId", - onDelete: "CASCADE", -}); -DailyDistance.belongsTo(Vehicle, { foreignKey: "vehicleId" }); - -// A Vehicle can have many DailyTrip records. -Vehicle.hasMany(DailyTrip, { foreignKey: "vehicleId", onDelete: "CASCADE" }); -DailyTrip.belongsTo(Vehicle, { foreignKey: "vehicleId" }); - -// A Vehicle can have many Feedback entries. A User can give many Feedbacks. -Vehicle.hasMany(Feedback, { foreignKey: "vehicleId", onDelete: "CASCADE" }); -Feedback.belongsTo(Vehicle, { foreignKey: "vehicleId" }); -User.hasMany(Feedback, { foreignKey: "userId", onDelete: "CASCADE" }); -Feedback.belongsTo(User, { foreignKey: "userId" }); - -const syncDB = async () => { - try { - // In a production environment, you should use migrations instead of `sync`. - // `sync({ alter: true })` is dangerous and can lead to data loss. - // For development, `sync()` is acceptable. For production, this should be disabled - // and a migration tool like Sequelize-CLI or Umzug should be used instead. - if (process.env.NODE_ENV !== "production") { - await sequelize.sync(); - console.log( - "✅ Database synchronized (Note: Use migrations for production).", - ); - } - } catch (error) { - console.error("❌ Error synchronizing the database:", error); - // Re-throw the error to be caught by the server startup logic. - throw error; - } -}; - -module.exports = { - sequelize, - syncDB, - User, - Stage, - Vehicle, - DailyDistance, - DailyTrip, - Feedback, -}; diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js deleted file mode 100644 index 3ca3833..0000000 --- a/server/routes/adminRoutes.js +++ /dev/null @@ -1,91 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { body, param } = require("express-validator"); -const authMiddleware = require("../middleware/authMiddleware"); -const { adminLimiter, dashboardLimiter } = require("../middleware/rateLimiters"); -const { - getAllStages, - createStage, - getAllVehicles, - createVehicle, - deleteVehicle, - assignVehicleToStage, - removeVehicleFromStage, - getDashboardData, - getAllFeedback, - getAllUsers, - updateUserByAdmin, - deleteUserByAdmin, - createUserByAdmin, - getAllMatAdmins, -} = require("../controllers/adminController"); - -// All routes in this file are for Admins only -router.use(adminLimiter); -router.use(authMiddleware("Admin")); - -// Dashboard data -router.get("/dashboard-data", dashboardLimiter, getDashboardData); - -// User Management -router.get("/users", getAllUsers); -router.post( - "/users", - body("name", "Name is required").notEmpty().trim().escape(), - body("email", "Please provide a valid email").isEmail().normalizeEmail(), - body("password", "Password must be at least 6 characters long").isLength({ - min: 6, - }), - body("role") - .isIn(["Admin", "MatAdmin"]) - .withMessage("Invalid role specified"), - createUserByAdmin, -); -router.put( - "/users/:id", - body("disabled", "Disabled status must be a boolean").isBoolean(), - updateUserByAdmin, -); -router.delete("/users/:id", deleteUserByAdmin); - -router.get("/feedback", getAllFeedback); -router.get("/mat-admins", getAllMatAdmins); -router.get("/stages", getAllStages); -router.post( - "/stages", - body("name", "Stage name is required").notEmpty().trim(), - body("latitude", "A valid latitude is required").isFloat({ - min: -90, - max: 90, - }), - body("longitude", "A valid longitude is required").isFloat({ - min: -180, - max: 180, - }), - createStage, -); -router.get("/vehicles", getAllVehicles); -router.post( - "/vehicles", - body("carId", "Car ID is required").notEmpty().trim(), - body("plateNumber", "Plate number is required").notEmpty().trim(), - body("ownerId", "Owner ID must be an integer") - .optional({ checkFalsy: true }) - .isInt(), - createVehicle, -); -router.delete("/vehicles/:id", deleteVehicle); -router.post( - "/stages/assign-vehicle", - body("stageId", "Stage ID must be a positive integer").isInt({ min: 1 }), - body("vehicleId", "Vehicle ID must be a positive integer").isInt({ min: 1 }), - assignVehicleToStage, -); -router.delete( - "/stages/:stageId/vehicles/:vehicleId", - param("stageId", "Stage ID must be a positive integer").isInt({ min: 1 }), - param("vehicleId", "Vehicle ID must be a positive integer").isInt({ min: 1 }), - removeVehicleFromStage, -); - -module.exports = router; diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js deleted file mode 100644 index 72804e4..0000000 --- a/server/routes/authRoutes.js +++ /dev/null @@ -1,121 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { body, param } = require("express-validator"); -const { - loginUser, - createUser, - updateProfile, - me, - refreshAccessToken, - logoutUser, - requestPasswordReset, - resetPassword, - generateTwoFactorSecret, - verifyTwoFactorSecret, - verifyLoginTwoFactor, - disableTwoFactor, -} = require("../controllers/authController"); -const authMiddleware = require("../middleware/authMiddleware"); -const passport = require("passport"); - -// Login Route -router.post( - "/login", - body("email", "Please provide a valid email").isEmail().normalizeEmail(), - body("password", "Password cannot be empty").notEmpty(), - loginUser, -); - -// Create User Route (For Initial Setup) -router.post( - "/register", - body("name", "Name is required").notEmpty().trim().escape(), - body("email", "Please provide a valid email").isEmail().normalizeEmail(), - body("password", "Password must be at least 6 characters long").isLength({ - min: 6, - }), - createUser, -); - -// Check login status -router.get("/me/admin", authMiddleware("Admin"), me); -router.get("/me/passenger", authMiddleware("Passenger"), me); -router.get("/me/mat-admin", authMiddleware("MatAdmin"), me); - -// Update user profile -router.put( - "/me", - authMiddleware(), - body("name").optional().trim().escape(), - body("phoneNumber") - .optional({ checkFalsy: true }) - .isString() - .withMessage("Invalid phone number"), - updateProfile, -); - -// Refresh Token -router.post("/refresh-token", refreshAccessToken); - -// Log Out -router.post("/logout", authMiddleware(), logoutUser); - -//Route to request password reset\ -router.post("/requestPasswordReset", requestPasswordReset); - -// Route to reset password -router.post( - "/resetpassword/:id/:token", - param("id", "Invalid user ID").isInt({ min: 1 }), - param("token", "Invalid token format").isJWT(), - body("password", "Password must be at least 6 characters long").isLength({ - min: 6, - }), - resetPassword, -); - -// --- Two-Factor Authentication Routes --- -router.post("/2fa/setup", authMiddleware(), generateTwoFactorSecret); -router.post("/2fa/verify", authMiddleware(), verifyTwoFactorSecret); -router.post("/2fa/disable", authMiddleware(), disableTwoFactor); -router.post("/2fa/login", verifyLoginTwoFactor); - -// --- Google OAuth Routes --- - -// 1. Route to start the Google sign-in flow -router.get("/google", (req, res, next) => { - passport.authenticate("google", { scope: ["profile", "email"] })( - req, - res, - next, - ); -}); - -// 2. Callback route that Google redirects to after authentication -router.get( - "/google/callback", - (req, res, next) => { - passport.authenticate( - "google", - { failureRedirect: "/login.html?error=auth_failed", session: false }, - (err, user, info) => { - if (err) { - console.error(" Passport authenticate error:", err); - return next(err); - } - if (!user) { - console.log( - " User not found or authentication failed. Info:", - info, - ); - return res.redirect("/login.html?error=auth_failed"); - } - req.user = user; // Manually attach user to request - next(); - }, - )(req, res, next); - }, - require("../controllers/authController").googleCallback, -); - -module.exports = router; diff --git a/server/routes/etaRoutes.js b/server/routes/etaRoutes.js deleted file mode 100644 index 69f6193..0000000 --- a/server/routes/etaRoutes.js +++ /dev/null @@ -1,9 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { getStageEta } = require("../controllers/etaController"); -const authMiddleware = require("../middleware/authMiddleware"); -const { apiLimiter } = require("../middleware/rateLimiters"); - -router.get('/:stageId', apiLimiter, authMiddleware(), getStageEta); - -module.exports = router; diff --git a/server/routes/feedbackRoutes.js b/server/routes/feedbackRoutes.js deleted file mode 100644 index 663cd95..0000000 --- a/server/routes/feedbackRoutes.js +++ /dev/null @@ -1,24 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { body } = require("express-validator"); -const authMiddleware = require("../middleware/authMiddleware"); -const { apiLimiter } = require("../middleware/rateLimiters"); -const { - submitFeedback, - getAllVehiclesForFeedback, -} = require("../controllers/feedbackController"); - -// All routes in this file are for authenticated passengers -router.use(apiLimiter); -router.use(authMiddleware("Passenger")); - -router.get("/vehicles", getAllVehiclesForFeedback); -router.post( - "/", - body("vehicleId", "Vehicle ID is required").isInt(), - body("rating", "Rating must be between 1 and 5").isInt({ min: 1, max: 5 }), - body("comment").optional().trim().escape(), - submitFeedback, -); - -module.exports = router; diff --git a/server/routes/matAdminRoutes.js b/server/routes/matAdminRoutes.js deleted file mode 100644 index d244a67..0000000 --- a/server/routes/matAdminRoutes.js +++ /dev/null @@ -1,33 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { body } = require("express-validator"); -const authMiddleware = require("../middleware/authMiddleware"); -const { adminLimiter, dashboardLimiter } = require("../middleware/rateLimiters"); -const { - getDashboardData, - createVehicle, - getMyVehicles, - getDashboardSummary, - getVehicleStats, - getVehicleChartData, - getFleetAggregateData, -} = require("../controllers/matAdminController"); - -// All routes in this file are for MatAdmins only -router.use(adminLimiter); -router.use(authMiddleware("MatAdmin")); - -router.get("/dashboard-summary", dashboardLimiter, getDashboardSummary); -router.get("/dashboard-data", dashboardLimiter, getDashboardData); // For live vehicle status -router.get("/vehicle-stats", getVehicleStats); -router.get("/fleet-aggregate-data", getFleetAggregateData); -router.get("/vehicles/:vehicleId/chart-data", getVehicleChartData); -router.post( - "/vehicles", - body("carId", "Car ID is required").notEmpty().trim(), - body("plateNumber", "Plate number is required").notEmpty().trim(), - createVehicle, -); -router.get("/vehicles", getMyVehicles); - -module.exports = router; diff --git a/server/routes/stageRoutes.js b/server/routes/stageRoutes.js deleted file mode 100644 index 0c90942..0000000 --- a/server/routes/stageRoutes.js +++ /dev/null @@ -1,30 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const { - getAllStages, - getSubscriptionStatus, - subscribeToStage, - unsubscribeFromStage, -} = require("../controllers/stageController"); -const authMiddleware = require("../middleware/authMiddleware"); -const { subscriptionLimiter, apiLimiter } = require("../middleware/rateLimiters"); - -// This route is for any authenticated user (Passenger or Admin) -router.get("/", authMiddleware(), apiLimiter, getAllStages); - -// Routes for managing SMS alert subscriptions -router.get("/:stageId/subscription", authMiddleware(), apiLimiter, getSubscriptionStatus); -router.post( - "/:stageId/subscribe", - authMiddleware(), - subscriptionLimiter, - subscribeToStage, -); -router.post( - "/:stageId/unsubscribe", - authMiddleware(), - subscriptionLimiter, - unsubscribeFromStage, -); - -module.exports = router; diff --git a/server/server.js b/server/server.js deleted file mode 100644 index 52a600b..0000000 --- a/server/server.js +++ /dev/null @@ -1,82 +0,0 @@ -require("dotenv").config(); -const path = require("path"); -const express = require("express"); -const cookieParser = require("cookie-parser"); -const { syncDB } = require("./models"); -const app = express(); -const cors = require("cors"); -const helmet = require("helmet"); -const passport = require("passport"); -const csrf = require("csurf"); - -const isPkg = typeof process.pkg !== "undefined"; -const basePath = isPkg ? path.dirname(process.execPath) : __dirname; -app.use(express.static(path.join(basePath, "../public"))); - -// Middleware -// Trust the first proxy in front of the app (e.g., Nginx, Heroku, etc.) -// This is crucial for rate limiting to work correctly behind a proxy. -app.set("trust proxy", 1); -app.use(helmet()); // Adds various security headers -app.use( - cors({ - origin: process.env.CORS_ORIGIN || "http://localhost:5000", // Restrict to frontend origin - credentials: true, // Allow cookies to be sent - }), -); -app.use(cookieParser()); -app.use(express.json()); - -// CSRF protection middleware -const csrfProtection = csrf({ cookie: true }); -app.use(csrfProtection); - -app.use(passport.initialize()); - -// Import rate limiters -const { authLimiter } = require("./middleware/rateLimiters"); - -// This should be after dotenv config and passport.initialize -require("./config/passport"); // Import the passport configuration - -// Routes -app.use("/api/auth", authLimiter, require("./routes/authRoutes")); -app.use("/api/eta", require("./routes/etaRoutes")); // ETA routes are already authenticated -app.use("/api/stages", require("./routes/stageRoutes")); // Rate limiting is applied within the route file -app.use("/api/admin", require("./routes/adminRoutes")); -app.use("/api/feedback", require("./routes/feedbackRoutes")); -app.use("/api/mat-admin", require("./routes/matAdminRoutes")); - -// --- Background ETA Update Service --- -const { startEtaUpdateCycle } = require("./services/cronService"); -const UPDATE_INTERVAL_SECONDS = 30; - -// --- Centralized Error Handler --- -const errorHandler = require("./middleware/errorHandler"); -app.use(errorHandler); - -// --- Start Server Function --- -const startServer = async () => { - try { - // 1. Connect to and sync the database. - await syncDB(); - - // 2. If DB sync is successful, start the server. - const PORT = process.env.PORT || 5000; - app.listen(PORT, () => { - console.log(`🚀 Server running on port ${PORT}`); - console.log("Open front end at http://localhost:5000/"); - - // 3. Start the background ETA update cycle using node-cron. - startEtaUpdateCycle(UPDATE_INTERVAL_SECONDS); - }); - } catch (error) { - console.error( - "❌ Failed to start server due to database connection issues. Please check your .env file and ensure your database is running.", - ); - process.exit(1); // Exit with a failure code. - } -}; - -// --- Run the application --- -startServer(); diff --git a/server/services/cronService.js b/server/services/cronService.js deleted file mode 100644 index 313a8e6..0000000 --- a/server/services/cronService.js +++ /dev/null @@ -1,208 +0,0 @@ -const cron = require("node-cron"); -const { sequelize, Stage, Vehicle, User } = require("../models"); -const { calculateEtasForStage } = require("./etaCalculationService"); -const { sendSms } = require("./smsService"); -const { - updateStageEtas, - getAllVehicleLocations, -} = require("./firebaseService"); -const { setLocations } = require("./locationCache"); -const { updateVehicleDistance } = require("./distanceTrackingService"); - -let isCycleRunning = false; - -const runEtaUpdateCycle = async () => { - if (isCycleRunning) { - console.log( - "ETA update cycle is already running. Skipping this execution.", - ); - return; - } - isCycleRunning = true; - console.log("Running ETA update cycle..."); - try { - // Outer try-catch for fetching stages - const stages = await Stage.findAll({ - include: [ - // Eager load all necessary data at the start of the cycle - { model: Vehicle, through: { attributes: [] }, required: false }, - { model: User, as: "Subscribers", attributes: ["id", "phoneNumber"] }, - ], - }); - - // --- 1. Read Phase: Get all vehicle data at once --- - const allVehicles = await Vehicle.findAll(); - const allFirebaseLocations = await getAllVehicleLocations(); // Single call to Firebase - const vehicleLocations = new Map(); - - if (allFirebaseLocations) { - for (const vehicle of allVehicles) { - const location = allFirebaseLocations[vehicle.carId]; - if (location) { - vehicleLocations.set(vehicle.id, location); - } - } - } - - // Update the shared cache with the newly fetched locations - setLocations(vehicleLocations); - - // This map will track the final calculated status for each vehicle across all stages. - const vehicleFinalStatuses = new Map(); - - // --- 2. Calculation Phase: Use the fetched data --- - for (const stage of stages) { - try { - // Inner try-catch for processing each stage - const vehicleEtas = await calculateEtasForStage( - stage, - vehicleLocations, - ); - - // Transform the array into an object with carId as the key for Firebase - const etasForFirebase = vehicleEtas.reduce((acc, vehicle) => { - acc[vehicle.carId] = { - plateNumber: vehicle.plateNumber, - eta: vehicle.eta, - status: vehicle.status, - }; - return acc; - }, {}); - - await updateStageEtas(stage.name, etasForFirebase); - - // Store the status of each vehicle for this stage. - for (const vehicle of vehicleEtas) { - // If the vehicle is 'Arrived' at this stage, its final status for the cycle is 'Arrived'. - // Otherwise, only set the status if it hasn't already been marked as 'Arrived' by another stage. - if ( - vehicle.status === "Arrived" || - !vehicleFinalStatuses.has(vehicle.id) - ) { - vehicleFinalStatuses.set(vehicle.id, vehicle.status); - } - } - - // --- SMS Alert Logic --- - for (const vehicle of vehicleEtas) { - const vehicleInstance = allVehicles.find((v) => v.id === vehicle.id); - if (!vehicleInstance) continue; - - if (vehicle.status === "Arrived") { - // --- New Trip Counting Logic --- - // Use a transaction with row-level locking to prevent race conditions - await sequelize.transaction(async (t) => { - const freshVehicle = await Vehicle.findByPk(vehicle.id, { - transaction: t, // Find within the transaction - }); - - if (freshVehicle && !freshVehicle.isAtStage) { - const today = new Date().toISOString().slice(0, 10); - // Use a raw query with ON DUPLICATE KEY UPDATE for an atomic increment. - // This is more performant and less prone to deadlocks than findOrCreate/increment. - await sequelize.query( - `INSERT INTO DailyTrips (vehicleId, date, tripCount, createdAt, updatedAt) - VALUES (:vehicleId, :date, 1, NOW(), NOW()) - ON DUPLICATE KEY UPDATE tripCount = tripCount + 1, updatedAt = NOW()`, - { - replacements: { vehicleId: vehicle.id, date: today }, - type: sequelize.QueryTypes.INSERT, - transaction: t, - }, - ); - - await freshVehicle.update( - { isAtStage: true }, - { transaction: t }, - ); - console.log(`Trip counted for ${vehicle.plateNumber}.`); - } - }); - - // We iterate over a copy of the subscribers array because we are modifying the subscription - // in the loop, which could cause issues with the iterator. - const smsTasks = stage.Subscribers.map(async (subscriber) => { - try { - const isStillSubscribed = await subscriber.hasAlertSubscription( - stage.id, - ); - if (subscriber.phoneNumber && isStillSubscribed) { - const message = `Matatu ${vehicle.plateNumber} has arrived at ${stage.name}.`; - console.log( - `Sending SMS to ${subscriber.phoneNumber}: "${message}"`, - ); - const smsResponse = await sendSms( - subscriber.phoneNumber, - message, - ); - // Only unsubscribe if the SMS was successfully sent (status code 101). - if ( - smsResponse?.SMSMessageData?.Recipients?.[0]?.statusCode === - 101 - ) { - await subscriber.removeAlertSubscription(stage); - } else { - console.warn( - `SMS to ${subscriber.phoneNumber} may have failed or was skipped. User will not be unsubscribed.`, - ); - } - } - } catch (smsError) { - // Log errors for individual SMS/DB failures without stopping the whole batch. - console.error( - `Failed to process SMS for user ${subscriber.id}:`, - smsError, - ); - } - }); - // Process all SMS notifications concurrently for this arrival event. - await Promise.allSettled(smsTasks); - } - } - } catch (stageError) { - console.error(`❌ Error processing stage ${stage.name}:`, stageError); - } - } - - // --- 2.5. State Reset Phase --- - // Now that all stages are processed, reset the isAtStage flag for any vehicle - // that is no longer 'Arrived' at ANY of its assigned stages. - for (const vehicle of allVehicles) { - if ( - vehicle.isAtStage && - vehicleFinalStatuses.get(vehicle.id) !== "Arrived" - ) { - await vehicle.update({ isAtStage: false }); - } - } - // --- 3. Write Phase: Update distances and last known locations --- - console.log("Updating vehicle distances and locations..."); - for (const vehicle of allVehicles) { - const currentLocation = vehicleLocations.get(vehicle.id); - if (currentLocation) { - // This service now handles both distance calculation and conditionally updating the last known location. - await updateVehicleDistance(vehicle, currentLocation); - } - } - - console.log("✅ ETA update cycle completed successfully."); - } catch (error) { - console.error("❌ Error during ETA update cycle:", error); - } finally { - isCycleRunning = false; - } -}; - -const startEtaUpdateCycle = (intervalSeconds) => { - // Schedule the task to run every `intervalSeconds`. - // The cron job ensures that a new job won't start until the previous one has finished. - cron.schedule(`*/${intervalSeconds} * * * * *`, runEtaUpdateCycle, { - scheduled: true, - timezone: "UTC", - }); - console.log( - `✅ ETA update cycle scheduled to run every ${intervalSeconds} seconds.`, - ); -}; - -module.exports = { startEtaUpdateCycle }; diff --git a/server/services/distanceTrackingService.js b/server/services/distanceTrackingService.js deleted file mode 100644 index 0507875..0000000 --- a/server/services/distanceTrackingService.js +++ /dev/null @@ -1,57 +0,0 @@ -const { Vehicle, DailyDistance } = require("../models"); -const { getVehicleLocation } = require("./firebaseService"); -const { Op } = require("sequelize"); -const { haversineDistance } = require("./geolocationService"); - -/** - * Updates the daily distance for a single vehicle. - * @param {object} vehicle - The Sequelize vehicle instance. - * @param {object} currentLocation - The vehicle's current location { latitude, longitude }. - */ -async function updateVehicleDistance(vehicle, currentLocation) { - const { latitude: lat2, longitude: lon2 } = currentLocation; - const { lastLatitude: lat1, lastLongitude: lon1 } = vehicle; - - // If we have previous coordinates from the last cycle, calculate the distance moved - if (lat1 && lon1) { - const distanceMoved = haversineDistance(lat1, lon1, lat2, lon2); - - // A threshold is crucial to prevent accumulation of false distance from GPS drift. - // Only count movement if it's greater than a reasonable threshold (e.g., 10 meters). - if (distanceMoved > 10) { - const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD - - // Find or create a daily distance record for this vehicle for today - const [dailyDistance] = await DailyDistance.findOrCreate({ - where: { - vehicleId: vehicle.id, - date: today, - }, - defaults: { - vehicleId: vehicle.id, - date: today, - distanceCovered: 0, - }, - }); - - // Add the new distance to the existing total for the day - await dailyDistance.increment("distanceCovered", { by: distanceMoved }); - - // Update the last known location for the *next* cycle because significant movement was detected. - await vehicle.update({ - lastLatitude: lat2, - lastLongitude: lon2, - }); - } - // If distanceMoved <= 10, we do NOT update the last known location to prevent losing small increments. - } else { - // If there's no previous location, this is the first time we're seeing the vehicle. - // Set the current one as the baseline for the next cycle. - await vehicle.update({ - lastLatitude: lat2, - lastLongitude: lon2, - }); - } -} - -module.exports = { updateVehicleDistance }; diff --git a/server/services/etaCalculationService.js b/server/services/etaCalculationService.js deleted file mode 100644 index 399f123..0000000 --- a/server/services/etaCalculationService.js +++ /dev/null @@ -1,145 +0,0 @@ -const { getVehicleLocation } = require("./firebaseService"); -const { getDirectionsInfo } = require("./googleMapsService"); -const { calculateBearing, haversineDistance } = require("./geolocationService"); - -/** - * Calculates ETAs for all vehicles assigned to a specific stage. - * @param {object} stage - A Sequelize Stage object. - * @param {Map} vehicleLocations - A map of vehicleId to its current location. - * @returns {Promise} A promise that resolves to an array of vehicle ETA objects. - */ -const calculateEtasForStage = async (stage, vehicleLocations) => { - if (!stage || !stage.Vehicles) { - return []; - } - - const vehicleEtas = await Promise.all( - stage.Vehicles.map(async (vehicle) => { - const carId = vehicle.carId; - const currentVehicleLocation = vehicleLocations.get(vehicle.id); - - if ( - !currentVehicleLocation || - !currentVehicleLocation.latitude || - !currentVehicleLocation.longitude - ) { - console.log(`Location data not found or incomplete for ${carId}`); - return { - id: vehicle.id, - carId, - plateNumber: vehicle.plateNumber, - eta: "N/A", - status: "Offline", - }; - } - - const origin = { - latitude: currentVehicleLocation.latitude, - longitude: currentVehicleLocation.longitude, - }; - const destination = { - latitude: stage.latitude, - longitude: stage.longitude, - }; - - // --- Geofencing Check --- - // First, calculate the direct distance. Only query Google Maps if the vehicle is within a reasonable radius. - // This prevents excessive API calls for vehicles that are very far away. - const GEOFENCE_RADIUS_METERS = 5000; // 5km - const distanceToStage = haversineDistance( - origin.latitude, - origin.longitude, - destination.latitude, - destination.longitude, - ); - - if (distanceToStage > GEOFENCE_RADIUS_METERS) { - // The vehicle is outside the relevant zone, no need to calculate a route. - return { - id: vehicle.id, - carId, - plateNumber: vehicle.plateNumber, - eta: "N/A", - status: "Too Far", - }; - } - - const directions = await getDirectionsInfo(origin, destination); - - if (!directions || !directions.distance || !directions.duration) { - console.log(`Could not calculate directions for ${carId}`); - return { - id: vehicle.id, - carId, - plateNumber: vehicle.plateNumber, - eta: "N/A", - status: "No Route", - }; - } - - const duration = directions.duration_in_traffic || directions.duration; - const distanceInMeters = directions.distance.value; - let status = "Approaching"; - - if (distanceInMeters < 50) { - status = "Arrived"; - } else { - // Direction check for vehicles that are not at the stage - const previousLocation = - vehicle.lastLatitude && vehicle.lastLongitude - ? { - latitude: vehicle.lastLatitude, - longitude: vehicle.lastLongitude, - } - : null; - - // Check if the vehicle has moved significantly to avoid noise from GPS drift - const hasMoved = - previousLocation && - haversineDistance( - previousLocation.latitude, - previousLocation.longitude, - origin.latitude, - origin.longitude, - ) > 10; // Threshold of 10 meters - - if (previousLocation && hasMoved) { - const vehicleBearing = calculateBearing(previousLocation, origin); - const bearingToStage = calculateBearing(origin, destination); - - // Calculate the difference in angle. The result is between 0 and 180. - let angleDiff = Math.abs(vehicleBearing - bearingToStage); - if (angleDiff > 180) { - angleDiff = 360 - angleDiff; - } - - // If the angle is greater than 90 degrees, the vehicle is moving away from the stage. - if (angleDiff > 90) { - status = "Departed"; - } - } - } - - return { - id: vehicle.id, - carId: carId, - plateNumber: vehicle.plateNumber, - // Only show ETA if the vehicle is actually approaching - eta: - status === "Approaching" - ? duration.text - : status === "Arrived" - ? "Arrived" - : "N/A", - status: status, - }; - }), - ); - - // Filter out vehicles that have departed so they don't show up on the passenger's screen for that stage - return vehicleEtas.filter( - (v) => v.status !== "Departed" && v.status !== "Too Far", - ); -}; - -module.exports = { calculateEtasForStage }; diff --git a/server/services/firebaseService.js b/server/services/firebaseService.js deleted file mode 100644 index 48d8b01..0000000 --- a/server/services/firebaseService.js +++ /dev/null @@ -1,88 +0,0 @@ -const admin = require("firebase-admin"); -const path = require("path"); -const fs = require("fs"); - -const serviceAccountPath = path.join( - __dirname, - "../config/firebase-service-account.json", -); -let firebaseInitialized = false; - -let serviceAccount; - -if (process.env.FIREBASE_CREDENTIALS_BASE64) { - try { - const decodedString = Buffer.from( - process.env.FIREBASE_CREDENTIALS_BASE64, - "base64", - ).toString("utf-8"); - serviceAccount = JSON.parse(decodedString); - } catch (e) { - console.error("❌ Failed to parse FIREBASE_CREDENTIALS_BASE64:", e); - } -} else if (fs.existsSync(serviceAccountPath)) { - serviceAccount = require(serviceAccountPath); -} - -if (serviceAccount) { - try { - admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), - databaseURL: - "https://matatu-eta-default-rtdb.europe-west1.firebasedatabase.app/", - }); - console.log("✅ Firebase Admin SDK initialized successfully."); - firebaseInitialized = true; - } catch (error) { - console.error("❌ Error initializing Firebase Admin SDK:", error); - } -} else { - console.warn( - "⚠️ Firebase credentials not found. Checked FIREBASE_CREDENTIALS_BASE64 env var and 'server/config/firebase-service-account.json' file.", - ); - console.warn( - "Firebase-dependent features (live locations, ETAs) will be disabled.", - ); -} - -const db = firebaseInitialized ? admin.database() : null; -const locationsRef = firebaseInitialized ? db.ref("locations") : null; -const etasRef = firebaseInitialized ? db.ref("etas") : null; - -/** - * Fetches the real-time location data for a specific vehicle. - * @param {string} carId - The ID of the car (e.g., "CAR_001"). - * @returns {Promise} A promise that resolves to the vehicle's location data or null if not found. - */ -const getVehicleLocation = async (carId) => { - if (!firebaseInitialized) return null; - const snapshot = await locationsRef.child(carId).once("value"); - return snapshot.val(); -}; - -/** - * Updates the ETA data for a specific stage in Firebase. - * @param {string} stageId - The ID of the stage. - * @param {object} etas - The ETA data object to be stored. - * @returns {Promise} - */ -const updateStageEtas = async (stageId, etas) => { - if (!firebaseInitialized) return; - await etasRef.child(stageId).set(etas); -}; - -/** - * Fetches all vehicle locations from Firebase in a single operation. - * @returns {Promise} A promise that resolves to an object of all vehicle locations or null. - */ -const getAllVehicleLocations = async () => { - if (!firebaseInitialized) return null; - const snapshot = await locationsRef.once("value"); - return snapshot.val(); -}; - -module.exports = { - getVehicleLocation, - updateStageEtas, - getAllVehicleLocations, -}; diff --git a/server/services/geolocationService.js b/server/services/geolocationService.js deleted file mode 100644 index fbf9253..0000000 --- a/server/services/geolocationService.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Converts degrees to radians. - * @param {number} degrees - * @returns {number} - */ -function toRadians(degrees) { - return (degrees * Math.PI) / 180; -} - -/** - * Converts radians to degrees. - * @param {number} radians - * @returns {number} - */ -function toDegrees(radians) { - return (radians * 180) / Math.PI; -} - -/** - * Calculates the bearing (direction) from one GPS coordinate to another. - * @param {{latitude: number, longitude: number}} start - The starting coordinate. - * @param {{latitude: number, longitude: number}} end - The ending coordinate. - * @returns {number} The bearing in degrees (0-360). - */ -function calculateBearing(start, end) { - const startLat = toRadians(start.latitude); - const startLng = toRadians(start.longitude); - const destLat = toRadians(end.latitude); - const destLng = toRadians(end.longitude); - - const y = Math.sin(destLng - startLng) * Math.cos(destLat); - const x = - Math.cos(startLat) * Math.sin(destLat) - - Math.sin(startLat) * Math.cos(destLat) * Math.cos(destLng - startLng); - - let brng = Math.atan2(y, x); - brng = toDegrees(brng); - - return (brng + 360) % 360; // Normalize to 0-360 -} - -/** - * Calculates the distance between two GPS coordinates in meters using the Haversine formula. - * @param {number} lat1 Latitude of the first point. - * @param {number} lon1 Longitude of the first point. - * @param {number} lat2 Latitude of the second point. - * @param {number} lon2 Longitude of the second point. - * @returns {number} The distance in meters. - */ -function haversineDistance(lat1, lon1, lat2, lon2) { - const R = 6371e3; // Earth's radius in meters - const φ1 = toRadians(lat1); - const φ2 = toRadians(lat2); - const Δφ = ((lat2 - lat1) * Math.PI) / 180; - const Δλ = ((lon2 - lon1) * Math.PI) / 180; - - const a = - Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + - Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return R * c; -} - -module.exports = { calculateBearing, haversineDistance }; diff --git a/server/services/googleMapsService.js b/server/services/googleMapsService.js deleted file mode 100644 index 748b354..0000000 --- a/server/services/googleMapsService.js +++ /dev/null @@ -1,117 +0,0 @@ -const axios = require("axios"); -require("dotenv").config(); - -const API_KEY = process.env.GOOGLE_MAPS_API_KEY; -const DIRECTIONS_URL = "https://maps.googleapis.com/maps/api/directions/json"; - -const routeCache = new Map(); -const geocodeCache = new Map(); -const CACHE_DURATION_MS = 5 * 60 * 1000; // Cache results for 5 minutes - -/** - * Calculates the estimated travel time and distance between an origin and a destination. - * @param {object} origin - The starting location { latitude, longitude }. - * @param {object} destination - The ending location { latitude, longitude }. - * @returns {Promise} A promise that resolves to the route leg object, or null on error. - */ -const getDirectionsInfo = async (origin, destination) => { - if (!API_KEY) { - console.error("❌ Google Maps API Key is missing."); - return null; - } - - // Use rounded coordinates for cache key to increase hit rate for minor GPS drifts - const cacheKey = `${origin.latitude.toFixed(3)},${origin.longitude.toFixed(3)}-${destination.latitude.toFixed(3)},${destination.longitude.toFixed(3)}`; - const cached = routeCache.get(cacheKey); - if (cached && cached.expiresAt > Date.now()) { - return cached.data; - } - routeCache.delete(cacheKey); // Clear stale entry - - const params = { - origin: `${origin.latitude},${origin.longitude}`, - destination: `${destination.latitude},${destination.longitude}`, - departure_time: "now", // Add this to get traffic-aware ETAs - key: API_KEY, - }; - - try { - const response = await axios.get(DIRECTIONS_URL, { params, timeout: 5000 }); - const route = response.data.routes[0]; - if (route && route.legs && route.legs[0]) { - const leg = route.legs[0]; - routeCache.set(cacheKey, { - data: leg, - expiresAt: Date.now() + CACHE_DURATION_MS, - }); - return leg; // e.g., { distance: { text, value }, duration: { text, value }, ... } - } - return null; - } catch (error) { - console.error( - "❌ Error fetching directions from Google Maps:", - error.response ? error.response.data : error.message, - ); - return null; - } -}; - -/** - * Converts coordinates into a human-readable address. - * @param {object} location - The location to geocode { latitude, longitude }. - * @returns {Promise} A promise that resolves to a formatted address string or null on error. - */ -const getLocationName = async (location) => { - if (!API_KEY) { - console.error("❌ Google Maps API Key is missing."); - return "Unknown Location"; - } - - // Use rounded coordinates for cache key - const cacheKey = `${location.latitude.toFixed(3)},${location.longitude.toFixed(3)}`; - const cached = geocodeCache.get(cacheKey); - if (cached && cached.expiresAt > Date.now()) { - return cached.data; - } - geocodeCache.delete(cacheKey); // Clear stale entry - - const GEOCODE_URL = "https://maps.googleapis.com/maps/api/geocode/json"; - const params = { - latlng: `${location.latitude},${location.longitude}`, - key: API_KEY, - // Optional: Add location_type to filter results. ROOFTOP, RANGE_INTERPOLATED, GEOMETRIC_CENTER, APPROXIMATE - // Optional: Add result_type to filter. e.g., 'route' to prefer roads. - }; - - try { - const response = await axios.get(GEOCODE_URL, { params, timeout: 5000 }); - if (response.data.results && response.data.results.length > 0) { - const results = response.data.results; - - // --- New Logic to find the best address --- - // 1. Look for a specific street name ('route'). - const route = results.find((r) => r.types.includes("route")); - let address; - if (route) { - address = route.formatted_address; - } else { - // 2. If no street is found, fall back to the most specific result (the first one). - address = results[0].formatted_address; - } - geocodeCache.set(cacheKey, { - data: address, - expiresAt: Date.now() + CACHE_DURATION_MS, - }); - return address; - } - return "Location not found"; - } catch (error) { - console.error( - "❌ Error fetching geocode data from Google Maps:", - error.response ? error.response.data : error.message, - ); - return "Unknown Location"; - } -}; - -module.exports = { getDirectionsInfo, getLocationName }; diff --git a/server/services/locationCache.js b/server/services/locationCache.js deleted file mode 100644 index e8b5ec1..0000000 --- a/server/services/locationCache.js +++ /dev/null @@ -1,61 +0,0 @@ -require("dotenv").config(); - -let redis; -if (process.env.REDIS_URL) { - try { - const Redis = require("ioredis"); - redis = new Redis(process.env.REDIS_URL); - console.log("✅ Redis connected successfully."); - redis.on("error", (err) => - console.error("❌ Redis connection error:", err), - ); - } catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - console.warn( - "⚠️ `ioredis` package not found. Caching service will be in-memory only.", - ); - console.warn("Run `npm install ioredis` to enable Redis caching."); - redis = null; - } else { - // For other errors, it's better to let the app crash. - throw e; - } - } -} else { - console.warn( - "REDIS_URL not set. Caching service will be in-memory only and not scalable.", - ); -} -// This service will now handle the vehicle locations cache. -// It uses Redis if available, otherwise falls back to in-memory Map. -const inMemoryCache = new Map(); -const CACHE_KEY = "vehicle_locations"; -const CACHE_TTL_SECONDS = 60; // How long to keep the data in Redis - -const getLocations = async () => { - if (redis) { - const data = await redis.get(CACHE_KEY); - return data ? new Map(JSON.parse(data)) : new Map(); - } - return inMemoryCache; -}; - -const setLocations = async (locations) => { - const mapAsArray = Array.from(locations.entries()); - if (redis) { - await redis.set( - CACHE_KEY, - JSON.stringify(mapAsArray), - "EX", - CACHE_TTL_SECONDS, - ); - } else { - // For in-memory, we just replace the map. - inMemoryCache.clear(); - for (const [key, value] of locations) { - inMemoryCache.set(key, value); - } - } -}; - -module.exports = { getLocations, setLocations, redis }; diff --git a/server/services/smsService.js b/server/services/smsService.js deleted file mode 100644 index 5d675ba..0000000 --- a/server/services/smsService.js +++ /dev/null @@ -1,42 +0,0 @@ -require("dotenv").config(); -const AfricasTalking = require("africastalking"); - -const credentials = { - apiKey: process.env.AFRICAS_TALKING_API_KEY, - username: process.env.AFRICAS_TALKING_USERNAME, -}; - -// Check for credentials early and initialize the SDK. -let sms; -if (credentials.apiKey && credentials.username) { - const at = AfricasTalking(credentials); - sms = at.SMS; -} else { - console.warn( - "Africa's Talking credentials not set. SMS service is disabled.", - ); -} - -const sendSms = async (phoneNumber, message) => { - // If the SDK was not initialized, skip sending. - if (!sms) { - console.log("Skipping SMS: Africa's Talking service is not configured."); - return { status: "skipped" }; - } - - try { - const response = await sms.send({ - to: [phoneNumber], // The SDK expects an array of numbers - message: message, - }); - - console.log("SMS sent successfully:", response); - return response; - } catch (error) { - // The SDK provides a more structured error object - console.error(`Error sending SMS: ${error.toString()}`); - throw error; - } -}; - -module.exports = { sendSms }; diff --git a/server/services/tokenBlocklistService.js b/server/services/tokenBlocklistService.js deleted file mode 100644 index 238b5cd..0000000 --- a/server/services/tokenBlocklistService.js +++ /dev/null @@ -1,81 +0,0 @@ -require("dotenv").config(); - -let redis; -// In-memory fallback for the blocklist -const inMemoryBlocklist = new Map(); // Use a Map to store jti -> expiration -let cleanupInterval = null; - -if (process.env.REDIS_URL) { - try { - const Redis = require("ioredis"); - redis = new Redis(process.env.REDIS_URL); - console.log("✅ Token blocklist connected to Redis."); - } catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - console.warn( - "⚠️ `ioredis` package not found. Token blocklist will be in-memory only.", - ); - redis = null; - } else { - throw e; - } - } -} else { - console.warn( - "REDIS_URL not set. Token blocklist will be in-memory only and not scalable.", - ); - - // Start a single cleanup interval for the in-memory blocklist - if (!cleanupInterval) { - cleanupInterval = setInterval( - () => { - const now = Math.floor(Date.now() / 1000); - let deletedCount = 0; - for (const [jti, expires] of inMemoryBlocklist.entries()) { - if (expires < now) { - inMemoryBlocklist.delete(jti); - deletedCount++; - } - } - if (deletedCount > 0 && process.env.NODE_ENV !== "production") { - console.log( - `In-memory blocklist cleaned up ${deletedCount} expired tokens. Size: ${inMemoryBlocklist.size}`, - ); - } - }, - 15 * 60 * 1000, - ); // Run every 15 minutes - cleanupInterval.unref(); // Allow the process to exit if this is the only timer - } -} - -const BLOCKLIST_PREFIX = "blocklist:"; - -const addToBlocklist = async (jti, expires) => { - // Calculate the remaining time-to-live for the token in seconds. - const now = Math.floor(Date.now() / 1000); - const ttl = expires - now; - - // Only add to blocklist if the token hasn't already expired. - if (ttl <= 0) { - return; - } - - if (redis) { - await redis.set(`${BLOCKLIST_PREFIX}${jti}`, "1", "EX", ttl); - } else { - // Fallback to in-memory Map, storing the expiration time - inMemoryBlocklist.set(jti, expires); - } -}; - -const isBlocklisted = async (jti) => { - if (redis) { - const result = await redis.get(`${BLOCKLIST_PREFIX}${jti}`); - return result === "1"; - } - // Fallback to in-memory Set - return inMemoryBlocklist.has(jti); -}; - -module.exports = { addToBlocklist, isBlocklisted }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..022ddab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "es2023"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], + "exclude": ["node_modules", "src/generated"] +} From 33637b67bf69062621fd545146bb365002401e1e Mon Sep 17 00:00:00 2001 From: cliffkoome Date: Mon, 19 Oct 2026 20:49:48 +0300 Subject: [PATCH 2/6] Add Prisma schema for Neon Postgres, init migration and seed - Models ported from Sequelize with snake_case tables, timestamptz, explicit FKs - Explicit join tables, soft-deleted vehicles, revoked_tokens and job_locks tables - CHECK constraint on feedback rating (added by hand to the init migration) - prisma.config.ts: pooled DATABASE_URL at runtime, DIRECT_URL for the CLI - Idempotent dev seed --- prisma.config.ts | 25 +++ .../20261019000000_init/migration.sql | 205 ++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 191 ++++++++++++++++ prisma/seed.ts | 72 ++++++ 5 files changed, 496 insertions(+) create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20261019000000_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.ts diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..6df99e5 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,25 @@ +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +/** + * Prisma CLI configuration. + * + * Neon exposes two connection strings: + * - DATABASE_URL: the *pooled* (PgBouncer, `-pooler` host) URL used by the app at runtime. + * - DIRECT_URL: the *direct* (unpooled) URL. Migrations need a direct session + * (advisory locks, DDL in transactions), so the CLI always prefers it. + * + * `prisma generate` does not need a database, so a missing URL is allowed here; commands + * that do need one (`migrate`, `db`, `studio`) will fail with a clear Prisma error. + */ +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + seed: "tsx prisma/seed.ts", + }, + datasource: { + url: process.env.DIRECT_URL ?? process.env.DATABASE_URL ?? "", + shadowDatabaseUrl: process.env.SHADOW_DATABASE_URL, + }, +}); diff --git a/prisma/migrations/20261019000000_init/migration.sql b/prisma/migrations/20261019000000_init/migration.sql new file mode 100644 index 0000000..ba92cdf --- /dev/null +++ b/prisma/migrations/20261019000000_init/migration.sql @@ -0,0 +1,205 @@ +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "public"; + +-- CreateEnum +CREATE TYPE "user_role" AS ENUM ('Passenger', 'Admin', 'MatAdmin'); + +-- CreateTable +CREATE TABLE "users" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT, + "role" "user_role" NOT NULL DEFAULT 'Passenger', + "google_id" TEXT, + "phone_number" TEXT, + "profile_picture_url" TEXT, + "two_factor_secret" TEXT, + "two_factor_enabled" BOOLEAN NOT NULL DEFAULT false, + "disabled" BOOLEAN NOT NULL DEFAULT false, + "tokens_valid_from" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "stages" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "latitude" DOUBLE PRECISION NOT NULL, + "longitude" DOUBLE PRECISION NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "stages_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "vehicles" ( + "id" SERIAL NOT NULL, + "car_id" TEXT NOT NULL, + "plate_number" TEXT NOT NULL, + "owner_id" INTEGER, + "last_latitude" DOUBLE PRECISION, + "last_longitude" DOUBLE PRECISION, + "is_at_stage" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + "deleted_at" TIMESTAMPTZ(3), + + CONSTRAINT "vehicles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "stage_vehicles" ( + "stage_id" INTEGER NOT NULL, + "vehicle_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "stage_vehicles_pkey" PRIMARY KEY ("stage_id","vehicle_id") +); + +-- CreateTable +CREATE TABLE "stage_subscriptions" ( + "user_id" INTEGER NOT NULL, + "stage_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "stage_subscriptions_pkey" PRIMARY KEY ("user_id","stage_id") +); + +-- CreateTable +CREATE TABLE "daily_distances" ( + "id" SERIAL NOT NULL, + "vehicle_id" INTEGER NOT NULL, + "date" DATE NOT NULL, + "distance_covered" DOUBLE PRECISION NOT NULL DEFAULT 0, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "daily_distances_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "daily_trips" ( + "id" SERIAL NOT NULL, + "vehicle_id" INTEGER NOT NULL, + "date" DATE NOT NULL, + "trip_count" INTEGER NOT NULL DEFAULT 0, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "daily_trips_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "feedback" ( + "id" SERIAL NOT NULL, + "vehicle_id" INTEGER NOT NULL, + "user_id" INTEGER NOT NULL, + "rating" INTEGER NOT NULL, + "comment" TEXT, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "feedback_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "job_locks" ( + "name" TEXT NOT NULL, + "locked_until" TIMESTAMPTZ(3) NOT NULL, + "locked_by" TEXT, + "updated_at" TIMESTAMPTZ(3) NOT NULL, + + CONSTRAINT "job_locks_pkey" PRIMARY KEY ("name") +); + +-- CreateTable +CREATE TABLE "revoked_tokens" ( + "jti" TEXT NOT NULL, + "expires_at" TIMESTAMPTZ(3) NOT NULL, + "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "revoked_tokens_pkey" PRIMARY KEY ("jti") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_google_id_key" ON "users"("google_id"); + +-- CreateIndex +CREATE INDEX "users_role_idx" ON "users"("role"); + +-- CreateIndex +CREATE UNIQUE INDEX "stages_name_key" ON "stages"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "vehicles_car_id_key" ON "vehicles"("car_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "vehicles_plate_number_key" ON "vehicles"("plate_number"); + +-- CreateIndex +CREATE INDEX "vehicles_owner_id_idx" ON "vehicles"("owner_id"); + +-- CreateIndex +CREATE INDEX "vehicles_deleted_at_idx" ON "vehicles"("deleted_at"); + +-- CreateIndex +CREATE INDEX "stage_vehicles_vehicle_id_idx" ON "stage_vehicles"("vehicle_id"); + +-- CreateIndex +CREATE INDEX "stage_subscriptions_stage_id_idx" ON "stage_subscriptions"("stage_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "daily_distances_vehicle_id_date_key" ON "daily_distances"("vehicle_id", "date"); + +-- CreateIndex +CREATE UNIQUE INDEX "daily_trips_vehicle_id_date_key" ON "daily_trips"("vehicle_id", "date"); + +-- CreateIndex +CREATE INDEX "feedback_vehicle_id_created_at_idx" ON "feedback"("vehicle_id", "created_at"); + +-- CreateIndex +CREATE INDEX "feedback_user_id_idx" ON "feedback"("user_id"); + +-- CreateIndex +CREATE INDEX "feedback_created_at_idx" ON "feedback"("created_at"); + +-- CreateIndex +CREATE INDEX "revoked_tokens_expires_at_idx" ON "revoked_tokens"("expires_at"); + +-- AddForeignKey +ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "stage_vehicles" ADD CONSTRAINT "stage_vehicles_stage_id_fkey" FOREIGN KEY ("stage_id") REFERENCES "stages"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "stage_vehicles" ADD CONSTRAINT "stage_vehicles_vehicle_id_fkey" FOREIGN KEY ("vehicle_id") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "stage_subscriptions" ADD CONSTRAINT "stage_subscriptions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "stage_subscriptions" ADD CONSTRAINT "stage_subscriptions_stage_id_fkey" FOREIGN KEY ("stage_id") REFERENCES "stages"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "daily_distances" ADD CONSTRAINT "daily_distances_vehicle_id_fkey" FOREIGN KEY ("vehicle_id") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "daily_trips" ADD CONSTRAINT "daily_trips_vehicle_id_fkey" FOREIGN KEY ("vehicle_id") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "feedback" ADD CONSTRAINT "feedback_vehicle_id_fkey" FOREIGN KEY ("vehicle_id") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "feedback" ADD CONSTRAINT "feedback_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Manual addition: the Prisma schema cannot express CHECK constraints, and Prisma ignores them when diffing. +ALTER TABLE "feedback" ADD CONSTRAINT "feedback_rating_range" CHECK ("rating" BETWEEN 1 AND 5); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..fd6e3a6 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,191 @@ +// MatConnect database schema (PostgreSQL on Neon). +// +// Conventions (see docs/MIGRATIONS.md): +// - Models are PascalCase/singular in TypeScript, tables are snake_case/plural in Postgres (@@map). +// - Columns are snake_case in Postgres (@map) and camelCase in TypeScript. +// - Every table has created_at; mutable tables also have updated_at. +// - Timestamps are `timestamptz` (@db.Timestamptz(3)); calendar days are `date` (@db.Date). +// - Foreign keys always declare onDelete explicitly. +// - Never edit a migration that has been merged to main; add a new one instead. + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +enum Role { + Passenger + Admin + MatAdmin + + @@map("user_role") +} + +model User { + id Int @id @default(autoincrement()) + name String + /// Always stored lower-cased (normalised at the validation layer). + email String @unique + /// bcrypt hash. Null for accounts created through Google sign-in. + password String? + role Role @default(Passenger) + googleId String? @unique @map("google_id") + /// E.164 format, e.g. +254712345678. + phoneNumber String? @map("phone_number") + profilePictureUrl String? @map("profile_picture_url") + twoFactorSecret String? @map("two_factor_secret") + twoFactorEnabled Boolean @default(false) @map("two_factor_enabled") + disabled Boolean @default(false) + /// Tokens issued before this instant are rejected (logout-everywhere / block / password reset). + tokensValidFrom DateTime @default(now()) @map("tokens_valid_from") @db.Timestamptz(3) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + + ownedVehicles Vehicle[] + feedback Feedback[] + subscriptions StageSubscription[] + + @@index([role]) + @@map("users") +} + +model Stage { + id Int @id @default(autoincrement()) + name String @unique + latitude Float + longitude Float + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + + vehicles StageVehicle[] + subscribers StageSubscription[] + + @@map("stages") +} + +model Vehicle { + id Int @id @default(autoincrement()) + /// Identifier reported by the GPS device (key under `locations/` in Firebase). + carId String @unique @map("car_id") + plateNumber String @unique @map("plate_number") + ownerId Int? @map("owner_id") + lastLatitude Float? @map("last_latitude") + lastLongitude Float? @map("last_longitude") + /// True while the vehicle is inside an assigned stage's arrival radius (used for trip counting). + isAtStage Boolean @default(false) @map("is_at_stage") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + /// Soft delete marker. Always filter with `deletedAt: null` (see src/server/modules/vehicles). + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3) + + owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull) + stages StageVehicle[] + dailyDistances DailyDistance[] + dailyTrips DailyTrip[] + feedback Feedback[] + + @@index([ownerId]) + @@index([deletedAt]) + @@map("vehicles") +} + +/// Which vehicles serve which stages (the route a vehicle runs). +model StageVehicle { + stageId Int @map("stage_id") + vehicleId Int @map("vehicle_id") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + + stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade) + vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade) + + @@id([stageId, vehicleId]) + @@index([vehicleId]) + @@map("stage_vehicles") +} + +/// One-shot SMS alert subscriptions: removed automatically after the alert is delivered. +model StageSubscription { + userId Int @map("user_id") + stageId Int @map("stage_id") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + stage Stage @relation(fields: [stageId], references: [id], onDelete: Cascade) + + @@id([userId, stageId]) + @@index([stageId]) + @@map("stage_subscriptions") +} + +model DailyDistance { + id Int @id @default(autoincrement()) + vehicleId Int @map("vehicle_id") + date DateTime @db.Date + /// Metres travelled on `date` (UTC). + distanceCovered Float @default(0) @map("distance_covered") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + + vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade) + + @@unique([vehicleId, date]) + @@map("daily_distances") +} + +model DailyTrip { + id Int @id @default(autoincrement()) + vehicleId Int @map("vehicle_id") + date DateTime @db.Date + tripCount Int @default(0) @map("trip_count") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + + vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade) + + @@unique([vehicleId, date]) + @@map("daily_trips") +} + +model Feedback { + id Int @id @default(autoincrement()) + vehicleId Int @map("vehicle_id") + userId Int @map("user_id") + /// 1–5, enforced by the `feedback_rating_range` CHECK constraint in the init migration. + rating Int + comment String? + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + + vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([vehicleId, createdAt]) + @@index([userId]) + @@index([createdAt]) + @@map("feedback") +} + +/// Lease-style mutex for background jobs (works through Neon's PgBouncer pooler, unlike session +/// advisory locks), so overlapping cron invocations or several app instances never run a job twice. +model JobLock { + name String @id + lockedUntil DateTime @map("locked_until") @db.Timestamptz(3) + lockedBy String? @map("locked_by") + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3) + + @@map("job_locks") +} + +/// Revoked JWT IDs (logout and refresh-token rotation). Rows past `expiresAt` are purged by the ETA cycle. +model RevokedToken { + jti String @id + expiresAt DateTime @map("expires_at") @db.Timestamptz(3) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3) + + @@index([expiresAt]) + @@map("revoked_tokens") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..3abdfd6 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,72 @@ +/** + * Development seed — idempotent, safe to re-run: npm run db:seed + * + * Creates one account per role (password: `matconnect123`), a few Nairobi stages and vehicles. + * Never run against production: it refuses when NODE_ENV=production. + */ +import "dotenv/config"; +import { PrismaPg } from "@prisma/adapter-pg"; +import bcrypt from "bcryptjs"; +import pg from "pg"; +import { PrismaClient } from "../src/generated/prisma/client.ts"; + +if (process.env.NODE_ENV === "production") { + console.error("Refusing to seed with NODE_ENV=production."); + process.exit(1); +} + +const connectionString = process.env.DIRECT_URL ?? process.env.DATABASE_URL; +if (!connectionString) throw new Error("Set DATABASE_URL (or DIRECT_URL) before seeding."); + +// Same UTC session rule as src/server/db.ts (see createUtcPool there). +const pool = new pg.Pool({ connectionString, onConnect: async (client) => void (await client.query("SET TIME ZONE 'UTC'")) }); +const db = new PrismaClient({ adapter: new PrismaPg(pool) }); + +async function main() { + const password = await bcrypt.hash("matconnect123", 10); + const user = (email: string, name: string, role: "Admin" | "MatAdmin" | "Passenger", phoneNumber?: string) => + db.user.upsert({ + where: { email }, + update: {}, + create: { email, name, role, password, phoneNumber }, + }); + + const [, owner] = await Promise.all([ + user("admin@matconnect.local", "System Admin", "Admin"), + user("owner@matconnect.local", "Matatu Owner", "MatAdmin"), + user("passenger@matconnect.local", "Pat Passenger", "Passenger", "+254700000000"), + ]); + + const stages = await Promise.all( + [ + { name: "Kenyatta Avenue", latitude: -1.2841, longitude: 36.8233 }, + { name: "Kencom", latitude: -1.2864, longitude: 36.8253 }, + { name: "Westlands", latitude: -1.2676, longitude: 36.8108 }, + { name: "Archives", latitude: -1.2836, longitude: 36.8265 }, + ].map((s) => db.stage.upsert({ where: { name: s.name }, update: {}, create: s })), + ); + + const vehicles = await Promise.all( + [ + { carId: "CAR_001", plateNumber: "KDH 420X" }, + { carId: "CAR_002", plateNumber: "KCL 112A" }, + { carId: "CAR_003", plateNumber: "KDJ 899B" }, + ].map((v) => db.vehicle.upsert({ where: { carId: v.carId }, update: {}, create: { ...v, ownerId: owner.id } })), + ); + + // Route: every vehicle serves Westlands ↔ Kencom. + const route = stages.filter((s) => ["Westlands", "Kencom"].includes(s.name)); + await db.stageVehicle.createMany({ + data: vehicles.flatMap((v) => route.map((s) => ({ stageId: s.id, vehicleId: v.id }))), + skipDuplicates: true, + }); + + console.log(`Seeded ${stages.length} stages, ${vehicles.length} vehicles and 3 users (password: matconnect123).`); +} + +main() + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(() => db.$disconnect()); From 0c2f360333db8d5212d2516cb553113d1b26b1d9 Mon Sep 17 00:00:00 2001 From: cliffkoome Date: Mon, 19 Oct 2026 20:49:48 +0300 Subject: [PATCH 3/6] Port the API to route handlers on a layered server core route() wrapper (rate limits, auth, params, error mapping) -> feature services -> Prisma; pure domain logic for ETA/geo/periods; integrations for Firebase, Google Maps, Africa's Talking and SMTP; ETA cycle with a Postgres job lease, runnable from Vercel Cron, an in-process scheduler or a standalone worker. Fixes from the old server: typed JWTs (2FA temp token was accepted as an access token), working refresh rotation (isBlocklisted was never imported), blocks/password resets take effect immediately, Google sign-in redirect read the wrong part of the URL. --- scripts/eta-worker.ts | 21 ++ src/app/api/admin/_shared.ts | 5 + src/app/api/admin/dashboard-data/route.ts | 5 + src/app/api/admin/feedback/route.ts | 6 + src/app/api/admin/mat-admins/route.ts | 5 + .../[stageId]/vehicles/[vehicleId]/route.ts | 9 + .../api/admin/stages/assign-vehicle/route.ts | 9 + src/app/api/admin/stages/route.ts | 8 + src/app/api/admin/users/[id]/route.ts | 17 ++ src/app/api/admin/users/route.ts | 8 + src/app/api/admin/vehicles/[id]/route.ts | 6 + src/app/api/admin/vehicles/route.ts | 9 + src/app/api/auth/2fa/disable/route.ts | 8 + src/app/api/auth/2fa/login/route.ts | 13 + src/app/api/auth/2fa/setup/route.ts | 5 + src/app/api/auth/2fa/verify/route.ts | 10 + src/app/api/auth/google/callback/route.ts | 35 +++ src/app/api/auth/google/route.ts | 21 ++ src/app/api/auth/login/route.ts | 15 + src/app/api/auth/logout/route.ts | 17 ++ src/app/api/auth/me/admin/route.ts | 6 + src/app/api/auth/me/mat-admin/route.ts | 6 + src/app/api/auth/me/passenger/route.ts | 6 + src/app/api/auth/me/route.ts | 12 + src/app/api/auth/refresh-token/route.ts | 14 + src/app/api/auth/register/route.ts | 9 + .../api/auth/requestPasswordReset/route.ts | 9 + .../auth/resetpassword/[id]/[token]/route.ts | 11 + src/app/api/cron/eta-cycle/route.ts | 44 +++ src/app/api/eta/[stageId]/route.ts | 12 + src/app/api/feedback/route.ts | 7 + src/app/api/feedback/vehicles/route.ts | 5 + src/app/api/health/route.ts | 15 + src/app/api/mat-admin/_shared.ts | 4 + src/app/api/mat-admin/dashboard-data/route.ts | 5 + .../api/mat-admin/dashboard-summary/route.ts | 5 + .../mat-admin/fleet-aggregate-data/route.ts | 5 + src/app/api/mat-admin/vehicle-stats/route.ts | 6 + .../vehicles/[vehicleId]/chart-data/route.ts | 7 + src/app/api/mat-admin/vehicles/route.ts | 16 ++ .../api/stages/[stageId]/subscribe/route.ts | 8 + .../stages/[stageId]/subscription/route.ts | 8 + .../api/stages/[stageId]/unsubscribe/route.ts | 8 + src/app/api/stages/params.ts | 5 + src/app/api/stages/route.ts | 5 + src/instrumentation.ts | 22 ++ src/server/auth/context.ts | 14 + src/server/auth/cookies.ts | 22 ++ src/server/auth/google.ts | 75 +++++ src/server/auth/guard.ts | 54 ++++ src/server/auth/password.ts | 10 + src/server/auth/tokens.ts | 127 +++++++++ src/server/auth/totp.ts | 24 ++ src/server/db.ts | 71 +++++ src/server/domain/eta.ts | 93 +++++++ src/server/domain/geo.ts | 44 +++ src/server/domain/periods.ts | 78 ++++++ src/server/domain/phone.ts | 18 ++ src/server/domain/routes.ts | 10 + src/server/env.ts | 72 +++++ src/server/http/errors.ts | 30 ++ src/server/http/rate-limit.ts | 72 +++++ src/server/http/route.ts | 157 +++++++++++ src/server/integrations/firebase.ts | 91 ++++++ src/server/integrations/google-maps.ts | 85 ++++++ src/server/integrations/mailer.ts | 53 ++++ src/server/integrations/sms.ts | 48 ++++ src/server/jobs/eta-cycle.ts | 257 +++++++++++++++++ src/server/jobs/job-lock.ts | 31 +++ src/server/jobs/scheduler.ts | 35 +++ src/server/lib/ttl-cache.ts | 33 +++ src/server/modules/admin/admin.schemas.ts | 53 ++++ src/server/modules/admin/admin.service.ts | 182 ++++++++++++ src/server/modules/auth/auth.schemas.ts | 36 +++ src/server/modules/auth/auth.service.ts | 258 ++++++++++++++++++ src/server/modules/auth/revocation.ts | 20 ++ src/server/modules/eta/eta.service.ts | 55 ++++ .../modules/feedback/feedback.service.ts | 46 ++++ .../modules/mat-admin/mat-admin.service.ts | 130 +++++++++ src/server/modules/stages/stages.service.ts | 39 +++ src/server/modules/vehicles/vehicles.ts | 76 ++++++ src/server/validation/common.ts | 32 +++ 82 files changed, 3023 insertions(+) create mode 100644 scripts/eta-worker.ts create mode 100644 src/app/api/admin/_shared.ts create mode 100644 src/app/api/admin/dashboard-data/route.ts create mode 100644 src/app/api/admin/feedback/route.ts create mode 100644 src/app/api/admin/mat-admins/route.ts create mode 100644 src/app/api/admin/stages/[stageId]/vehicles/[vehicleId]/route.ts create mode 100644 src/app/api/admin/stages/assign-vehicle/route.ts create mode 100644 src/app/api/admin/stages/route.ts create mode 100644 src/app/api/admin/users/[id]/route.ts create mode 100644 src/app/api/admin/users/route.ts create mode 100644 src/app/api/admin/vehicles/[id]/route.ts create mode 100644 src/app/api/admin/vehicles/route.ts create mode 100644 src/app/api/auth/2fa/disable/route.ts create mode 100644 src/app/api/auth/2fa/login/route.ts create mode 100644 src/app/api/auth/2fa/setup/route.ts create mode 100644 src/app/api/auth/2fa/verify/route.ts create mode 100644 src/app/api/auth/google/callback/route.ts create mode 100644 src/app/api/auth/google/route.ts create mode 100644 src/app/api/auth/login/route.ts create mode 100644 src/app/api/auth/logout/route.ts create mode 100644 src/app/api/auth/me/admin/route.ts create mode 100644 src/app/api/auth/me/mat-admin/route.ts create mode 100644 src/app/api/auth/me/passenger/route.ts create mode 100644 src/app/api/auth/me/route.ts create mode 100644 src/app/api/auth/refresh-token/route.ts create mode 100644 src/app/api/auth/register/route.ts create mode 100644 src/app/api/auth/requestPasswordReset/route.ts create mode 100644 src/app/api/auth/resetpassword/[id]/[token]/route.ts create mode 100644 src/app/api/cron/eta-cycle/route.ts create mode 100644 src/app/api/eta/[stageId]/route.ts create mode 100644 src/app/api/feedback/route.ts create mode 100644 src/app/api/feedback/vehicles/route.ts create mode 100644 src/app/api/health/route.ts create mode 100644 src/app/api/mat-admin/_shared.ts create mode 100644 src/app/api/mat-admin/dashboard-data/route.ts create mode 100644 src/app/api/mat-admin/dashboard-summary/route.ts create mode 100644 src/app/api/mat-admin/fleet-aggregate-data/route.ts create mode 100644 src/app/api/mat-admin/vehicle-stats/route.ts create mode 100644 src/app/api/mat-admin/vehicles/[vehicleId]/chart-data/route.ts create mode 100644 src/app/api/mat-admin/vehicles/route.ts create mode 100644 src/app/api/stages/[stageId]/subscribe/route.ts create mode 100644 src/app/api/stages/[stageId]/subscription/route.ts create mode 100644 src/app/api/stages/[stageId]/unsubscribe/route.ts create mode 100644 src/app/api/stages/params.ts create mode 100644 src/app/api/stages/route.ts create mode 100644 src/instrumentation.ts create mode 100644 src/server/auth/context.ts create mode 100644 src/server/auth/cookies.ts create mode 100644 src/server/auth/google.ts create mode 100644 src/server/auth/guard.ts create mode 100644 src/server/auth/password.ts create mode 100644 src/server/auth/tokens.ts create mode 100644 src/server/auth/totp.ts create mode 100644 src/server/db.ts create mode 100644 src/server/domain/eta.ts create mode 100644 src/server/domain/geo.ts create mode 100644 src/server/domain/periods.ts create mode 100644 src/server/domain/phone.ts create mode 100644 src/server/domain/routes.ts create mode 100644 src/server/env.ts create mode 100644 src/server/http/errors.ts create mode 100644 src/server/http/rate-limit.ts create mode 100644 src/server/http/route.ts create mode 100644 src/server/integrations/firebase.ts create mode 100644 src/server/integrations/google-maps.ts create mode 100644 src/server/integrations/mailer.ts create mode 100644 src/server/integrations/sms.ts create mode 100644 src/server/jobs/eta-cycle.ts create mode 100644 src/server/jobs/job-lock.ts create mode 100644 src/server/jobs/scheduler.ts create mode 100644 src/server/lib/ttl-cache.ts create mode 100644 src/server/modules/admin/admin.schemas.ts create mode 100644 src/server/modules/admin/admin.service.ts create mode 100644 src/server/modules/auth/auth.schemas.ts create mode 100644 src/server/modules/auth/auth.service.ts create mode 100644 src/server/modules/auth/revocation.ts create mode 100644 src/server/modules/eta/eta.service.ts create mode 100644 src/server/modules/feedback/feedback.service.ts create mode 100644 src/server/modules/mat-admin/mat-admin.service.ts create mode 100644 src/server/modules/stages/stages.service.ts create mode 100644 src/server/modules/vehicles/vehicles.ts create mode 100644 src/server/validation/common.ts diff --git a/scripts/eta-worker.ts b/scripts/eta-worker.ts new file mode 100644 index 0000000..c4139aa --- /dev/null +++ b/scripts/eta-worker.ts @@ -0,0 +1,21 @@ +/** + * Standalone ETA worker for platforms that run a separate worker process (Railway, Fly, Render…) + * while the web app runs on serverless with ETA_CYCLE_MODE=cron or off. + * + * npm run eta:worker + */ +import "dotenv/config"; +import { startEtaScheduler, stopEtaScheduler } from "../src/server/jobs/scheduler"; + +startEtaScheduler(); +// Keep the process alive (the scheduler's timers are unref'd so they don't block Next.js shutdown). +const keepAlive = setInterval(() => {}, 1 << 30); + +for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, () => { + console.info(`[eta-worker] ${signal} received, stopping`); + stopEtaScheduler(); + clearInterval(keepAlive); + process.exit(0); + }); +} diff --git a/src/app/api/admin/_shared.ts b/src/app/api/admin/_shared.ts new file mode 100644 index 0000000..c9a05a0 --- /dev/null +++ b/src/app/api/admin/_shared.ts @@ -0,0 +1,5 @@ +import { rateLimits } from "@/server/http/rate-limit"; + +/** Every /api/admin route: Admin role + the admin rate limit (as in the Express router). */ +export const adminOnly = { auth: "Admin", rateLimit: rateLimits.admin } as const; +export const adminDashboard = { auth: "Admin", rateLimit: [rateLimits.admin, rateLimits.dashboard] } as const; diff --git a/src/app/api/admin/dashboard-data/route.ts b/src/app/api/admin/dashboard-data/route.ts new file mode 100644 index 0000000..16bcda7 --- /dev/null +++ b/src/app/api/admin/dashboard-data/route.ts @@ -0,0 +1,5 @@ +import { route } from "@/server/http/route"; +import { dashboardData } from "@/server/modules/admin/admin.service"; +import { adminDashboard } from "../_shared"; + +export const GET = route(adminDashboard, () => dashboardData()); diff --git a/src/app/api/admin/feedback/route.ts b/src/app/api/admin/feedback/route.ts new file mode 100644 index 0000000..132afb5 --- /dev/null +++ b/src/app/api/admin/feedback/route.ts @@ -0,0 +1,6 @@ +import { parseQuery, route } from "@/server/http/route"; +import { feedbackQuerySchema } from "@/server/modules/admin/admin.schemas"; +import { listFeedback } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../_shared"; + +export const GET = route(adminOnly, ({ req }) => listFeedback(parseQuery(req, feedbackQuerySchema))); diff --git a/src/app/api/admin/mat-admins/route.ts b/src/app/api/admin/mat-admins/route.ts new file mode 100644 index 0000000..bd86635 --- /dev/null +++ b/src/app/api/admin/mat-admins/route.ts @@ -0,0 +1,5 @@ +import { route } from "@/server/http/route"; +import { listMatAdmins } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../_shared"; + +export const GET = route(adminOnly, () => listMatAdmins()); diff --git a/src/app/api/admin/stages/[stageId]/vehicles/[vehicleId]/route.ts b/src/app/api/admin/stages/[stageId]/vehicles/[vehicleId]/route.ts new file mode 100644 index 0000000..98f4665 --- /dev/null +++ b/src/app/api/admin/stages/[stageId]/vehicles/[vehicleId]/route.ts @@ -0,0 +1,9 @@ +import { parseParams, route } from "@/server/http/route"; +import { stageVehicleParamsSchema } from "@/server/modules/admin/admin.schemas"; +import { removeVehicleFromStage } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../../../../_shared"; + +export const DELETE = route<{ stageId: string; vehicleId: string }>(adminOnly, ({ params }) => { + const { stageId, vehicleId } = parseParams(params, stageVehicleParamsSchema); + return removeVehicleFromStage(stageId, vehicleId); +}); diff --git a/src/app/api/admin/stages/assign-vehicle/route.ts b/src/app/api/admin/stages/assign-vehicle/route.ts new file mode 100644 index 0000000..eaf2073 --- /dev/null +++ b/src/app/api/admin/stages/assign-vehicle/route.ts @@ -0,0 +1,9 @@ +import { parseBody, route } from "@/server/http/route"; +import { assignVehicleSchema } from "@/server/modules/admin/admin.schemas"; +import { assignVehicleToStage } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../../_shared"; + +export const POST = route(adminOnly, async ({ req }) => { + const { stageId, vehicleId } = await parseBody(req, assignVehicleSchema); + return assignVehicleToStage(stageId, vehicleId); +}); diff --git a/src/app/api/admin/stages/route.ts b/src/app/api/admin/stages/route.ts new file mode 100644 index 0000000..36f45d4 --- /dev/null +++ b/src/app/api/admin/stages/route.ts @@ -0,0 +1,8 @@ +import { json, parseBody, parseQuery, route } from "@/server/http/route"; +import { createStageSchema, paginationQuerySchema } from "@/server/modules/admin/admin.schemas"; +import { createStage, listStagesWithVehicles } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../_shared"; + +export const GET = route(adminOnly, ({ req }) => listStagesWithVehicles(parseQuery(req, paginationQuerySchema))); + +export const POST = route(adminOnly, async ({ req }) => json(await createStage(await parseBody(req, createStageSchema)), 201)); diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts new file mode 100644 index 0000000..d034d56 --- /dev/null +++ b/src/app/api/admin/users/[id]/route.ts @@ -0,0 +1,17 @@ +import { parseBody, parseParams, route } from "@/server/http/route"; +import { idParamSchema, updateUserSchema } from "@/server/modules/admin/admin.schemas"; +import { deleteUser, setUserDisabled } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../../_shared"; + +type Params = { id: string }; + +/** Block / unblock. */ +export const PUT = route(adminOnly, async ({ req, params, user }) => { + const { id } = parseParams(params, idParamSchema); + const { disabled } = await parseBody(req, updateUserSchema); + return setUserDisabled(user.id, id, disabled); +}); + +export const DELETE = route(adminOnly, ({ params, user }) => + deleteUser(user.id, parseParams(params, idParamSchema).id), +); diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..810d92f --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,8 @@ +import { json, parseBody, parseQuery, route } from "@/server/http/route"; +import { createUserSchema, listUsersQuerySchema } from "@/server/modules/admin/admin.schemas"; +import { createUser, listUsers } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../_shared"; + +export const GET = route(adminOnly, ({ req, user }) => listUsers(user.id, parseQuery(req, listUsersQuerySchema))); + +export const POST = route(adminOnly, async ({ req }) => json(await createUser(await parseBody(req, createUserSchema)), 201)); diff --git a/src/app/api/admin/vehicles/[id]/route.ts b/src/app/api/admin/vehicles/[id]/route.ts new file mode 100644 index 0000000..60f47ab --- /dev/null +++ b/src/app/api/admin/vehicles/[id]/route.ts @@ -0,0 +1,6 @@ +import { parseParams, route } from "@/server/http/route"; +import { idParamSchema } from "@/server/modules/admin/admin.schemas"; +import { deleteVehicle } from "@/server/modules/admin/admin.service"; +import { adminOnly } from "../../_shared"; + +export const DELETE = route<{ id: string }>(adminOnly, ({ params }) => deleteVehicle(parseParams(params, idParamSchema).id)); diff --git a/src/app/api/admin/vehicles/route.ts b/src/app/api/admin/vehicles/route.ts new file mode 100644 index 0000000..3c20e19 --- /dev/null +++ b/src/app/api/admin/vehicles/route.ts @@ -0,0 +1,9 @@ +import { json, parseBody, parseQuery, route } from "@/server/http/route"; +import { createVehicleSchema, paginationQuerySchema } from "@/server/modules/admin/admin.schemas"; +import { listVehicles } from "@/server/modules/admin/admin.service"; +import { createVehicle } from "@/server/modules/vehicles/vehicles"; +import { adminOnly } from "../_shared"; + +export const GET = route(adminOnly, ({ req }) => listVehicles(parseQuery(req, paginationQuerySchema))); + +export const POST = route(adminOnly, async ({ req }) => json(await createVehicle(await parseBody(req, createVehicleSchema)), 201)); diff --git a/src/app/api/auth/2fa/disable/route.ts b/src/app/api/auth/2fa/disable/route.ts new file mode 100644 index 0000000..97aebbe --- /dev/null +++ b/src/app/api/auth/2fa/disable/route.ts @@ -0,0 +1,8 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { disableTwoFactor } from "@/server/modules/auth/auth.service"; + +export const POST = route({ auth: "any", rateLimit: rateLimits.auth }, async ({ user }) => { + await disableTwoFactor(user.id); + return { message: "2FA disabled successfully" }; +}); diff --git a/src/app/api/auth/2fa/login/route.ts b/src/app/api/auth/2fa/login/route.ts new file mode 100644 index 0000000..a825d7e --- /dev/null +++ b/src/app/api/auth/2fa/login/route.ts @@ -0,0 +1,13 @@ +import { setRefreshCookie } from "@/server/auth/cookies"; +import { rateLimits } from "@/server/http/rate-limit"; +import { json, parseBody, route } from "@/server/http/route"; +import { twoFactorLoginSchema } from "@/server/modules/auth/auth.schemas"; +import { completeTwoFactorLogin } from "@/server/modules/auth/auth.service"; + +export const POST = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const { tempToken, token } = await parseBody(req, twoFactorLoginSchema); + const session = await completeTwoFactorLogin(tempToken, token); + const res = json({ message: "Login successful", accessToken: session.accessToken, role: session.role }); + setRefreshCookie(res, session.refreshToken); + return res; +}); diff --git a/src/app/api/auth/2fa/setup/route.ts b/src/app/api/auth/2fa/setup/route.ts new file mode 100644 index 0000000..f09fa89 --- /dev/null +++ b/src/app/api/auth/2fa/setup/route.ts @@ -0,0 +1,5 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { beginTwoFactorSetup } from "@/server/modules/auth/auth.service"; + +export const POST = route({ auth: "any", rateLimit: rateLimits.auth }, ({ user }) => beginTwoFactorSetup(user.id)); diff --git a/src/app/api/auth/2fa/verify/route.ts b/src/app/api/auth/2fa/verify/route.ts new file mode 100644 index 0000000..38c3565 --- /dev/null +++ b/src/app/api/auth/2fa/verify/route.ts @@ -0,0 +1,10 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { parseBody, route } from "@/server/http/route"; +import { verifyTwoFactorSchema } from "@/server/modules/auth/auth.schemas"; +import { confirmTwoFactorSetup } from "@/server/modules/auth/auth.service"; + +export const POST = route({ auth: "any", rateLimit: rateLimits.auth }, async ({ req, user }) => { + const { token } = await parseBody(req, verifyTwoFactorSchema); + await confirmTwoFactorSetup(user.id, token); + return { message: "2FA enabled successfully" }; +}); diff --git a/src/app/api/auth/google/callback/route.ts b/src/app/api/auth/google/callback/route.ts new file mode 100644 index 0000000..e29ab9f --- /dev/null +++ b/src/app/api/auth/google/callback/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { setRefreshCookie } from "@/server/auth/cookies"; +import { OAUTH_STATE_COOKIE, exchangeGoogleCode } from "@/server/auth/google"; +import { HttpError } from "@/server/http/errors"; +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { loginWithGoogle } from "@/server/modules/auth/auth.service"; + +export const GET = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const failure = (reason = "auth_failed") => { + const res = NextResponse.redirect(new URL(`/login?error=${reason}`, req.url)); + res.cookies.delete({ name: OAUTH_STATE_COOKIE, path: "/api/auth/google" }); + return res; + }; + + const code = req.nextUrl.searchParams.get("code"); + const state = req.nextUrl.searchParams.get("state"); + const expectedState = req.cookies.get(OAUTH_STATE_COOKIE)?.value; + if (!code || !state || !expectedState || state !== expectedState) return failure(); + + try { + const session = await loginWithGoogle(await exchangeGoogleCode(code)); + // The token travels in the fragment, which browsers never send to servers or put in logs. + const target = new URL("/auth/success", req.url); + target.hash = new URLSearchParams({ accessToken: session.accessToken, role: session.role }).toString(); + const res = NextResponse.redirect(target); + res.cookies.delete({ name: OAUTH_STATE_COOKIE, path: "/api/auth/google" }); + setRefreshCookie(res, session.refreshToken); + return res; + } catch (error) { + if (error instanceof HttpError && error.status === 403) return failure("account_disabled"); + console.error("[auth] Google sign-in failed", error); + return failure(); + } +}); diff --git a/src/app/api/auth/google/route.ts b/src/app/api/auth/google/route.ts new file mode 100644 index 0000000..92aedb9 --- /dev/null +++ b/src/app/api/auth/google/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { OAUTH_STATE_COOKIE, buildGoogleAuthUrl, googleCredentials } from "@/server/auth/google"; +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; + +/** Starts Google sign-in. A random `state` cookie protects the callback against CSRF. */ +export const GET = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const creds = googleCredentials(); + if (!creds) return NextResponse.redirect(new URL("/login?error=google_unavailable", req.url)); + + const state = crypto.randomUUID(); + const res = NextResponse.redirect(buildGoogleAuthUrl(creds.clientId, creds.redirectUri, state)); + res.cookies.set(OAUTH_STATE_COOKIE, state, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/api/auth/google", + maxAge: 600, + }); + return res; +}); diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..fc14c99 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,15 @@ +import { setRefreshCookie } from "@/server/auth/cookies"; +import { rateLimits } from "@/server/http/rate-limit"; +import { json, parseBody, route } from "@/server/http/route"; +import { loginSchema } from "@/server/modules/auth/auth.schemas"; +import { login } from "@/server/modules/auth/auth.service"; + +export const POST = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const result = await login(await parseBody(req, loginSchema)); + if (result.twoFactorRequired) { + return { twoFactorRequired: true, tempToken: result.tempToken }; + } + const res = json({ message: "Login successful", accessToken: result.accessToken, role: result.role }); + setRefreshCookie(res, result.refreshToken); + return res; +}); diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..8fe8f5e --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,17 @@ +import { REFRESH_COOKIE, clearRefreshCookie } from "@/server/auth/cookies"; +import { rateLimits } from "@/server/http/rate-limit"; +import { json, route } from "@/server/http/route"; +import { logout } from "@/server/modules/auth/auth.service"; + +/** + * Revokes the presented access + refresh tokens and clears the cookie. Works with an expired + * access token too, so a user can always log out. + */ +export const POST = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const header = req.headers.get("authorization"); + const accessToken = header?.startsWith("Bearer ") ? header.slice(7) : undefined; + await logout(accessToken, req.cookies.get(REFRESH_COOKIE)?.value); + const res = json({ message: "Logged out successfully." }); + clearRefreshCookie(res); + return res; +}); diff --git a/src/app/api/auth/me/admin/route.ts b/src/app/api/auth/me/admin/route.ts new file mode 100644 index 0000000..dc6d659 --- /dev/null +++ b/src/app/api/auth/me/admin/route.ts @@ -0,0 +1,6 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { getProfile } from "@/server/modules/auth/auth.service"; + +/** Kept for API compatibility: profile, but only for Admins. Prefer GET /api/auth/me. */ +export const GET = route({ auth: "Admin", rateLimit: rateLimits.auth }, ({ user }) => getProfile(user.id)); diff --git a/src/app/api/auth/me/mat-admin/route.ts b/src/app/api/auth/me/mat-admin/route.ts new file mode 100644 index 0000000..a98025a --- /dev/null +++ b/src/app/api/auth/me/mat-admin/route.ts @@ -0,0 +1,6 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { getProfile } from "@/server/modules/auth/auth.service"; + +/** Kept for API compatibility: profile, but only for Matatu admins. Prefer GET /api/auth/me. */ +export const GET = route({ auth: "MatAdmin", rateLimit: rateLimits.auth }, ({ user }) => getProfile(user.id)); diff --git a/src/app/api/auth/me/passenger/route.ts b/src/app/api/auth/me/passenger/route.ts new file mode 100644 index 0000000..d8d0384 --- /dev/null +++ b/src/app/api/auth/me/passenger/route.ts @@ -0,0 +1,6 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { getProfile } from "@/server/modules/auth/auth.service"; + +/** Kept for API compatibility: profile, but only for Passengers. Prefer GET /api/auth/me. */ +export const GET = route({ auth: "Passenger", rateLimit: rateLimits.auth }, ({ user }) => getProfile(user.id)); diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..f2e7735 --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,12 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { parseBody, route } from "@/server/http/route"; +import { updateProfileSchema } from "@/server/modules/auth/auth.schemas"; +import { getProfile, updateProfile } from "@/server/modules/auth/auth.service"; + +/** Current user's profile (any role). */ +export const GET = route({ auth: "any", rateLimit: rateLimits.auth }, ({ user }) => getProfile(user.id)); + +export const PUT = route({ auth: "any", rateLimit: rateLimits.auth }, async ({ req, user }) => { + await updateProfile(user.id, await parseBody(req, updateProfileSchema)); + return { message: "Profile updated successfully" }; +}); diff --git a/src/app/api/auth/refresh-token/route.ts b/src/app/api/auth/refresh-token/route.ts new file mode 100644 index 0000000..cecdb56 --- /dev/null +++ b/src/app/api/auth/refresh-token/route.ts @@ -0,0 +1,14 @@ +import { REFRESH_COOKIE, setRefreshCookie } from "@/server/auth/cookies"; +import { rateLimits } from "@/server/http/rate-limit"; +import { json, route } from "@/server/http/route"; +import { refreshSession } from "@/server/modules/auth/auth.service"; + +/** Rotates the refresh cookie and returns a new access token. */ +export const POST = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const header = req.headers.get("authorization"); + const accessToken = header?.startsWith("Bearer ") ? header.slice(7) : undefined; + const session = await refreshSession(req.cookies.get(REFRESH_COOKIE)?.value, accessToken); + const res = json({ accessToken: session.accessToken, role: session.role }); + setRefreshCookie(res, session.refreshToken); + return res; +}); diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..d03df1d --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,9 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { json, parseBody, route } from "@/server/http/route"; +import { registerSchema } from "@/server/modules/auth/auth.schemas"; +import { register } from "@/server/modules/auth/auth.service"; + +export const POST = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const user = await register(await parseBody(req, registerSchema)); + return json({ message: "User created successfully", user }, 201); +}); diff --git a/src/app/api/auth/requestPasswordReset/route.ts b/src/app/api/auth/requestPasswordReset/route.ts new file mode 100644 index 0000000..b92afec --- /dev/null +++ b/src/app/api/auth/requestPasswordReset/route.ts @@ -0,0 +1,9 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { parseBody, route } from "@/server/http/route"; +import { requestPasswordResetSchema } from "@/server/modules/auth/auth.schemas"; +import { requestPasswordReset } from "@/server/modules/auth/auth.service"; + +export const POST = route({ rateLimit: rateLimits.auth }, async ({ req }) => { + const { email } = await parseBody(req, requestPasswordResetSchema); + return { message: await requestPasswordReset(email) }; +}); diff --git a/src/app/api/auth/resetpassword/[id]/[token]/route.ts b/src/app/api/auth/resetpassword/[id]/[token]/route.ts new file mode 100644 index 0000000..c056556 --- /dev/null +++ b/src/app/api/auth/resetpassword/[id]/[token]/route.ts @@ -0,0 +1,11 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { parseBody, parseParams, route } from "@/server/http/route"; +import { resetPasswordBodySchema, resetPasswordParamsSchema } from "@/server/modules/auth/auth.schemas"; +import { resetPassword } from "@/server/modules/auth/auth.service"; + +export const POST = route<{ id: string; token: string }>({ rateLimit: rateLimits.auth }, async ({ req, params }) => { + const { id, token } = parseParams(params, resetPasswordParamsSchema); + const { password } = await parseBody(req, resetPasswordBodySchema); + await resetPassword(id, token, password); + return { message: "Password has been reset" }; +}); diff --git a/src/app/api/cron/eta-cycle/route.ts b/src/app/api/cron/eta-cycle/route.ts new file mode 100644 index 0000000..29c8df6 --- /dev/null +++ b/src/app/api/cron/eta-cycle/route.ts @@ -0,0 +1,44 @@ +import { timingSafeEqual } from "node:crypto"; +import { env } from "@/server/env"; +import { errorResponse, json } from "@/server/http/route"; +import { runEtaCycle } from "@/server/jobs/eta-cycle"; + +export const dynamic = "force-dynamic"; +export const maxDuration = 60; + +function authorised(header: string | null, secret: string | undefined): boolean { + if (!secret || !header?.startsWith("Bearer ")) return false; + const given = Buffer.from(header.slice(7)); + const expected = Buffer.from(secret); + return given.length === expected.length && timingSafeEqual(given, expected); +} + +const CYCLE_SPACING_MS = 30_000; + +/** + * Runs the ETA cycle. Called by Vercel Cron (GET, `Authorization: Bearer $CRON_SECRET`) or any + * external scheduler. Overlapping calls are harmless: the job lease makes extra runs skip. + * + * Cron schedules have one-minute granularity, so `?runs=2` runs a second cycle 30 s after the first + * to keep the original 30-second cadence from a once-a-minute trigger. + */ +async function handler(req: Request) { + if (!authorised(req.headers.get("authorization"), env().CRON_SECRET)) { + return json({ message: "Unauthorized" }, 401); + } + const runs = Math.min(Math.max(Number(new URL(req.url).searchParams.get("runs")) || 1, 1), 2); + try { + const startedAt = Date.now(); + const reports = [await runEtaCycle()]; + for (let i = 1; i < runs; i++) { + const wait = startedAt + i * CYCLE_SPACING_MS - Date.now(); + if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait)); + reports.push(await runEtaCycle()); + } + return json({ runs: reports }); + } catch (error) { + return errorResponse(error); + } +} + +export { handler as GET, handler as POST }; diff --git a/src/app/api/eta/[stageId]/route.ts b/src/app/api/eta/[stageId]/route.ts new file mode 100644 index 0000000..a1df23e --- /dev/null +++ b/src/app/api/eta/[stageId]/route.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; +import { rateLimits } from "@/server/http/rate-limit"; +import { parseParams, route } from "@/server/http/route"; +import { getStageEta } from "@/server/modules/eta/eta.service"; +import { id } from "@/server/validation/common"; + +const paramsSchema = z.object({ stageId: id("Stage ID") }); + +/** Live ETAs for the vehicles serving a stage. */ +export const GET = route<{ stageId: string }>({ auth: "any", rateLimit: rateLimits.api }, ({ params }) => + getStageEta(parseParams(params, paramsSchema).stageId), +); diff --git a/src/app/api/feedback/route.ts b/src/app/api/feedback/route.ts new file mode 100644 index 0000000..44ea652 --- /dev/null +++ b/src/app/api/feedback/route.ts @@ -0,0 +1,7 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { json, parseBody, route } from "@/server/http/route"; +import { submitFeedback, submitFeedbackSchema } from "@/server/modules/feedback/feedback.service"; + +export const POST = route({ auth: "Passenger", rateLimit: rateLimits.api }, async ({ req, user }) => + json(await submitFeedback(user.id, await parseBody(req, submitFeedbackSchema)), 201), +); diff --git a/src/app/api/feedback/vehicles/route.ts b/src/app/api/feedback/vehicles/route.ts new file mode 100644 index 0000000..c29ee26 --- /dev/null +++ b/src/app/api/feedback/vehicles/route.ts @@ -0,0 +1,5 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { listVehiclesForFeedback } from "@/server/modules/feedback/feedback.service"; + +export const GET = route({ auth: "Passenger", rateLimit: rateLimits.api }, () => listVehiclesForFeedback()); diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..66537e3 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,15 @@ +import { db } from "@/server/db"; +import { json } from "@/server/http/route"; + +export const dynamic = "force-dynamic"; + +/** Liveness + database reachability, for uptime checks and deploy smoke tests. */ +export async function GET() { + try { + await db.$queryRaw`SELECT 1`; + return json({ status: "ok", database: "ok" }); + } catch (error) { + console.error("[health] database check failed", error); + return json({ status: "degraded", database: "unreachable" }, 503); + } +} diff --git a/src/app/api/mat-admin/_shared.ts b/src/app/api/mat-admin/_shared.ts new file mode 100644 index 0000000..e0ddb77 --- /dev/null +++ b/src/app/api/mat-admin/_shared.ts @@ -0,0 +1,4 @@ +import { rateLimits } from "@/server/http/rate-limit"; + +export const matAdminOnly = { auth: "MatAdmin", rateLimit: rateLimits.admin } as const; +export const matAdminDashboard = { auth: "MatAdmin", rateLimit: [rateLimits.admin, rateLimits.dashboard] } as const; diff --git a/src/app/api/mat-admin/dashboard-data/route.ts b/src/app/api/mat-admin/dashboard-data/route.ts new file mode 100644 index 0000000..f2cbe9c --- /dev/null +++ b/src/app/api/mat-admin/dashboard-data/route.ts @@ -0,0 +1,5 @@ +import { route } from "@/server/http/route"; +import { dashboardData } from "@/server/modules/mat-admin/mat-admin.service"; +import { matAdminDashboard } from "../_shared"; + +export const GET = route(matAdminDashboard, ({ user }) => dashboardData(user.id)); diff --git a/src/app/api/mat-admin/dashboard-summary/route.ts b/src/app/api/mat-admin/dashboard-summary/route.ts new file mode 100644 index 0000000..aa9d6b8 --- /dev/null +++ b/src/app/api/mat-admin/dashboard-summary/route.ts @@ -0,0 +1,5 @@ +import { route } from "@/server/http/route"; +import { dashboardSummary } from "@/server/modules/mat-admin/mat-admin.service"; +import { matAdminDashboard } from "../_shared"; + +export const GET = route(matAdminDashboard, ({ user }) => dashboardSummary(user.id)); diff --git a/src/app/api/mat-admin/fleet-aggregate-data/route.ts b/src/app/api/mat-admin/fleet-aggregate-data/route.ts new file mode 100644 index 0000000..49f2493 --- /dev/null +++ b/src/app/api/mat-admin/fleet-aggregate-data/route.ts @@ -0,0 +1,5 @@ +import { parseQuery, route } from "@/server/http/route"; +import { fleetAggregate, periodQuerySchema } from "@/server/modules/mat-admin/mat-admin.service"; +import { matAdminOnly } from "../_shared"; + +export const GET = route(matAdminOnly, ({ req, user }) => fleetAggregate(user.id, parseQuery(req, periodQuerySchema).period)); diff --git a/src/app/api/mat-admin/vehicle-stats/route.ts b/src/app/api/mat-admin/vehicle-stats/route.ts new file mode 100644 index 0000000..083de1a --- /dev/null +++ b/src/app/api/mat-admin/vehicle-stats/route.ts @@ -0,0 +1,6 @@ +import { parseQuery, route } from "@/server/http/route"; +import { statsQuerySchema, vehicleStats } from "@/server/modules/mat-admin/mat-admin.service"; +import { matAdminOnly } from "../_shared"; + +/** ?date=YYYY-MM-DD (UTC) — defaults to today. */ +export const GET = route(matAdminOnly, ({ req, user }) => vehicleStats(user.id, parseQuery(req, statsQuerySchema).date)); diff --git a/src/app/api/mat-admin/vehicles/[vehicleId]/chart-data/route.ts b/src/app/api/mat-admin/vehicles/[vehicleId]/chart-data/route.ts new file mode 100644 index 0000000..eb96bc8 --- /dev/null +++ b/src/app/api/mat-admin/vehicles/[vehicleId]/chart-data/route.ts @@ -0,0 +1,7 @@ +import { parseParams, parseQuery, route } from "@/server/http/route"; +import { periodQuerySchema, vehicleChartData, vehicleIdParamSchema } from "@/server/modules/mat-admin/mat-admin.service"; +import { matAdminOnly } from "../../../_shared"; + +export const GET = route<{ vehicleId: string }>(matAdminOnly, ({ req, params, user }) => + vehicleChartData(user.id, parseParams(params, vehicleIdParamSchema).vehicleId, parseQuery(req, periodQuerySchema).period), +); diff --git a/src/app/api/mat-admin/vehicles/route.ts b/src/app/api/mat-admin/vehicles/route.ts new file mode 100644 index 0000000..f48dc08 --- /dev/null +++ b/src/app/api/mat-admin/vehicles/route.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; +import { json, parseBody, route } from "@/server/http/route"; +import { createVehicleSchema } from "@/server/modules/admin/admin.schemas"; +import { myVehicles } from "@/server/modules/mat-admin/mat-admin.service"; +import { createVehicle } from "@/server/modules/vehicles/vehicles"; +import { matAdminOnly } from "../_shared"; + +const bodySchema = createVehicleSchema.pick({ carId: true, plateNumber: true }); + +export const GET = route(matAdminOnly, ({ user }) => myVehicles(user.id)); + +/** A Matatu admin's new vehicle is always owned by them. */ +export const POST = route(matAdminOnly, async ({ req, user }) => { + const input: z.infer = await parseBody(req, bodySchema); + return json(await createVehicle({ ...input, ownerId: user.id }), 201); +}); diff --git a/src/app/api/stages/[stageId]/subscribe/route.ts b/src/app/api/stages/[stageId]/subscribe/route.ts new file mode 100644 index 0000000..b1b2ae8 --- /dev/null +++ b/src/app/api/stages/[stageId]/subscribe/route.ts @@ -0,0 +1,8 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { parseParams, route } from "@/server/http/route"; +import { subscribe } from "@/server/modules/stages/stages.service"; +import { stageParamsSchema, type StageParams } from "../../params"; + +export const POST = route({ auth: "any", rateLimit: rateLimits.subscription }, ({ params, user }) => + subscribe(user.id, parseParams(params, stageParamsSchema).stageId), +); diff --git a/src/app/api/stages/[stageId]/subscription/route.ts b/src/app/api/stages/[stageId]/subscription/route.ts new file mode 100644 index 0000000..3e2ba34 --- /dev/null +++ b/src/app/api/stages/[stageId]/subscription/route.ts @@ -0,0 +1,8 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { parseParams, route } from "@/server/http/route"; +import { getSubscriptionStatus } from "@/server/modules/stages/stages.service"; +import { stageParamsSchema, type StageParams } from "../../params"; + +export const GET = route({ auth: "any", rateLimit: rateLimits.api }, ({ params, user }) => + getSubscriptionStatus(user.id, parseParams(params, stageParamsSchema).stageId), +); diff --git a/src/app/api/stages/[stageId]/unsubscribe/route.ts b/src/app/api/stages/[stageId]/unsubscribe/route.ts new file mode 100644 index 0000000..cbc599b --- /dev/null +++ b/src/app/api/stages/[stageId]/unsubscribe/route.ts @@ -0,0 +1,8 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { parseParams, route } from "@/server/http/route"; +import { unsubscribe } from "@/server/modules/stages/stages.service"; +import { stageParamsSchema, type StageParams } from "../../params"; + +export const POST = route({ auth: "any", rateLimit: rateLimits.subscription }, ({ params, user }) => + unsubscribe(user.id, parseParams(params, stageParamsSchema).stageId), +); diff --git a/src/app/api/stages/params.ts b/src/app/api/stages/params.ts new file mode 100644 index 0000000..0f1fe0e --- /dev/null +++ b/src/app/api/stages/params.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; +import { id } from "@/server/validation/common"; + +export const stageParamsSchema = z.object({ stageId: id("Stage ID") }); +export type StageParams = { stageId: string }; diff --git a/src/app/api/stages/route.ts b/src/app/api/stages/route.ts new file mode 100644 index 0000000..5a81ef8 --- /dev/null +++ b/src/app/api/stages/route.ts @@ -0,0 +1,5 @@ +import { rateLimits } from "@/server/http/rate-limit"; +import { route } from "@/server/http/route"; +import { listStages } from "@/server/modules/stages/stages.service"; + +export const GET = route({ auth: "any", rateLimit: rateLimits.api }, () => listStages()); diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 0000000..3cef616 --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,22 @@ +/** + * Next.js instrumentation hook — runs once per server process. + * + * ETA_CYCLE_MODE: + * interval → run the ETA cycle every 30 s in this process (default when self-hosting) + * cron → do nothing here; a scheduler calls /api/cron/eta-cycle (default on Vercel) + * off → disabled + */ +export async function register() { + if (process.env.NEXT_RUNTIME !== "nodejs") return; + if (process.env.NEXT_PHASE === "phase-production-build") return; + + const mode = process.env.ETA_CYCLE_MODE ?? (process.env.VERCEL ? "cron" : "interval"); + if (mode !== "interval") return; + if (!process.env.DATABASE_URL) { + console.warn("[eta-cycle] DATABASE_URL not set — in-process scheduler not started."); + return; + } + + const { startEtaScheduler } = await import("./server/jobs/scheduler"); + startEtaScheduler(); +} diff --git a/src/server/auth/context.ts b/src/server/auth/context.ts new file mode 100644 index 0000000..1e24b51 --- /dev/null +++ b/src/server/auth/context.ts @@ -0,0 +1,14 @@ +import "server-only"; +import { env } from "@/server/env"; +import { createTokenService, type TokenService } from "./tokens"; + +let service: TokenService | undefined; + +/** Token service bound to the configured secrets. */ +export function tokens(): TokenService { + if (!service) { + const { JWT_SECRET, JWT_REFRESH_SECRET } = env(); + service = createTokenService({ accessSecret: JWT_SECRET, refreshSecret: JWT_REFRESH_SECRET }); + } + return service; +} diff --git a/src/server/auth/cookies.ts b/src/server/auth/cookies.ts new file mode 100644 index 0000000..81941a7 --- /dev/null +++ b/src/server/auth/cookies.ts @@ -0,0 +1,22 @@ +import "server-only"; +import type { NextResponse } from "next/server"; +import { REFRESH_TOKEN_TTL_SECONDS } from "./tokens"; + +export const REFRESH_COOKIE = "refreshToken"; + +const baseOptions = () => ({ + httpOnly: true, + secure: process.env.NODE_ENV === "production", + // Same-site app: Lax is enough and, unlike the old `SameSite=None`, isn't sent on cross-site POSTs. + sameSite: "lax" as const, + // Only the auth endpoints ever need to read it. + path: "/api/auth", +}); + +export function setRefreshCookie(res: NextResponse, token: string) { + res.cookies.set(REFRESH_COOKIE, token, { ...baseOptions(), maxAge: REFRESH_TOKEN_TTL_SECONDS }); +} + +export function clearRefreshCookie(res: NextResponse) { + res.cookies.set(REFRESH_COOKIE, "", { ...baseOptions(), maxAge: 0 }); +} diff --git a/src/server/auth/google.ts b/src/server/auth/google.ts new file mode 100644 index 0000000..cffad75 --- /dev/null +++ b/src/server/auth/google.ts @@ -0,0 +1,75 @@ +import "server-only"; +import { z } from "zod"; +import { env } from "@/server/env"; +import type { GoogleProfile } from "@/server/modules/auth/auth.service"; + +/** + * Minimal Google OAuth 2.0 / OpenID Connect authorisation-code flow (replaces passport-google-oauth20). + */ + +const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; +const TOKEN_URL = "https://oauth2.googleapis.com/token"; +const USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"; + +export const OAUTH_STATE_COOKIE = "oauth_state"; + +export function googleCredentials() { + const { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, APP_URL } = env(); + if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) return null; + return { + clientId: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + redirectUri: new URL("/api/auth/google/callback", APP_URL).toString(), + }; +} + +export function buildGoogleAuthUrl(clientId: string, redirectUri: string, state: string): string { + const url = new URL(AUTH_URL); + url.search = new URLSearchParams({ + client_id: clientId, + redirect_uri: redirectUri, + response_type: "code", + scope: "openid email profile", + state, + prompt: "select_account", + }).toString(); + return url.toString(); +} + +const userInfoSchema = z.object({ + sub: z.string(), + email: z.string().email(), + email_verified: z.boolean().optional(), + name: z.string().optional(), + picture: z.string().url().optional(), +}); + +export async function exchangeGoogleCode(code: string): Promise { + const creds = googleCredentials(); + if (!creds) throw new Error("Google OAuth is not configured"); + + const tokenRes = await fetch(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + code, + client_id: creds.clientId, + client_secret: creds.clientSecret, + redirect_uri: creds.redirectUri, + grant_type: "authorization_code", + }), + signal: AbortSignal.timeout(10_000), + }); + if (!tokenRes.ok) throw new Error(`Google token exchange failed: HTTP ${tokenRes.status}`); + const { access_token } = z.object({ access_token: z.string() }).parse(await tokenRes.json()); + + const infoRes = await fetch(USERINFO_URL, { + headers: { Authorization: `Bearer ${access_token}` }, + signal: AbortSignal.timeout(10_000), + }); + if (!infoRes.ok) throw new Error(`Google userinfo failed: HTTP ${infoRes.status}`); + const info = userInfoSchema.parse(await infoRes.json()); + if (info.email_verified === false) throw new Error("Google email address is not verified"); + + return { sub: info.sub, email: info.email, name: info.name ?? info.email, picture: info.picture }; +} diff --git a/src/server/auth/guard.ts b/src/server/auth/guard.ts new file mode 100644 index 0000000..ae1edac --- /dev/null +++ b/src/server/auth/guard.ts @@ -0,0 +1,54 @@ +import "server-only"; +import type { Role } from "@/generated/prisma/enums"; +import { db } from "@/server/db"; +import { HttpError, forbidden, unauthorized } from "@/server/http/errors"; +import { tokens } from "./context"; +import { TokenError, issuedBeforeInvalidation, type AccessClaims } from "./tokens"; + +/** Extracts the bearer token, or throws the same 401/400 responses the Express middleware returned. */ +export function bearerToken(headers: Headers): string { + const header = headers.get("authorization"); + if (!header) throw unauthorized("Access denied. No token provided."); + const [scheme, token, ...rest] = header.split(" "); + if (scheme !== "Bearer" || !token || rest.length) throw new HttpError(400, "Invalid token format"); + return token; +} + +/** + * Verifies the access token and checks it against the claims-based rules: + * disabled flag, tokensValidFrom invalidation, required role, and the revocation list (logout). + */ +export async function authenticate(headers: Headers, role?: Role): Promise { + const token = bearerToken(headers); + + let claims: AccessClaims; + try { + claims = await tokens().verifyAccess(token); + } catch (error) { + if (error instanceof TokenError && error.reason === "expired") throw unauthorized("Token expired"); + throw unauthorized("Invalid token"); + } + + if (claims.disabled) throw forbidden("Account disabled"); + if (role && claims.role !== role) throw forbidden(); + + // The Express version only compared the token against its *own* tokensValidFrom claim, which can + // never change, so blocking a user or resetting a password didn't affect live access tokens. + // Checking the current row makes both take effect immediately. + const [user, revoked] = await Promise.all([ + db.user.findUnique({ + where: { id: claims.id }, + select: { disabled: true, tokensValidFrom: true, role: true }, + }), + db.revokedToken.findUnique({ where: { jti: claims.jti }, select: { jti: true } }), + ]); + + if (!user || revoked) throw unauthorized("Token has been invalidated"); + if (user.disabled) throw forbidden("Account disabled"); + if (user.role !== claims.role) throw unauthorized("Token has been invalidated"); + if (issuedBeforeInvalidation(claims.iat, user.tokensValidFrom.getTime())) { + throw unauthorized("Token has been invalidated"); + } + + return claims; +} diff --git a/src/server/auth/password.ts b/src/server/auth/password.ts new file mode 100644 index 0000000..8f4f9df --- /dev/null +++ b/src/server/auth/password.ts @@ -0,0 +1,10 @@ +import bcrypt from "bcryptjs"; + +/** bcryptjs produces/accepts the same `$2a$`/`$2b$` hashes as the native `bcrypt` package used before. */ +const ROUNDS = 10; + +export const hashPassword = (plain: string) => bcrypt.hash(plain, ROUNDS); +export const verifyPassword = (plain: string, hash: string) => bcrypt.compare(plain, hash); + +/** Spend roughly the same time as a real hash so response timing doesn't reveal whether an account exists. */ +export const dummyPasswordWork = () => bcrypt.hash(crypto.randomUUID(), ROUNDS); diff --git a/src/server/auth/tokens.ts b/src/server/auth/tokens.ts new file mode 100644 index 0000000..a19e01e --- /dev/null +++ b/src/server/auth/tokens.ts @@ -0,0 +1,127 @@ +import { SignJWT, errors as joseErrors, jwtVerify, type JWTPayload } from "jose"; +import type { Role } from "@/generated/prisma/enums"; + +/** + * JWT handling. + * + * Every token carries a `typ` claim so one kind can never be replayed as another. (The original + * app signed the 2FA "temp token" with the access-token secret and no type, so it was accepted + * as a full access token — bypassing the second factor.) + */ + +export const ACCESS_TOKEN_TTL = "15m"; +export const REFRESH_TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60; +export const TWO_FACTOR_TOKEN_TTL = "5m"; +export const RESET_TOKEN_TTL = "1h"; + +export type AccessClaims = { + typ: "access"; + id: number; + role: Role; + disabled: boolean; + /** Epoch milliseconds; tokens issued before this are rejected. */ + tokensValidFrom: number; + jti: string; + iat: number; + exp: number; +}; + +export type RefreshClaims = { typ: "refresh"; id: number; jti: string; iat: number; exp: number }; +export type TwoFactorClaims = { typ: "2fa"; id: number; iat: number; exp: number }; +export type ResetClaims = { typ: "reset"; id: number; email: string; iat: number; exp: number }; + +export type TokenUser = { + id: number; + role: Role; + disabled: boolean; + tokensValidFrom: Date; +}; + +export class TokenError extends Error { + constructor( + readonly reason: "expired" | "invalid" | "wrong-type", + message: string, + ) { + super(message); + this.name = "TokenError"; + } +} + +const encoder = new TextEncoder(); +const key = (secret: string) => encoder.encode(secret); +const newJti = () => crypto.randomUUID(); + +async function sign(payload: JWTPayload, secret: string, expiresIn: string | number, jti?: string) { + let jwt = new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt(); + jwt = typeof expiresIn === "number" ? jwt.setExpirationTime(Math.floor(Date.now() / 1000) + expiresIn) : jwt.setExpirationTime(expiresIn); + if (jti) jwt = jwt.setJti(jti); + return jwt.sign(key(secret)); +} + +async function verify( + token: string, + secret: string, + typ: T["typ"], + { allowExpired = false } = {}, +): Promise { + let payload: JWTPayload; + try { + ({ payload } = await jwtVerify(token, key(secret), { algorithms: ["HS256"] })); + } catch (error) { + // jose verifies the signature before checking `exp`, so an expired token's payload is authentic. + if (allowExpired && error instanceof joseErrors.JWTExpired) { + payload = error.payload; + } else if (error instanceof joseErrors.JWTExpired) { + throw new TokenError("expired", "Token expired"); + } else { + throw new TokenError("invalid", "Invalid token"); + } + } + if (payload.typ !== typ) throw new TokenError("wrong-type", "Invalid token type"); + return payload as unknown as T; +} + +/** True when the token was issued before the user's tokens were invalidated. */ +export function issuedBeforeInvalidation(iatSeconds: number, tokensValidFromMs: number): boolean { + // `iat` has second precision; compare at second precision so a token minted in the same + // second as a password reset / unblock is not spuriously rejected. + return iatSeconds < Math.floor(tokensValidFromMs / 1000); +} + +export type TokenSecrets = { accessSecret: string; refreshSecret: string }; + +export function createTokenService({ accessSecret, refreshSecret }: TokenSecrets) { + return { + signAccess(user: TokenUser) { + return sign( + { + typ: "access", + id: user.id, + role: user.role, + disabled: user.disabled, + tokensValidFrom: user.tokensValidFrom.getTime(), + }, + accessSecret, + ACCESS_TOKEN_TTL, + newJti(), + ); + }, + verifyAccess: (token: string, opts?: { allowExpired?: boolean }) => + verify(token, accessSecret, "access", opts), + + signRefresh: (userId: number) => + sign({ typ: "refresh", id: userId }, refreshSecret, REFRESH_TOKEN_TTL_SECONDS, newJti()), + verifyRefresh: (token: string) => verify(token, refreshSecret, "refresh"), + + signTwoFactor: (userId: number) => sign({ typ: "2fa", id: userId }, accessSecret, TWO_FACTOR_TOKEN_TTL), + verifyTwoFactor: (token: string) => verify(token, accessSecret, "2fa"), + + /** Reset tokens are bound to the current password hash, so they stop working once used. */ + signReset: (user: { id: number; email: string; password: string }) => + sign({ typ: "reset", id: user.id, email: user.email }, accessSecret + user.password, RESET_TOKEN_TTL), + verifyReset: (token: string, passwordHash: string) => + verify(token, accessSecret + passwordHash, "reset"), + }; +} + +export type TokenService = ReturnType; diff --git a/src/server/auth/totp.ts b/src/server/auth/totp.ts new file mode 100644 index 0000000..cd40f48 --- /dev/null +++ b/src/server/auth/totp.ts @@ -0,0 +1,24 @@ +import { generateSecret, generateURI, verify } from "otplib"; +import QRCode from "qrcode"; + +/** Base32 secrets are interchangeable with the ones `speakeasy` generated, so existing 2FA users keep working. */ +export function createTotpSecret(accountEmail: string) { + const secret = generateSecret(); + const otpauthUrl = generateURI({ issuer: "MatConnect", label: accountEmail, secret }); + return { secret, otpauthUrl }; +} + +export const qrCodeDataUrl = (otpauthUrl: string) => QRCode.toDataURL(otpauthUrl); + +/** + * @param toleranceSteps how many 30s steps either side to accept (1 = the original `window: 1`). + */ +export async function verifyTotp(secret: string, token: string, toleranceSteps = 1, epoch?: number) { + if (!/^\d{6}$/.test(token)) return false; + try { + const result = await verify({ secret, token, epochTolerance: toleranceSteps * 30, epoch }); + return result.valid; + } catch { + return false; + } +} diff --git a/src/server/db.ts b/src/server/db.ts new file mode 100644 index 0000000..a3f3c29 --- /dev/null +++ b/src/server/db.ts @@ -0,0 +1,71 @@ +import "server-only"; +import { PrismaNeon } from "@prisma/adapter-neon"; +import { PrismaPg } from "@prisma/adapter-pg"; +import pg from "pg"; +import { PrismaClient } from "@/generated/prisma/client"; +import { env } from "./env"; + +/** + * Prisma client singleton. + * + * Neon: `DATABASE_URL` should be the pooled connection string (host contains `-pooler`). By default + * Neon hosts use the Neon serverless driver (low-latency, serverless-friendly); anything else + * (local Docker Postgres, CI service containers) uses node-postgres. Override with DATABASE_DRIVER. + */ +export function resolveDriver(url: string, explicit?: "neon" | "pg"): "neon" | "pg" { + if (explicit) return explicit; + try { + return new URL(url).hostname.endsWith(".neon.tech") ? "neon" : "pg"; + } catch { + return "pg"; + } +} + +/** + * node-postgres pool whose sessions run in UTC. + * + * The pg driver adapter sends JS Dates as UTC wall-clock strings without an offset, so a server whose + * TimeZone isn't UTC (a laptop install, some managed hosts) would shift every timestamptz write. + * Neon defaults to GMT; this makes other servers behave the same. + */ +export function createUtcPool(connectionString: string): pg.Pool { + // `onConnect` is awaited before the pool hands the client out, so no query can race the SET. + return new pg.Pool({ connectionString, onConnect: async (client) => void (await client.query("SET TIME ZONE 'UTC'")) }); +} + +function createClient(): PrismaClient { + const { DATABASE_URL, DATABASE_DRIVER, NODE_ENV } = env(); + const adapter = + resolveDriver(DATABASE_URL, DATABASE_DRIVER) === "neon" + ? new PrismaNeon({ connectionString: DATABASE_URL }) + : new PrismaPg(createUtcPool(DATABASE_URL)); + + return new PrismaClient({ + adapter, + log: NODE_ENV === "development" ? ["warn", "error"] : ["error"], + }); +} + +const globalForPrisma = globalThis as unknown as { __prisma?: PrismaClient }; + +/** Lazily-created client, reused across hot reloads in development. */ +export function getDb(): PrismaClient { + if (!globalForPrisma.__prisma) { + globalForPrisma.__prisma = createClient(); + } + return globalForPrisma.__prisma; +} + +/** + * Convenience proxy so modules can `import { db } from "@/server/db"` without creating a + * connection at import time (keeps `next build` and unit tests free of DB access). + */ +export const db: PrismaClient = new Proxy({} as PrismaClient, { + get(_target, prop) { + const client = getDb(); + const value = Reflect.get(client, prop); + return typeof value === "function" ? value.bind(client) : value; + }, +}); + +export type Db = PrismaClient; diff --git a/src/server/domain/eta.ts b/src/server/domain/eta.ts new file mode 100644 index 0000000..f3e7551 --- /dev/null +++ b/src/server/domain/eta.ts @@ -0,0 +1,93 @@ +import { bearingDifference, calculateBearing, haversineDistance, type LatLng } from "./geo"; + +/** Only ask Google for directions when the vehicle is within this straight-line distance. */ +export const GEOFENCE_RADIUS_M = 5_000; +/** Route distance below which a vehicle counts as "Arrived". */ +export const ARRIVAL_RADIUS_M = 50; +/** Movement below this is treated as GPS drift. */ +export const MOVEMENT_THRESHOLD_M = 10; +/** Heading this far away from the stage means the vehicle has already left it. */ +export const DEPARTED_ANGLE_DEG = 90; + +export type VehicleStatus = "Offline" | "Too Far" | "No Route" | "Approaching" | "Arrived" | "Departed"; + +export type Directions = { + distance: { value: number; text: string }; + duration: { value: number; text: string }; + duration_in_traffic?: { value: number; text: string }; +}; + +export type VehicleEta = { + id: number; + carId: string; + plateNumber: string; + eta: string; + status: VehicleStatus; +}; + +/** Statuses passengers should not see for a stage. */ +export const HIDDEN_STATUSES: ReadonlySet = new Set(["Departed", "Too Far"]); + +export type EtaVehicle = { + id: number; + carId: string; + plateNumber: string; + lastLatitude: number | null; + lastLongitude: number | null; +}; + +/** + * Pure classification step of the ETA calculation (same rules as the original + * etaCalculationService, minus the I/O). `getDirections` is only called inside the geofence. + */ +export async function computeVehicleEta( + vehicle: EtaVehicle, + current: LatLng | undefined, + stage: LatLng, + getDirections: (origin: LatLng, destination: LatLng) => Promise, +): Promise { + const base = { id: vehicle.id, carId: vehicle.carId, plateNumber: vehicle.plateNumber }; + + if (!current) return { ...base, eta: "N/A", status: "Offline" }; + + if (haversineDistance(current, stage) > GEOFENCE_RADIUS_M) { + return { ...base, eta: "N/A", status: "Too Far" }; + } + + const directions = await getDirections(current, stage); + if (!directions?.distance || !directions.duration) { + return { ...base, eta: "N/A", status: "No Route" }; + } + + if (directions.distance.value < ARRIVAL_RADIUS_M) { + return { ...base, eta: "Arrived", status: "Arrived" }; + } + + const previous = + vehicle.lastLatitude != null && vehicle.lastLongitude != null + ? { latitude: vehicle.lastLatitude, longitude: vehicle.lastLongitude } + : null; + + if (previous && haversineDistance(previous, current) > MOVEMENT_THRESHOLD_M) { + const heading = calculateBearing(previous, current); + const towardsStage = calculateBearing(current, stage); + if (bearingDifference(heading, towardsStage) > DEPARTED_ANGLE_DEG) { + return { ...base, eta: "N/A", status: "Departed" }; + } + } + + const duration = directions.duration_in_traffic ?? directions.duration; + return { ...base, eta: duration.text, status: "Approaching" }; +} + +/** + * A vehicle can serve several stages. Its status for the cycle is "Arrived" if it arrived at any + * of them, otherwise the first status seen. + */ +export function mergeVehicleStatus( + statuses: Map, + vehicleId: number, + status: VehicleStatus, +): void { + if (status === "Arrived" || !statuses.has(vehicleId)) statuses.set(vehicleId, status); +} diff --git a/src/server/domain/geo.ts b/src/server/domain/geo.ts new file mode 100644 index 0000000..0a4dbcd --- /dev/null +++ b/src/server/domain/geo.ts @@ -0,0 +1,44 @@ +export type LatLng = { latitude: number; longitude: number }; + +const EARTH_RADIUS_M = 6371e3; +const toRadians = (deg: number) => (deg * Math.PI) / 180; +const toDegrees = (rad: number) => (rad * 180) / Math.PI; + +/** Great-circle distance in metres. */ +export function haversineDistance(a: LatLng, b: LatLng): number { + const φ1 = toRadians(a.latitude); + const φ2 = toRadians(b.latitude); + const Δφ = toRadians(b.latitude - a.latitude); + const Δλ = toRadians(b.longitude - a.longitude); + const h = Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2; + return EARTH_RADIUS_M * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)); +} + +/** Initial bearing from `start` to `end`, in degrees [0, 360). */ +export function calculateBearing(start: LatLng, end: LatLng): number { + const φ1 = toRadians(start.latitude); + const φ2 = toRadians(end.latitude); + const Δλ = toRadians(end.longitude - start.longitude); + const y = Math.sin(Δλ) * Math.cos(φ2); + const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ); + return (toDegrees(Math.atan2(y, x)) + 360) % 360; +} + +/** Smallest absolute difference between two bearings, in degrees [0, 180]. */ +export function bearingDifference(a: number, b: number): number { + const diff = Math.abs(a - b) % 360; + return diff > 180 ? 360 - diff : diff; +} + +export function isValidLatLng(value: unknown): value is LatLng { + if (!value || typeof value !== "object") return false; + const { latitude, longitude } = value as Record; + return ( + typeof latitude === "number" && + typeof longitude === "number" && + Number.isFinite(latitude) && + Number.isFinite(longitude) && + Math.abs(latitude) <= 90 && + Math.abs(longitude) <= 180 + ); +} diff --git a/src/server/domain/periods.ts b/src/server/domain/periods.ts new file mode 100644 index 0000000..06e334f --- /dev/null +++ b/src/server/domain/periods.ts @@ -0,0 +1,78 @@ +/** + * Date bucketing for the Matatu-admin charts and reports. + * + * The Express version leaned on MySQL's YEARWEEK()/DATE_FORMAT(); doing it here keeps the queries + * portable and lets us unit-test the maths. Everything is UTC, matching how daily rows are keyed. + */ + +export type Period = "daily" | "weekly" | "monthly"; +export const PERIODS: readonly Period[] = ["daily", "weekly", "monthly"]; + +/** `YYYY-MM-DD` for the UTC calendar day of `date`. */ +export const utcDateKey = (date: Date) => date.toISOString().slice(0, 10); + +/** Midnight UTC of the given day (what a Postgres DATE column round-trips to). */ +export function startOfUtcDay(date: Date): Date { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); +} + +export function addUtcDays(date: Date, days: number): Date { + const d = startOfUtcDay(date); + d.setUTCDate(d.getUTCDate() + days); + return d; +} + +/** ISO-8601 week key, e.g. `2026-W43`. */ +export function isoWeekKey(date: Date): string { + const d = startOfUtcDay(date); + const day = d.getUTCDay() || 7; // Mon=1 … Sun=7 + d.setUTCDate(d.getUTCDate() + 4 - day); // Thursday of this week decides the year + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86_400_000 + 1) / 7); + return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`; +} + +export const monthKey = (date: Date) => date.toISOString().slice(0, 7); + +export function periodKey(date: Date, period: Period): string { + switch (period) { + case "weekly": + return isoWeekKey(date); + case "monthly": + return monthKey(date); + default: + return utcDateKey(date); + } +} + +/** + * First day included for a period, counting `now` as part of the range: + * daily → last 7 days, weekly → last 28 days, monthly → this month and the 11 before it. + */ +export function periodRangeStart(period: Period, now: Date): Date { + switch (period) { + case "weekly": + return addUtcDays(now, -27); + case "monthly": + return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 11, 1)); + default: + return addUtcDays(now, -6); + } +} + +export type DatedValue = { date: Date; value: number }; +export type PeriodTotal = { period: string; total: number }; + +/** Sums values per period key, sorted ascending. */ +export function bucketByPeriod(rows: DatedValue[], period: Period): PeriodTotal[] { + const totals = new Map(); + for (const row of rows) { + const key = periodKey(row.date, period); + totals.set(key, (totals.get(key) ?? 0) + row.value); + } + return [...totals.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([periodLabel, total]) => ({ period: periodLabel, total })); +} + +export const metresToKm = (metres: number) => Math.round((metres / 1000) * 100) / 100; diff --git a/src/server/domain/phone.ts b/src/server/domain/phone.ts new file mode 100644 index 0000000..4f312a0 --- /dev/null +++ b/src/server/domain/phone.ts @@ -0,0 +1,18 @@ +/** + * Normalises a phone number for SMS delivery. + * - strips spaces, dashes and brackets + * - local Kenyan numbers (07…, 01…) become +254… + * - 254… without a plus gets one + * Returns null for an empty value (clears the number). + */ +export function normalizePhoneNumber(input: string): string | null { + const compact = input.replace(/[\s\-().]/g, ""); + if (!compact) return null; + if (compact.startsWith("+")) return compact; + if (compact.startsWith("0")) return `+254${compact.slice(1)}`; + if (compact.startsWith("254")) return `+${compact}`; + return compact; +} + +/** Loose E.164 check: + followed by 8–15 digits. */ +export const isE164 = (value: string) => /^\+[1-9]\d{7,14}$/.test(value); diff --git a/src/server/domain/routes.ts b/src/server/domain/routes.ts new file mode 100644 index 0000000..b91cd82 --- /dev/null +++ b/src/server/domain/routes.ts @@ -0,0 +1,10 @@ +/** + * Human-readable route label from the stages a vehicle serves ("A - Z", a single stage name, or + * "Not Assigned"). Stages are sorted by name for a stable label until stages get an explicit order. + */ +export function describeRoute(stageNames: string[]): string { + if (stageNames.length === 0) return "Not Assigned"; + const sorted = [...stageNames].sort((a, b) => a.localeCompare(b)); + if (sorted.length === 1) return sorted[0]!; + return `${sorted[0]} - ${sorted[sorted.length - 1]}`; +} diff --git a/src/server/env.ts b/src/server/env.ts new file mode 100644 index 0000000..81c52a3 --- /dev/null +++ b/src/server/env.ts @@ -0,0 +1,72 @@ +import "server-only"; +import { z } from "zod"; + +/** + * Typed, validated server environment. + * + * Parsed lazily on first access so `next build` (which imports route modules) does not require + * runtime secrets. Optional integrations (Firebase, Google Maps, SMS, email, Google OAuth) degrade + * gracefully when their variables are missing, matching the behaviour of the original Express app. + */ +const schema = z.object({ + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + + // Database (Neon): pooled URL for the app, direct URL for migrations (see prisma.config.ts). + DATABASE_URL: z.string().min(1, "DATABASE_URL is required"), + /** "neon" uses the Neon serverless driver, "pg" uses node-postgres. Defaults to auto-detect. */ + DATABASE_DRIVER: z.enum(["neon", "pg"]).optional(), + + APP_URL: z.string().url().default("http://localhost:3000"), + + JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters"), + JWT_REFRESH_SECRET: z.string().min(32, "JWT_REFRESH_SECRET must be at least 32 characters"), + + GOOGLE_CLIENT_ID: z.string().optional(), + GOOGLE_CLIENT_SECRET: z.string().optional(), + + GOOGLE_MAPS_API_KEY: z.string().optional(), + + FIREBASE_CREDENTIALS_BASE64: z.string().optional(), + FIREBASE_DATABASE_URL: z + .string() + .url() + .default("https://matatu-eta-default-rtdb.europe-west1.firebasedatabase.app/"), + + AFRICAS_TALKING_API_KEY: z.string().optional(), + AFRICAS_TALKING_USERNAME: z.string().optional(), + + EMAIL_HOST: z.string().default("smtp.gmail.com"), + EMAIL_PORT: z.coerce.number().int().positive().default(465), + EMAIL_SECURE: z + .string() + .optional() + .transform((v) => v !== "false"), + EMAIL_USER: z.string().optional(), + EMAIL_PASS: z.string().optional(), + EMAIL_FROM: z.string().optional(), + + /** Shared secret for /api/cron/* (Vercel Cron sends it as `Authorization: Bearer `). */ + CRON_SECRET: z.string().min(16).optional(), + /** "interval" runs the ETA cycle in-process every 30s (self-hosted `next start`); "cron" relies on /api/cron. */ + ETA_CYCLE_MODE: z.enum(["interval", "cron", "off"]).optional(), +}); + +export type Env = z.infer; + +let cached: Env | undefined; + +export function env(): Env { + if (cached) return cached; + const parsed = schema.safeParse(process.env); + if (!parsed.success) { + const details = parsed.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n"); + throw new Error(`Invalid environment configuration:\n${details}`); + } + cached = parsed.data; + return cached; +} + +/** Test helper: forget the cached env so tests can change process.env between cases. */ +export function resetEnvCache(): void { + cached = undefined; +} diff --git a/src/server/http/errors.ts b/src/server/http/errors.ts new file mode 100644 index 0000000..0cab396 --- /dev/null +++ b/src/server/http/errors.ts @@ -0,0 +1,30 @@ +export type FieldError = { field: string; message: string }; + +/** An error that maps directly to an HTTP response. Thrown from services, caught by `route()`. */ +export class HttpError extends Error { + readonly status: number; + readonly errors?: FieldError[]; + readonly headers?: Record; + + constructor( + status: number, + message: string, + options: { errors?: FieldError[]; headers?: Record } = {}, + ) { + super(message); + this.name = "HttpError"; + this.status = status; + this.errors = options.errors; + this.headers = options.headers; + } +} + +export const badRequest = (message: string, errors?: FieldError[]) => + new HttpError(400, message, { errors }); +export const unauthorized = (message = "Not authenticated") => new HttpError(401, message); +export const forbidden = (message = "Forbidden: Insufficient permissions") => + new HttpError(403, message); +export const notFound = (message = "Not found") => new HttpError(404, message); +export const conflict = (message: string) => new HttpError(409, message); +export const tooManyRequests = (message: string, retryAfterSeconds: number) => + new HttpError(429, message, { headers: { "Retry-After": String(retryAfterSeconds) } }); diff --git a/src/server/http/rate-limit.ts b/src/server/http/rate-limit.ts new file mode 100644 index 0000000..8fc00fc --- /dev/null +++ b/src/server/http/rate-limit.ts @@ -0,0 +1,72 @@ +import { tooManyRequests } from "./errors"; + +export type RateLimitRule = { + /** Bucket name; requests are counted per (bucket, client key). */ + name: string; + windowMs: number; + max: number; + message: string; +}; + +/** + * Same limits as the original express-rate-limit configuration. + * + * NOTE: counters live in process memory. On serverless platforms each instance counts separately, + * so these are best-effort abuse limits rather than hard quotas. Put a shared store (e.g. Upstash + * Redis) behind `RateLimiter` if you need global limits. + */ +export const rateLimits = { + auth: { name: "auth", windowMs: 15 * 60_000, max: 100, message: "Too many requests from this IP, please try again after 15 minutes" }, + subscription: { name: "subscription", windowMs: 10 * 60_000, max: 30, message: "Too many subscription requests. Please try again later." }, + api: { name: "api", windowMs: 15 * 60_000, max: 100, message: "Too many requests from this IP, please try again after 15 minutes" }, + admin: { name: "admin", windowMs: 15 * 60_000, max: 50, message: "Too many admin requests. Please try again later." }, + dashboard: { name: "dashboard", windowMs: 60_000, max: 30, message: "Too many dashboard requests. Please try again later." }, +} satisfies Record; + +type Window = { count: number; resetAt: number }; + +/** Fixed-window counter. */ +export class RateLimiter { + private readonly windows = new Map(); + private lastSweep = 0; + + constructor(private readonly now: () => number = Date.now) {} + + /** Records a hit and throws a 429 HttpError when the limit is exceeded. */ + hit(rule: RateLimitRule, clientKey: string): { remaining: number; resetAt: number } { + const now = this.now(); + this.sweep(now); + const key = `${rule.name}:${clientKey}`; + let window = this.windows.get(key); + if (!window || window.resetAt <= now) { + window = { count: 0, resetAt: now + rule.windowMs }; + this.windows.set(key, window); + } + window.count += 1; + if (window.count > rule.max) { + throw tooManyRequests(rule.message, Math.ceil((window.resetAt - now) / 1000)); + } + return { remaining: rule.max - window.count, resetAt: window.resetAt }; + } + + reset(): void { + this.windows.clear(); + } + + private sweep(now: number) { + if (now - this.lastSweep < 60_000) return; + this.lastSweep = now; + for (const [key, window] of this.windows) { + if (window.resetAt <= now) this.windows.delete(key); + } + } +} + +export const rateLimiter = new RateLimiter(); + +/** Best-effort client IP (first hop of X-Forwarded-For, as set by Vercel / most proxies). */ +export function clientKeyFrom(headers: Headers): string { + const forwarded = headers.get("x-forwarded-for"); + if (forwarded) return forwarded.split(",")[0]!.trim(); + return headers.get("x-real-ip") ?? "unknown"; +} diff --git a/src/server/http/route.ts b/src/server/http/route.ts new file mode 100644 index 0000000..04268b1 --- /dev/null +++ b/src/server/http/route.ts @@ -0,0 +1,157 @@ +import "server-only"; +import { NextResponse, type NextRequest } from "next/server"; +import { ZodError, type z } from "zod"; +import { Prisma } from "@/generated/prisma/client"; +import type { Role } from "@/generated/prisma/enums"; +import { authenticate } from "@/server/auth/guard"; +import type { AccessClaims } from "@/server/auth/tokens"; +import { HttpError, badRequest } from "./errors"; +import { clientKeyFrom, rateLimiter, type RateLimitRule } from "./rate-limit"; + +/** + * Thin adapter between Next.js route handlers and the feature services. + * + * export const GET = route({ auth: "Admin", rateLimit: rateLimits.admin }, async ({ user }) => …) + * + * It applies rate limits, authenticates/authorises, awaits dynamic params, serialises the result + * as JSON, and maps thrown errors (HttpError, Zod, Prisma) to consistent JSON error responses: + * { message: string, errors?: { field, message }[] } + */ + +type Params = Record; + +export type RouteContext

    = { + req: NextRequest; + params: P; + ip: string; +}; +export type AuthedRouteContext

    = RouteContext

    & { user: AccessClaims }; + +type Options = { rateLimit?: RateLimitRule | readonly RateLimitRule[] }; +type AuthOptions = Options & { auth: Role | "any" }; + +type NextHandler

    = (req: NextRequest, context: { params: Promise

    }) => Promise; + +export function route

    ( + options: AuthOptions, + handler: (ctx: AuthedRouteContext

    ) => Promise, +): NextHandler

    ; +export function route

    ( + options: Options, + handler: (ctx: RouteContext

    ) => Promise, +): NextHandler

    ; +export function route

    ( + options: Options & { auth?: Role | "any" }, + handler: (ctx: AuthedRouteContext

    ) => Promise, +): NextHandler

    { + const rules = options.rateLimit ? ([] as RateLimitRule[]).concat(options.rateLimit) : []; + + return async (req, context) => { + try { + const ip = clientKeyFrom(req.headers); + for (const rule of rules) rateLimiter.hit(rule, ip); + + const params = ((await context?.params) ?? {}) as P; + const user = options.auth + ? await authenticate(req.headers, options.auth === "any" ? undefined : options.auth) + : (null as unknown as AccessClaims); + + const result = await handler({ req, params, ip, user }); + if (result instanceof Response) return result; + return NextResponse.json(result ?? { ok: true }); + } catch (error) { + return errorResponse(error); + } + }; +} + +export function json(data: T, status = 200, init?: ResponseInit) { + return NextResponse.json(data, { ...init, status }); +} + +export async function parseBody(req: Request, schema: S): Promise> { + let raw: unknown = {}; + const text = await req.text(); + if (text.trim()) { + try { + raw = JSON.parse(text); + } catch { + throw badRequest("Malformed JSON in request body"); + } + } + return schema.parse(raw); +} + +export function parseQuery(req: NextRequest, schema: S): z.infer { + return schema.parse(Object.fromEntries(req.nextUrl.searchParams)); +} + +export function parseParams(params: unknown, schema: S): z.infer { + return schema.parse(params); +} + +const camel = (snake: string) => snake.replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase()); + +/** + * Field names from a Prisma unique-constraint error. Handles the classic `meta.target` shape and the + * driver-adapter shape, where only the index name is known (e.g. `vehicles_car_id_key` → `carId`). + */ +export function uniqueFields(error: Prisma.PrismaClientKnownRequestError): string[] { + const meta = error.meta as Record | undefined; + const target = meta?.target; + if (Array.isArray(target)) return target.map(String); + if (typeof target === "string") return [target]; + + const cause = (meta?.driverAdapterError as { cause?: Record } | undefined)?.cause; + const constraint = cause?.constraint as { fields?: unknown; index?: unknown } | undefined; + if (Array.isArray(constraint?.fields)) return constraint.fields.map((f) => camel(String(f).replaceAll('"', ""))); + if (typeof constraint?.index === "string") { + const table = typeof cause?.table === "string" ? cause.table : ""; + const column = constraint.index.replace(new RegExp(`^${table}_`), "").replace(/_key$/, ""); + return column ? [camel(column)] : []; + } + return []; +} + +export function errorResponse(error: unknown): Response { + if (error instanceof HttpError) { + return NextResponse.json( + { message: error.message, ...(error.errors ? { errors: error.errors } : {}) }, + { status: error.status, headers: error.headers }, + ); + } + + if (error instanceof ZodError) { + return NextResponse.json( + { + message: "Validation failed", + errors: error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message })), + }, + { status: 400 }, + ); + } + + if (error instanceof Prisma.PrismaClientKnownRequestError) { + switch (error.code) { + case "P2002": { + const fields = uniqueFields(error); + return NextResponse.json( + { + message: fields.length + ? `A record with this ${fields.join(", ")} already exists.` + : "A record with these details already exists.", + errors: fields.map((field) => ({ field, message: `${field} must be unique` })), + }, + { status: 409 }, + ); + } + case "P2025": + return NextResponse.json({ message: "Not found" }, { status: 404 }); + case "P2003": + return NextResponse.json({ message: "A related record does not exist." }, { status: 400 }); + } + } + + console.error("[api] unhandled error", error); + return NextResponse.json({ message: "An unexpected internal server error occurred." }, { status: 500 }); +} diff --git a/src/server/integrations/firebase.ts b/src/server/integrations/firebase.ts new file mode 100644 index 0000000..c976a6f --- /dev/null +++ b/src/server/integrations/firebase.ts @@ -0,0 +1,91 @@ +import "server-only"; +import { cert, getApps, initializeApp, type App, type ServiceAccount } from "firebase-admin/app"; +import { getDatabase, type Database } from "firebase-admin/database"; +import { isValidLatLng, type LatLng } from "@/server/domain/geo"; +import { env } from "@/server/env"; + +/** + * Firebase Realtime Database: IoT devices write GPS fixes to `locations/`, and the ETA + * cycle publishes `etas/` for live clients. + * + * Credentials come from FIREBASE_CREDENTIALS_BASE64 (base64 of the service-account JSON) — + * the file-on-disk option from the Express app doesn't suit serverless deploys. + */ + +export type VehicleLocation = LatLng & { gpsStatus?: string }; +export type StageEtaPayload = Record; + +let database: Database | null | undefined; + +function getRtdb(): Database | null { + if (database !== undefined) return database; + + const { FIREBASE_CREDENTIALS_BASE64, FIREBASE_DATABASE_URL } = env(); + if (!FIREBASE_CREDENTIALS_BASE64) { + console.warn("[firebase] FIREBASE_CREDENTIALS_BASE64 not set — live locations and ETAs are disabled."); + database = null; + return database; + } + + try { + const serviceAccount = JSON.parse( + Buffer.from(FIREBASE_CREDENTIALS_BASE64, "base64").toString("utf8"), + ) as ServiceAccount; + const app: App = + getApps().find((a) => a.name === "matconnect") ?? + initializeApp({ credential: cert(serviceAccount), databaseURL: FIREBASE_DATABASE_URL }, "matconnect"); + database = getDatabase(app); + } catch (error) { + console.error("[firebase] failed to initialise", error); + database = null; + } + return database; +} + +/** Raw `locations` node, keeping only entries that look like valid coordinates. */ +export async function fetchVehicleLocations(): Promise> { + const rtdb = getRtdb(); + const result = new Map(); + if (!rtdb) return result; + + const snapshot = await rtdb.ref("locations").once("value"); + const raw = (snapshot.val() ?? {}) as Record; + for (const [carId, value] of Object.entries(raw)) { + if (isValidLatLng(value)) { + const { latitude, longitude, gpsStatus } = value as VehicleLocation; + result.set(carId, { latitude, longitude, gpsStatus }); + } + } + return result; +} + +export async function publishStageEtas(stageName: string, etas: StageEtaPayload): Promise { + const rtdb = getRtdb(); + if (!rtdb) return; + await rtdb.ref("etas").child(stageName).set(etas); +} + +// --- Short-lived cache ----------------------------------------------------------------------- +// Replaces the Express app's in-memory/Redis "locationCache". Instead of depending on a background +// loop in the same process (which doesn't exist on serverless), readers fetch from Firebase and +// share the result for a few seconds. + +const LOCATION_CACHE_TTL_MS = 10_000; +let cache: { at: number; value: Promise> } | undefined; + +export function getVehicleLocations(now = Date.now()): Promise> { + if (!cache || now - cache.at > LOCATION_CACHE_TTL_MS) { + const value = fetchVehicleLocations().catch((error) => { + console.error("[firebase] failed to read locations", error); + cache = undefined; + return new Map(); + }); + cache = { at: now, value }; + } + return cache.value; +} + +/** Lets the ETA cycle seed the cache with what it just read. */ +export function primeVehicleLocations(locations: Map, now = Date.now()) { + cache = { at: now, value: Promise.resolve(locations) }; +} diff --git a/src/server/integrations/google-maps.ts b/src/server/integrations/google-maps.ts new file mode 100644 index 0000000..225dee2 --- /dev/null +++ b/src/server/integrations/google-maps.ts @@ -0,0 +1,85 @@ +import "server-only"; +import type { Directions } from "@/server/domain/eta"; +import type { LatLng } from "@/server/domain/geo"; +import { env } from "@/server/env"; +import { TtlCache } from "@/server/lib/ttl-cache"; + +const DIRECTIONS_URL = "https://maps.googleapis.com/maps/api/directions/json"; +const GEOCODE_URL = "https://maps.googleapis.com/maps/api/geocode/json"; +const CACHE_TTL_MS = 5 * 60_000; +const TIMEOUT_MS = 5_000; + +const routeCache = new TtlCache(CACHE_TTL_MS, 2_000); +const geocodeCache = new TtlCache(CACHE_TTL_MS, 2_000); + +/** ~110 m grid, so small GPS jitter still hits the cache. */ +const round = (p: LatLng) => `${p.latitude.toFixed(3)},${p.longitude.toFixed(3)}`; + +let warned = false; +function apiKey(): string | null { + const key = env().GOOGLE_MAPS_API_KEY ?? null; + if (!key && !warned) { + console.warn("[google-maps] GOOGLE_MAPS_API_KEY not set — ETAs and place names are disabled."); + warned = true; + } + return key; +} + +async function getJson(url: URL): Promise { + const res = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT_MS), cache: "no-store" }); + if (!res.ok) throw new Error(`Google Maps HTTP ${res.status}`); + return res.json(); +} + +/** Traffic-aware directions leg (`departure_time=now`), or null. */ +export async function getDirections(origin: LatLng, destination: LatLng): Promise { + const key = apiKey(); + if (!key) return null; + + const cacheKey = `${round(origin)}-${round(destination)}`; + const cached = routeCache.get(cacheKey); + if (cached !== undefined) return cached; + + const url = new URL(DIRECTIONS_URL); + url.search = new URLSearchParams({ + origin: `${origin.latitude},${origin.longitude}`, + destination: `${destination.latitude},${destination.longitude}`, + departure_time: "now", + key, + }).toString(); + + try { + const data = (await getJson(url)) as { routes?: { legs?: Directions[] }[] }; + const leg = data.routes?.[0]?.legs?.[0] ?? null; + routeCache.set(cacheKey, leg); + return leg; + } catch (error) { + console.error("[google-maps] directions failed", error); + return null; + } +} + +/** Street-level address for coordinates (prefers a `route` result), with the same fallbacks as before. */ +export async function getLocationName(location: LatLng): Promise { + const key = apiKey(); + if (!key) return "Unknown Location"; + + const cacheKey = round(location); + const cached = geocodeCache.get(cacheKey); + if (cached !== undefined) return cached; + + const url = new URL(GEOCODE_URL); + url.search = new URLSearchParams({ latlng: `${location.latitude},${location.longitude}`, key }).toString(); + + try { + const data = (await getJson(url)) as { results?: { types: string[]; formatted_address: string }[] }; + const results = data.results ?? []; + if (!results.length) return "Location not found"; + const address = (results.find((r) => r.types.includes("route")) ?? results[0]!).formatted_address; + geocodeCache.set(cacheKey, address); + return address; + } catch (error) { + console.error("[google-maps] geocoding failed", error); + return "Unknown Location"; + } +} diff --git a/src/server/integrations/mailer.ts b/src/server/integrations/mailer.ts new file mode 100644 index 0000000..afbe27c --- /dev/null +++ b/src/server/integrations/mailer.ts @@ -0,0 +1,53 @@ +import "server-only"; +import nodemailer, { type Transporter } from "nodemailer"; +import { env } from "@/server/env"; + +export type Mail = { to: string; subject: string; text: string; html?: string }; + +let transporterPromise: Promise | undefined; + +async function createTransporter(): Promise { + const e = env(); + if (e.EMAIL_USER && e.EMAIL_PASS) { + return nodemailer.createTransport({ + host: e.EMAIL_HOST, + port: e.EMAIL_PORT, + secure: e.EMAIL_SECURE, + auth: { user: e.EMAIL_USER, pass: e.EMAIL_PASS }, + }); + } + if (e.NODE_ENV === "production") { + console.error("[mail] EMAIL_USER / EMAIL_PASS not set — emails will not be sent."); + return null; + } + // Development fallback: Ethereal catches the mail and logs a preview URL. + const account = await nodemailer.createTestAccount(); + console.warn("[mail] Using an Ethereal test inbox. Set EMAIL_* variables to send real email."); + return nodemailer.createTransport({ + host: "smtp.ethereal.email", + port: 587, + secure: false, + auth: { user: account.user, pass: account.pass }, + }); +} + +/** Sends an email. Never throws — failures are logged so callers can't leak account existence. */ +export async function sendMail(mail: Mail): Promise { + try { + transporterPromise ??= createTransporter(); + const transporter = await transporterPromise; + if (!transporter) return false; + const e = env(); + const info = await transporter.sendMail({ + from: e.EMAIL_FROM ?? e.EMAIL_USER ?? "MatConnect ", + ...mail, + }); + const preview = nodemailer.getTestMessageUrl(info); + if (preview) console.info("[mail] preview:", preview); + return true; + } catch (error) { + console.error("[mail] failed to send", error); + transporterPromise = undefined; + return false; + } +} diff --git a/src/server/integrations/sms.ts b/src/server/integrations/sms.ts new file mode 100644 index 0000000..ed371c3 --- /dev/null +++ b/src/server/integrations/sms.ts @@ -0,0 +1,48 @@ +import "server-only"; +import { env } from "@/server/env"; + +/** + * Africa's Talking SMS over its REST API (replaces the untyped `africastalking` SDK). + * Docs: https://developers.africastalking.com/docs/sms/sending + */ + +export type SmsResult = + | { status: "sent"; statusCode: number; raw: unknown } + | { status: "failed"; statusCode?: number; raw: unknown } + | { status: "skipped" }; + +/** Africa's Talking per-recipient status code for "Success". */ +export const AT_SUCCESS = 101; + +export function smsEndpoint(username: string) { + return username === "sandbox" + ? "https://api.sandbox.africastalking.com/version1/messaging" + : "https://api.africastalking.com/version1/messaging"; +} + +export async function sendSms(to: string, message: string): Promise { + const { AFRICAS_TALKING_API_KEY: apiKey, AFRICAS_TALKING_USERNAME: username } = env(); + if (!apiKey || !username) { + console.info("[sms] Africa's Talking not configured — skipping SMS."); + return { status: "skipped" }; + } + + const res = await fetch(smsEndpoint(username), { + method: "POST", + headers: { + apiKey, + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ username, to, message }), + signal: AbortSignal.timeout(10_000), + }); + + const raw = (await res.json().catch(() => null)) as { + SMSMessageData?: { Recipients?: { statusCode?: number }[] }; + } | null; + const statusCode = raw?.SMSMessageData?.Recipients?.[0]?.statusCode; + + if (res.ok && statusCode === AT_SUCCESS) return { status: "sent", statusCode, raw }; + return { status: "failed", statusCode, raw }; +} diff --git a/src/server/jobs/eta-cycle.ts b/src/server/jobs/eta-cycle.ts new file mode 100644 index 0000000..f874599 --- /dev/null +++ b/src/server/jobs/eta-cycle.ts @@ -0,0 +1,257 @@ +import "server-only"; +import { db as defaultDb, type Db } from "@/server/db"; +import { mergeVehicleStatus, type VehicleStatus } from "@/server/domain/eta"; +import { MOVEMENT_THRESHOLD_M } from "@/server/domain/eta"; +import { haversineDistance, type LatLng } from "@/server/domain/geo"; +import { startOfUtcDay } from "@/server/domain/periods"; +import { + fetchVehicleLocations, + primeVehicleLocations, + publishStageEtas, + type StageEtaPayload, + type VehicleLocation, +} from "@/server/integrations/firebase"; +import { getDirections } from "@/server/integrations/google-maps"; +import { sendSms as defaultSendSms, type SmsResult } from "@/server/integrations/sms"; +import { purgeExpiredRevocations } from "@/server/modules/auth/revocation"; +import { calculateEtasForStage, visibleArrivals, type DirectionsFn } from "@/server/modules/eta/eta.service"; +import { activeVehicle } from "@/server/modules/vehicles/vehicles"; +import { acquireJobLock } from "./job-lock"; + +/** + * The background ETA cycle (formerly cronService.js), run every ~30 s by either + * - Vercel Cron / any scheduler hitting POST /api/cron/eta-cycle, or + * - the in-process interval started from src/instrumentation.ts when self-hosting. + * + * 1. read every vehicle's GPS fix from Firebase (one call) + * 2. compute ETAs per stage and publish them to Firebase `etas/` + * 3. on arrival: count a trip (once per stay) and SMS one-shot subscribers + * 4. clear `isAtStage` for vehicles that are no longer at any stage + * 5. accumulate daily distance and remember the last position + */ + +export const ETA_LOCK_NAME = "eta-cycle"; +const LOCK_TTL_MS = 55_000; + +export type EtaCycleDeps = { + db: Db; + fetchLocations: () => Promise>; + publishEtas: (stageName: string, etas: StageEtaPayload) => Promise; + directions: DirectionsFn; + sendSms: (to: string, message: string) => Promise; + now: () => Date; + useLock: boolean; +}; + +export type EtaCycleReport = + | { skipped: true; reason: string } + | { + skipped: false; + stages: number; + vehiclesOnline: number; + arrivals: number; + tripsCounted: number; + smsSent: number; + errors: number; + durationMs: number; + }; + +const defaultDeps = (): EtaCycleDeps => ({ + db: defaultDb, + fetchLocations: fetchVehicleLocations, + publishEtas: publishStageEtas, + directions: getDirections, + sendSms: defaultSendSms, + now: () => new Date(), + useLock: true, +}); + +let runningInProcess = false; + +export async function runEtaCycle(overrides: Partial = {}): Promise { + const deps = { ...defaultDeps(), ...overrides }; + if (runningInProcess) return { skipped: true, reason: "already running in this process" }; + runningInProcess = true; + let release: (() => Promise) | null = null; + try { + if (deps.useLock) { + release = await acquireJobLock(deps.db, ETA_LOCK_NAME, LOCK_TTL_MS); + if (!release) return { skipped: true, reason: "another instance holds the lock" }; + } + return await cycle(deps); + } finally { + runningInProcess = false; + await release?.().catch((e) => console.error("[eta-cycle] failed to release lock", e)); + } +} + +async function cycle(deps: EtaCycleDeps): Promise { + const { db } = deps; + const startedAt = Date.now(); + const report = { stages: 0, vehiclesOnline: 0, arrivals: 0, tripsCounted: 0, smsSent: 0, errors: 0 }; + + // --- 1. Read phase --------------------------------------------------------------------------- + const [stages, vehicles, locations] = await Promise.all([ + db.stage.findMany({ + select: { + id: true, + name: true, + latitude: true, + longitude: true, + vehicles: { + where: { vehicle: activeVehicle }, + select: { + vehicle: { select: { id: true, carId: true, plateNumber: true, lastLatitude: true, lastLongitude: true } }, + }, + }, + subscribers: { select: { userId: true, user: { select: { phoneNumber: true } } } }, + }, + }), + db.vehicle.findMany({ + where: activeVehicle, + select: { id: true, carId: true, isAtStage: true, lastLatitude: true, lastLongitude: true }, + }), + deps.fetchLocations(), + ]); + primeVehicleLocations(locations); + report.stages = stages.length; + report.vehiclesOnline = vehicles.filter((v) => locations.has(v.carId)).length; + + const finalStatus = new Map(); + const today = startOfUtcDay(deps.now()); + + // --- 2. Calculation + arrival phase ---------------------------------------------------------- + for (const stage of stages) { + try { + const etas = await calculateEtasForStage( + stage, + stage.vehicles.map((v) => v.vehicle), + locations, + deps.directions, + ); + for (const eta of etas) mergeVehicleStatus(finalStatus, eta.id, eta.status); + + const payload: StageEtaPayload = Object.fromEntries( + visibleArrivals(etas).map((e) => [e.carId, { plateNumber: e.plateNumber, eta: e.eta, status: e.status }]), + ); + await deps.publishEtas(stage.name, payload); + + for (const arrived of etas.filter((e) => e.status === "Arrived")) { + report.arrivals += 1; + if (await countTripOnce(db, arrived.id, today)) report.tripsCounted += 1; + report.smsSent += await notifySubscribers(deps, stage, arrived.plateNumber); + } + } catch (error) { + report.errors += 1; + console.error(`[eta-cycle] stage "${stage.name}" failed`, error); + } + } + + // --- 3. Reset vehicles that have left every stage -------------------------------------------- + const leftStage = vehicles.filter((v) => v.isAtStage && finalStatus.get(v.id) !== "Arrived").map((v) => v.id); + if (leftStage.length) { + await db.vehicle.updateMany({ where: { id: { in: leftStage } }, data: { isAtStage: false } }); + } + + // --- 4. Distance + last-known-position phase ------------------------------------------------- + for (const vehicle of vehicles) { + const location = locations.get(vehicle.carId); + if (!location) continue; + try { + await trackDistance(db, vehicle, location, today); + } catch (error) { + report.errors += 1; + console.error(`[eta-cycle] distance update failed for vehicle ${vehicle.id}`, error); + } + } + + await purgeExpiredRevocations(deps.now()).catch((e) => console.error("[eta-cycle] revocation purge failed", e)); + + return { skipped: false, ...report, durationMs: Date.now() - startedAt }; +} + +/** + * Atomically flips isAtStage false→true; only the run that wins the flip counts the trip, so a + * vehicle parked at a stage for several cycles (or seen by two instances) is counted once. + */ +export async function countTripOnce(db: Db, vehicleId: number, date: Date): Promise { + return db.$transaction(async (tx) => { + const { count } = await tx.vehicle.updateMany({ + where: { id: vehicleId, isAtStage: false }, + data: { isAtStage: true }, + }); + if (count === 0) return false; + await tx.dailyTrip.upsert({ + where: { vehicleId_date: { vehicleId, date } }, + create: { vehicleId, date, tripCount: 1 }, + update: { tripCount: { increment: 1 } }, + }); + return true; + }); +} + +type StageWithSubscribers = { + id: number; + name: string; + subscribers: { userId: number; user: { phoneNumber: string | null } }[]; +}; + +/** One-shot alerts: a subscriber is removed only after Africa's Talking accepts the SMS. */ +async function notifySubscribers(deps: EtaCycleDeps, stage: StageWithSubscribers, plateNumber: string) { + let sent = 0; + const pending = stage.subscribers.filter((s) => s.user.phoneNumber); + const results = await Promise.allSettled( + pending.map(async (subscriber) => { + // Re-check: an earlier arrival in this same cycle may already have consumed the subscription. + const stillSubscribed = await deps.db.stageSubscription.count({ + where: { userId: subscriber.userId, stageId: stage.id }, + }); + if (!stillSubscribed) return; + const result = await deps.sendSms( + subscriber.user.phoneNumber!, + `Matatu ${plateNumber} has arrived at ${stage.name}.`, + ); + if (result.status === "sent") { + await deps.db.stageSubscription.deleteMany({ where: { userId: subscriber.userId, stageId: stage.id } }); + sent += 1; + } else { + console.warn(`[eta-cycle] SMS to user ${subscriber.userId} ${result.status}; keeping subscription.`); + } + }), + ); + for (const r of results) if (r.status === "rejected") console.error("[eta-cycle] SMS failed", r.reason); + return sent; +} + +type TrackedVehicle = { id: number; lastLatitude: number | null; lastLongitude: number | null }; + +/** + * Adds movement since the last remembered position to today's distance. Movement under the drift + * threshold is ignored *without* moving the baseline, so slow creeping still accumulates. + */ +export async function trackDistance(db: Db, vehicle: TrackedVehicle, current: LatLng, date: Date) { + const { lastLatitude, lastLongitude } = vehicle; + if (lastLatitude == null || lastLongitude == null) { + await db.vehicle.update({ + where: { id: vehicle.id }, + data: { lastLatitude: current.latitude, lastLongitude: current.longitude }, + }); + return 0; + } + + const moved = haversineDistance({ latitude: lastLatitude, longitude: lastLongitude }, current); + if (moved <= MOVEMENT_THRESHOLD_M) return 0; + + await db.$transaction([ + db.dailyDistance.upsert({ + where: { vehicleId_date: { vehicleId: vehicle.id, date } }, + create: { vehicleId: vehicle.id, date, distanceCovered: moved }, + update: { distanceCovered: { increment: moved } }, + }), + db.vehicle.update({ + where: { id: vehicle.id }, + data: { lastLatitude: current.latitude, lastLongitude: current.longitude }, + }), + ]); + return moved; +} diff --git a/src/server/jobs/job-lock.ts b/src/server/jobs/job-lock.ts new file mode 100644 index 0000000..63c23bc --- /dev/null +++ b/src/server/jobs/job-lock.ts @@ -0,0 +1,31 @@ +import "server-only"; +import type { Db } from "@/server/db"; + +/** + * Tries to take the named lease for `ttlMs`. Returns a release function, or null if another + * holder's lease is still live. Expired leases are taken over, so a crashed run can't wedge a job. + * + * The clock comes from the app (one `now` per call) rather than SQL `now()`, so the comparison is + * consistent with how Prisma writes timestamps regardless of the database session's time zone. + */ +export async function acquireJobLock(db: Db, name: string, ttlMs: number, holder: string = crypto.randomUUID()) { + const now = new Date(); + const until = new Date(now.getTime() + ttlMs); + const acquired = await db.$executeRaw` + INSERT INTO "job_locks" ("name", "locked_until", "locked_by", "updated_at") + VALUES (${name}, ${until}, ${holder}, ${now}) + ON CONFLICT ("name") DO UPDATE + SET "locked_until" = EXCLUDED."locked_until", + "locked_by" = EXCLUDED."locked_by", + "updated_at" = EXCLUDED."updated_at" + WHERE "job_locks"."locked_until" < ${now}`; + + if (acquired === 0) return null; + + return async function release() { + const releasedAt = new Date(); + await db.$executeRaw` + UPDATE "job_locks" SET "locked_until" = ${releasedAt}, "updated_at" = ${releasedAt} + WHERE "name" = ${name} AND "locked_by" = ${holder}`; + }; +} diff --git a/src/server/jobs/scheduler.ts b/src/server/jobs/scheduler.ts new file mode 100644 index 0000000..ec466fa --- /dev/null +++ b/src/server/jobs/scheduler.ts @@ -0,0 +1,35 @@ +import "server-only"; +import { runEtaCycle } from "./eta-cycle"; + +export const ETA_INTERVAL_MS = 30_000; + +let timer: ReturnType | undefined; + +/** + * In-process scheduler for long-running Node deployments (`next start` on a VM/container, or the + * standalone `npm run eta:worker`). Each run is scheduled after the previous one finishes, so runs + * never overlap within a process; the database lease prevents overlap across processes. + */ +export function startEtaScheduler(intervalMs = ETA_INTERVAL_MS) { + if (timer) return; + console.info(`[eta-cycle] in-process scheduler started (every ${intervalMs / 1000}s)`); + + const tick = async () => { + try { + const report = await runEtaCycle(); + if (!report.skipped && report.errors) console.warn("[eta-cycle] completed with errors", report); + } catch (error) { + console.error("[eta-cycle] run failed", error); + } finally { + timer = setTimeout(tick, intervalMs); + timer.unref?.(); + } + }; + timer = setTimeout(tick, 1_000); + timer.unref?.(); +} + +export function stopEtaScheduler() { + clearTimeout(timer); + timer = undefined; +} diff --git a/src/server/lib/ttl-cache.ts b/src/server/lib/ttl-cache.ts new file mode 100644 index 0000000..2429197 --- /dev/null +++ b/src/server/lib/ttl-cache.ts @@ -0,0 +1,33 @@ +/** Tiny in-memory TTL cache with a size cap (oldest entries evicted first). */ +export class TtlCache { + private readonly entries = new Map(); + + constructor( + private readonly ttlMs: number, + private readonly maxEntries = 1_000, + private readonly now: () => number = Date.now, + ) {} + + get(key: K): V | undefined { + const entry = this.entries.get(key); + if (!entry) return undefined; + if (entry.expiresAt <= this.now()) { + this.entries.delete(key); + return undefined; + } + return entry.value; + } + + set(key: K, value: V): void { + this.entries.delete(key); + this.entries.set(key, { value, expiresAt: this.now() + this.ttlMs }); + while (this.entries.size > this.maxEntries) { + const oldest = this.entries.keys().next().value as K; + this.entries.delete(oldest); + } + } + + get size() { + return this.entries.size; + } +} diff --git a/src/server/modules/admin/admin.schemas.ts b/src/server/modules/admin/admin.schemas.ts new file mode 100644 index 0000000..0f8946c --- /dev/null +++ b/src/server/modules/admin/admin.schemas.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import { Role } from "@/generated/prisma/enums"; +import { email, id, newPassword, pagination, personName } from "@/server/validation/common"; + +export const createStageSchema = z.object({ + name: z.string({ error: "Stage name is required" }).trim().min(1, "Stage name is required").max(120), + latitude: z.coerce + .number({ error: "A valid latitude is required" }) + .min(-90, "A valid latitude is required") + .max(90, "A valid latitude is required"), + longitude: z.coerce + .number({ error: "A valid longitude is required" }) + .min(-180, "A valid longitude is required") + .max(180, "A valid longitude is required"), +}); + +const trimmedRequired = (message: string) => z.string({ error: message }).trim().min(1, message).max(64); + +export const createVehicleSchema = z.object({ + carId: trimmedRequired("Car ID is required"), + plateNumber: trimmedRequired("Plate number is required").transform((p) => p.toUpperCase()), + ownerId: z + .union([z.literal(""), z.null(), id("Owner ID")]) + .optional() + .transform((v) => (typeof v === "number" ? v : null)), +}); + +export const assignVehicleSchema = z.object({ + stageId: id("Stage ID"), + vehicleId: id("Vehicle ID"), +}); + +export const stageVehicleParamsSchema = assignVehicleSchema; +export const idParamSchema = z.object({ id: id() }); + +export const listUsersQuerySchema = pagination().extend({ + q: z.string().trim().max(100).catch("").default(""), + role: z.enum(Role).optional().catch(undefined), +}); + +export const updateUserSchema = z.object({ + disabled: z.boolean({ error: "Disabled status must be a boolean" }), +}); + +export const createUserSchema = z.object({ + name: personName(), + email: email(), + password: newPassword, + role: z.enum([Role.Admin, Role.MatAdmin], { error: "Invalid role specified" }), +}); + +export const paginationQuerySchema = pagination(); +export const feedbackQuerySchema = pagination(10); diff --git a/src/server/modules/admin/admin.service.ts b/src/server/modules/admin/admin.service.ts new file mode 100644 index 0000000..4709179 --- /dev/null +++ b/src/server/modules/admin/admin.service.ts @@ -0,0 +1,182 @@ +import "server-only"; +import type { z } from "zod"; +import type { Prisma } from "@/generated/prisma/client"; +import { hashPassword } from "@/server/auth/password"; +import { db } from "@/server/db"; +import { badRequest, notFound } from "@/server/http/errors"; +import { feedbackSelect } from "@/server/modules/feedback/feedback.service"; +import { toPublicUser } from "@/server/modules/auth/auth.service"; +import { activeVehicle, liveVehicleStatus, vehicleSummarySelect } from "@/server/modules/vehicles/vehicles"; +import type { + createStageSchema, + createUserSchema, + listUsersQuerySchema, + paginationQuerySchema, +} from "./admin.schemas"; + +type Page = z.infer; + +// --- Stages --------------------------------------------------------------------------------- + +export async function listStagesWithVehicles({ limit, offset }: Page) { + const [stages, total] = await Promise.all([ + db.stage.findMany({ + orderBy: { name: "asc" }, + take: limit, + skip: offset, + select: { + id: true, + name: true, + latitude: true, + longitude: true, + vehicles: { + where: { vehicle: activeVehicle }, + orderBy: { vehicle: { plateNumber: "asc" } }, + select: { vehicle: { select: vehicleSummarySelect } }, + }, + }, + }), + db.stage.count(), + ]); + return { + stages: stages.map(({ vehicles, ...stage }) => ({ ...stage, vehicles: vehicles.map((v) => v.vehicle) })), + total, + }; +} + +export async function createStage(input: z.infer) { + const stage = await db.stage.create({ + data: input, + select: { id: true, name: true, latitude: true, longitude: true }, + }); + return { ...stage, vehicles: [] }; +} + +// --- Vehicles ------------------------------------------------------------------------------- + +export async function listVehicles({ limit, offset }: Page) { + const [vehicles, total] = await Promise.all([ + db.vehicle.findMany({ + where: activeVehicle, + orderBy: { plateNumber: "asc" }, + take: limit, + skip: offset, + select: vehicleSummarySelect, + }), + db.vehicle.count({ where: activeVehicle }), + ]); + return { vehicles, total }; +} + +/** Soft delete (keeps trip/distance/feedback history) and drop the vehicle from every route. */ +export async function deleteVehicle(vehicleId: number) { + const vehicle = await db.vehicle.findFirst({ where: { id: vehicleId, ...activeVehicle }, select: { id: true } }); + if (!vehicle) throw notFound("Vehicle not found"); + await db.$transaction([ + db.stageVehicle.deleteMany({ where: { vehicleId } }), + db.vehicle.update({ where: { id: vehicleId }, data: { deletedAt: new Date(), isAtStage: false } }), + ]); + return { message: "Vehicle deleted successfully." }; +} + +export async function assignVehicleToStage(stageId: number, vehicleId: number) { + const [stage, vehicle] = await Promise.all([ + db.stage.findUnique({ where: { id: stageId }, select: { id: true } }), + db.vehicle.findFirst({ where: { id: vehicleId, ...activeVehicle }, select: { id: true } }), + ]); + if (!stage || !vehicle) throw notFound("Stage or Vehicle not found"); + await db.stageVehicle.upsert({ + where: { stageId_vehicleId: { stageId, vehicleId } }, + create: { stageId, vehicleId }, + update: {}, + }); + return { message: "Vehicle assigned to stage successfully" }; +} + +export async function removeVehicleFromStage(stageId: number, vehicleId: number) { + const { count } = await db.stageVehicle.deleteMany({ where: { stageId, vehicleId } }); + if (count === 0) throw notFound("Stage or Vehicle not found"); + return { message: "Vehicle unassigned successfully" }; +} + +export async function dashboardData() { + return { vehicles: await liveVehicleStatus() }; +} + +// --- Feedback ------------------------------------------------------------------------------- + +export async function listFeedback({ limit, offset }: Page) { + const [feedback, total] = await Promise.all([ + db.feedback.findMany({ orderBy: { createdAt: "desc" }, take: limit, skip: offset, select: feedbackSelect }), + db.feedback.count(), + ]); + return { feedback, total }; +} + +// --- Users ---------------------------------------------------------------------------------- + +const userRowSelect = { + id: true, + name: true, + email: true, + role: true, + twoFactorEnabled: true, + disabled: true, + createdAt: true, +} satisfies Prisma.UserSelect; + +export function buildUserListWhere( + currentUserId: number, + { q, role }: Pick, "q" | "role">, +): Prisma.UserWhereInput { + return { + id: { not: currentUserId }, + ...(role ? { role } : {}), + ...(q + ? { + OR: [ + { name: { contains: q, mode: "insensitive" } }, + { email: { contains: q, mode: "insensitive" } }, + ], + } + : {}), + }; +} + +export async function listUsers(currentUserId: number, query: z.infer) { + const where = buildUserListWhere(currentUserId, query); + const [users, total] = await Promise.all([ + db.user.findMany({ where, orderBy: { name: "asc" }, take: query.limit, skip: query.offset, select: userRowSelect }), + db.user.count({ where }), + ]); + return { users, total }; +} + +export async function setUserDisabled(actingAdminId: number, userId: number, disabled: boolean) { + if (actingAdminId === userId) throw badRequest("You can't block your own account."); + const user = await db.user.findUnique({ where: { id: userId }, select: { id: true } }); + if (!user) throw notFound("User not found"); + await db.user.update({ + where: { id: userId }, + // Blocking also invalidates every token the user currently holds. + data: disabled ? { disabled, tokensValidFrom: new Date() } : { disabled }, + }); + return { message: `User has been ${disabled ? "blocked" : "unblocked"}.` }; +} + +export async function deleteUser(actingAdminId: number, userId: number) { + if (actingAdminId === userId) throw badRequest("You can't delete your own account."); + const { count } = await db.user.deleteMany({ where: { id: userId } }); + if (count === 0) throw notFound("User not found"); + return { message: "User deleted successfully." }; +} + +export const listMatAdmins = () => + db.user.findMany({ where: { role: "MatAdmin" }, orderBy: { name: "asc" }, select: { id: true, name: true } }); + +export async function createUser(input: z.infer) { + const user = await db.user.create({ + data: { ...input, password: await hashPassword(input.password) }, + }); + return { message: "User created successfully", user: toPublicUser(user) }; +} diff --git a/src/server/modules/auth/auth.schemas.ts b/src/server/modules/auth/auth.schemas.ts new file mode 100644 index 0000000..d8ab6cf --- /dev/null +++ b/src/server/modules/auth/auth.schemas.ts @@ -0,0 +1,36 @@ +import { z } from "zod"; +import { email, id, newPassword, otpCode, personName } from "@/server/validation/common"; + +export const loginSchema = z.object({ + email: email(), + password: z.string({ error: "Password cannot be empty" }).min(1, "Password cannot be empty"), +}); + +export const registerSchema = z.object({ + name: personName(), + email: email(), + password: newPassword, +}); + +export const updateProfileSchema = z.object({ + name: personName().optional(), + phoneNumber: z.string({ error: "Invalid phone number" }).max(32, "Invalid phone number").optional().nullable(), +}); + +export const requestPasswordResetSchema = z.object({ email: email() }); + +export const resetPasswordParamsSchema = z.object({ + id: id("User ID"), + token: z.string().regex(/^[\w-]+\.[\w-]+\.[\w-]+$/, "Invalid token format"), +}); +export const resetPasswordBodySchema = z.object({ password: newPassword }); + +export const verifyTwoFactorSchema = z.object({ token: otpCode }); +export const twoFactorLoginSchema = z.object({ + tempToken: z.string({ error: "2FA session token is required" }).min(1, "2FA session token is required"), + token: otpCode, +}); + +export type LoginInput = z.infer; +export type RegisterInput = z.infer; +export type UpdateProfileInput = z.infer; diff --git a/src/server/modules/auth/auth.service.ts b/src/server/modules/auth/auth.service.ts new file mode 100644 index 0000000..9acd232 --- /dev/null +++ b/src/server/modules/auth/auth.service.ts @@ -0,0 +1,258 @@ +import "server-only"; +import type { User } from "@/generated/prisma/client"; +import { Role } from "@/generated/prisma/enums"; +import { tokens } from "@/server/auth/context"; +import { dummyPasswordWork, hashPassword, verifyPassword } from "@/server/auth/password"; +import { TokenError, issuedBeforeInvalidation } from "@/server/auth/tokens"; +import { createTotpSecret, qrCodeDataUrl, verifyTotp } from "@/server/auth/totp"; +import { db } from "@/server/db"; +import { normalizePhoneNumber } from "@/server/domain/phone"; +import { env } from "@/server/env"; +import { HttpError, badRequest, forbidden, notFound, unauthorized } from "@/server/http/errors"; +import { sendMail } from "@/server/integrations/mailer"; +import type { LoginInput, RegisterInput, UpdateProfileInput } from "./auth.schemas"; +import { isRevoked, revokeToken } from "./revocation"; + +export type Session = { accessToken: string; refreshToken: string; role: Role }; +export type LoginResult = ({ twoFactorRequired: false } & Session) | { twoFactorRequired: true; tempToken: string }; + +export type PublicUser = Omit & { isGoogleUser: boolean }; + +/** Never send secrets to the client. */ +export function toPublicUser(user: User): PublicUser { + const { password: _password, twoFactorSecret: _secret, googleId, ...rest } = user; + return { ...rest, isGoogleUser: Boolean(googleId) }; +} + +async function issueSession(user: Pick): Promise { + const t = tokens(); + const [accessToken, refreshToken] = await Promise.all([t.signAccess(user), t.signRefresh(user.id)]); + return { accessToken, refreshToken, role: user.role }; +} + +const DISABLED_MESSAGE = "Your account has been disabled. Please contact an administrator."; + +export async function login({ email, password }: LoginInput): Promise { + const user = await db.user.findUnique({ where: { email } }); + if (!user) { + await dummyPasswordWork(); + throw notFound("User not found"); + } + if (user.disabled) throw forbidden(DISABLED_MESSAGE); + if (!user.password) throw badRequest("Please sign in using Google."); + if (!(await verifyPassword(password, user.password))) throw unauthorized("Invalid password"); + + if (user.twoFactorEnabled) { + return { twoFactorRequired: true, tempToken: await tokens().signTwoFactor(user.id) }; + } + return { twoFactorRequired: false, ...(await issueSession(user)) }; +} + +export async function completeTwoFactorLogin(tempToken: string, code: string): Promise { + let userId: number; + try { + ({ id: userId } = await tokens().verifyTwoFactor(tempToken)); + } catch (error) { + if (error instanceof TokenError && error.reason === "expired") { + throw unauthorized("2FA session expired, please log in again."); + } + throw unauthorized("Invalid 2FA session token."); + } + + const user = await db.user.findUnique({ where: { id: userId } }); + if (!user || !user.twoFactorEnabled || !user.twoFactorSecret) { + throw badRequest("2FA is not enabled for this user."); + } + if (user.disabled) throw forbidden(DISABLED_MESSAGE); + if (!(await verifyTotp(user.twoFactorSecret, code))) throw badRequest("Invalid 2FA token"); + + return issueSession(user); +} + +/** Self-registration. The very first account becomes the Admin (bootstrap), everyone else a Passenger. */ +export async function register({ name, email, password }: RegisterInput): Promise { + const hashed = await hashPassword(password); + const user = await db.$transaction(async (tx) => { + const role = (await tx.user.count()) === 0 ? Role.Admin : Role.Passenger; + return tx.user.create({ data: { name, email, password: hashed, role } }); + }); + if (user.role === Role.Admin) console.info("[auth] first user registered — promoted to Admin"); + return toPublicUser(user); +} + +/** + * Refresh-token rotation. The caller must also present its (possibly expired) access token for the + * same user: a cross-site request can carry the cookie but can't read the access token. + */ +export async function refreshSession(refreshToken: string | undefined, accessToken: string | undefined): Promise { + if (!refreshToken) throw unauthorized("Refresh token missing"); + if (!accessToken) throw unauthorized("Access token missing for refresh"); + + const t = tokens(); + let refresh; + try { + refresh = await t.verifyRefresh(refreshToken); + } catch { + throw new HttpError(403, "Invalid refresh token"); + } + if (await isRevoked(refresh.jti)) throw unauthorized("Token has been invalidated"); + + try { + const access = await t.verifyAccess(accessToken, { allowExpired: true }); + if (access.id !== refresh.id) throw new HttpError(403, "Token mismatch"); + } catch (error) { + if (error instanceof HttpError) throw error; + throw unauthorized("Invalid access token for refresh"); + } + + const user = await db.user.findUnique({ where: { id: refresh.id } }); + if (!user || user.disabled) throw new HttpError(403, "User account is disabled or not found"); + if (issuedBeforeInvalidation(refresh.iat, user.tokensValidFrom.getTime())) { + throw unauthorized("Token has been invalidated"); + } + + await revokeToken(refresh.jti, refresh.exp); + return issueSession(user); +} + +/** Revokes whichever tokens the client presents. Invalid tokens are ignored. */ +export async function logout(accessToken: string | undefined, refreshToken: string | undefined): Promise { + const t = tokens(); + const tasks: Promise[] = []; + if (accessToken) { + tasks.push( + t.verifyAccess(accessToken, { allowExpired: true }).then( + (c) => revokeToken(c.jti, c.exp), + () => undefined, + ), + ); + } + if (refreshToken) { + tasks.push( + t.verifyRefresh(refreshToken).then( + (c) => revokeToken(c.jti, c.exp), + () => undefined, + ), + ); + } + await Promise.all(tasks); +} + +export async function getProfile(userId: number): Promise { + const user = await db.user.findUnique({ where: { id: userId } }); + if (!user) throw notFound("User not found"); + return toPublicUser(user); +} + +export async function updateProfile(userId: number, input: UpdateProfileInput): Promise { + const data: { name?: string; phoneNumber?: string | null } = {}; + if (input.name) data.name = input.name; + if (input.phoneNumber !== undefined) { + data.phoneNumber = input.phoneNumber === null ? null : normalizePhoneNumber(input.phoneNumber); + } + const { count } = await db.user.updateMany({ where: { id: userId }, data }); + if (count === 0) throw notFound("User not found"); +} + +const RESET_RESPONSE = "If an account with that email exists, a password reset link has been sent."; + +export async function requestPasswordReset(email: string): Promise { + const user = await db.user.findUnique({ where: { email } }); + if (!user?.password) { + await dummyPasswordWork(); + return RESET_RESPONSE; + } + + const token = await tokens().signReset({ id: user.id, email: user.email, password: user.password }); + const link = new URL("/forgot-password", env().APP_URL); + link.searchParams.set("id", String(user.id)); + link.searchParams.set("token", token); + + await sendMail({ + to: user.email, + subject: "Reset your MatConnect password", + text: [ + "Someone asked to reset the password for your MatConnect account.", + "If it wasn't you, you can ignore this email.", + "", + `Reset your password (valid for 1 hour): ${link.toString()}`, + "", + `Or paste these into the reset form — User ID: ${user.id} Token: ${token}`, + ].join("\n"), + }); + + return RESET_RESPONSE; +} + +export async function resetPassword(userId: number, token: string, password: string): Promise { + const user = await db.user.findUnique({ where: { id: userId } }); + if (!user?.password) throw badRequest("Invalid or expired reset link."); + try { + const claims = await tokens().verifyReset(token, user.password); + if (claims.id !== user.id) throw new Error("id mismatch"); + } catch { + throw badRequest("Invalid or expired reset link."); + } + + await db.user.update({ + where: { id: user.id }, + // Changing the hash also invalidates the reset token; bumping tokensValidFrom logs out everywhere. + data: { password: await hashPassword(password), tokensValidFrom: new Date() }, + }); +} + +export async function beginTwoFactorSetup(userId: number) { + const user = await db.user.findUnique({ where: { id: userId } }); + if (!user) throw notFound("User not found"); + if (user.twoFactorEnabled) throw badRequest("2FA is already enabled. Disable it first to re-enrol."); + + const { secret, otpauthUrl } = createTotpSecret(user.email); + await db.user.update({ where: { id: user.id }, data: { twoFactorSecret: secret } }); + return { secret, qrCodeUrl: await qrCodeDataUrl(otpauthUrl) }; +} + +export async function confirmTwoFactorSetup(userId: number, code: string): Promise { + const user = await db.user.findUnique({ where: { id: userId } }); + if (!user?.twoFactorSecret) throw badRequest("2FA not set up or user not found."); + if (!(await verifyTotp(user.twoFactorSecret, code, 0))) throw badRequest("Invalid 2FA token"); + await db.user.update({ where: { id: user.id }, data: { twoFactorEnabled: true } }); +} + +export async function disableTwoFactor(userId: number): Promise { + const { count } = await db.user.updateMany({ + where: { id: userId }, + data: { twoFactorEnabled: false, twoFactorSecret: null }, + }); + if (count === 0) throw notFound("User not found"); +} + +export type GoogleProfile = { sub: string; email: string; name: string; picture?: string }; + +/** Finds or creates the account for a verified Google identity and starts a session. */ +export async function loginWithGoogle(profile: GoogleProfile): Promise { + const email = profile.email.toLowerCase(); + const existing = await db.user.findUnique({ where: { email } }); + + let user: User; + if (existing) { + user = existing.googleId + ? existing + : await db.user.update({ + where: { id: existing.id }, + data: { googleId: profile.sub, profilePictureUrl: existing.profilePictureUrl ?? profile.picture ?? null }, + }); + } else { + user = await db.user.create({ + data: { + googleId: profile.sub, + name: profile.name || email, + email, + profilePictureUrl: profile.picture ?? null, + role: Role.Passenger, + }, + }); + } + + if (user.disabled) throw forbidden(DISABLED_MESSAGE); + return issueSession(user); +} diff --git a/src/server/modules/auth/revocation.ts b/src/server/modules/auth/revocation.ts new file mode 100644 index 0000000..88f7946 --- /dev/null +++ b/src/server/modules/auth/revocation.ts @@ -0,0 +1,20 @@ +import "server-only"; +import { db } from "@/server/db"; + +/** Adds a token id to the revocation list until the token would have expired anyway. */ +export async function revokeToken(jti: string | undefined, expSeconds: number | undefined): Promise { + if (!jti || !expSeconds) return; + const expiresAt = new Date(expSeconds * 1000); + if (expiresAt.getTime() <= Date.now()) return; + await db.revokedToken.upsert({ where: { jti }, create: { jti, expiresAt }, update: {} }); +} + +export async function isRevoked(jti: string | undefined): Promise { + if (!jti) return false; + return (await db.revokedToken.count({ where: { jti } })) > 0; +} + +export async function purgeExpiredRevocations(now = new Date()): Promise { + const { count } = await db.revokedToken.deleteMany({ where: { expiresAt: { lt: now } } }); + return count; +} diff --git a/src/server/modules/eta/eta.service.ts b/src/server/modules/eta/eta.service.ts new file mode 100644 index 0000000..4873d55 --- /dev/null +++ b/src/server/modules/eta/eta.service.ts @@ -0,0 +1,55 @@ +import "server-only"; +import { db } from "@/server/db"; +import { HIDDEN_STATUSES, computeVehicleEta, type Directions, type EtaVehicle, type VehicleEta } from "@/server/domain/eta"; +import type { LatLng } from "@/server/domain/geo"; +import { notFound } from "@/server/http/errors"; +import { getVehicleLocations } from "@/server/integrations/firebase"; +import { getDirections } from "@/server/integrations/google-maps"; +import { activeVehicle } from "@/server/modules/vehicles/vehicles"; + +export type DirectionsFn = (origin: LatLng, destination: LatLng) => Promise; + +/** + * ETAs for every vehicle on a stage, in parallel. Returns all statuses; callers decide what to hide + * (`visibleArrivals`) — the cycle needs "Departed" vehicles for trip/state bookkeeping. + */ +export function calculateEtasForStage( + stage: LatLng, + vehicles: EtaVehicle[], + locationsByCarId: Map, + directions: DirectionsFn = getDirections, +): Promise { + return Promise.all( + vehicles.map((vehicle) => computeVehicleEta(vehicle, locationsByCarId.get(vehicle.carId), stage, directions)), + ); +} + +export const visibleArrivals = (etas: VehicleEta[]) => etas.filter((e) => !HIDDEN_STATUSES.has(e.status)); + +export async function getStageEta(stageId: number) { + const stage = await db.stage.findUnique({ + where: { id: stageId }, + select: { + name: true, + latitude: true, + longitude: true, + vehicles: { + where: { vehicle: activeVehicle }, + select: { + vehicle: { + select: { id: true, carId: true, plateNumber: true, lastLatitude: true, lastLongitude: true }, + }, + }, + }, + }, + }); + if (!stage) throw notFound("Stage not found"); + + const locations = await getVehicleLocations(); + const etas = await calculateEtasForStage( + stage, + stage.vehicles.map((v) => v.vehicle), + locations, + ); + return { stageName: stage.name, arrivals: visibleArrivals(etas) }; +} diff --git a/src/server/modules/feedback/feedback.service.ts b/src/server/modules/feedback/feedback.service.ts new file mode 100644 index 0000000..4729ff8 --- /dev/null +++ b/src/server/modules/feedback/feedback.service.ts @@ -0,0 +1,46 @@ +import "server-only"; +import { z } from "zod"; +import { db } from "@/server/db"; +import { notFound } from "@/server/http/errors"; +import { id } from "@/server/validation/common"; +import { activeVehicle } from "@/server/modules/vehicles/vehicles"; + +export const submitFeedbackSchema = z.object({ + vehicleId: id("Vehicle ID"), + rating: z.coerce + .number({ error: "Rating must be between 1 and 5" }) + .int("Rating must be between 1 and 5") + .min(1, "Rating must be between 1 and 5") + .max(5, "Rating must be between 1 and 5"), + comment: z + .string() + .trim() + .max(2000, "Comment is too long") + .optional() + .nullable() + .transform((v) => v || null), +}); + +export async function submitFeedback(userId: number, input: z.infer) { + const vehicle = await db.vehicle.findFirst({ where: { id: input.vehicleId, ...activeVehicle }, select: { id: true } }); + if (!vehicle) throw notFound("Vehicle not found."); + await db.feedback.create({ data: { ...input, userId } }); + return { message: "Thank you for your feedback!" }; +} + +export const listVehiclesForFeedback = () => + db.vehicle.findMany({ + where: activeVehicle, + orderBy: { plateNumber: "asc" }, + select: { id: true, plateNumber: true }, + }); + +/** Shape shared by the admin feed and the Matatu-admin vehicle cards. */ +export const feedbackSelect = { + id: true, + rating: true, + comment: true, + createdAt: true, + user: { select: { name: true } }, + vehicle: { select: { plateNumber: true } }, +} as const; diff --git a/src/server/modules/mat-admin/mat-admin.service.ts b/src/server/modules/mat-admin/mat-admin.service.ts new file mode 100644 index 0000000..f1507d1 --- /dev/null +++ b/src/server/modules/mat-admin/mat-admin.service.ts @@ -0,0 +1,130 @@ +import "server-only"; +import { z } from "zod"; +import { db } from "@/server/db"; +import { + PERIODS, + bucketByPeriod, + metresToKm, + periodRangeStart, + startOfUtcDay, + type Period, +} from "@/server/domain/periods"; +import { notFound } from "@/server/http/errors"; +import { feedbackSelect } from "@/server/modules/feedback/feedback.service"; +import { activeVehicle, liveVehicleStatus, vehicleSummarySelect } from "@/server/modules/vehicles/vehicles"; +import { id } from "@/server/validation/common"; + +export const periodQuerySchema = z.object({ + period: z.enum(PERIODS as [Period, ...Period[]]).catch("daily").default("daily"), +}); + +export const statsQuerySchema = z.object({ + date: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/) + .optional() + .catch(undefined), +}); + +export const vehicleIdParamSchema = z.object({ vehicleId: id("Vehicle ID") }); + +const ownedBy = (ownerId: number) => ({ ownerId, ...activeVehicle }); + +async function ownedVehicleIds(ownerId: number): Promise { + const rows = await db.vehicle.findMany({ where: ownedBy(ownerId), select: { id: true } }); + return rows.map((r) => r.id); +} + +export const myVehicles = (ownerId: number) => + db.vehicle.findMany({ where: ownedBy(ownerId), orderBy: { plateNumber: "asc" }, select: vehicleSummarySelect }); + +export async function dashboardData(ownerId: number) { + return { vehicles: await liveVehicleStatus({ ownerId }) }; +} + +export async function dashboardSummary(ownerId: number, now = new Date()) { + const vehicleIds = await ownedVehicleIds(ownerId); + const date = startOfUtcDay(now); + const [distance, trips] = await Promise.all([ + db.dailyDistance.aggregate({ where: { vehicleId: { in: vehicleIds }, date }, _sum: { distanceCovered: true } }), + db.dailyTrip.aggregate({ where: { vehicleId: { in: vehicleIds }, date }, _sum: { tripCount: true } }), + ]); + return { + totalVehicles: vehicleIds.length, + /** Metres, as before. */ + totalDistanceToday: distance._sum.distanceCovered ?? 0, + totalTripsToday: trips._sum.tripCount ?? 0, + }; +} + +/** Per-vehicle stats for one day (defaults to today, UTC) plus the five latest feedback entries. */ +export async function vehicleStats(ownerId: number, day?: string, now = new Date()) { + const date = day ? new Date(`${day}T00:00:00.000Z`) : startOfUtcDay(now); + const vehicles = await db.vehicle.findMany({ + where: ownedBy(ownerId), + orderBy: { plateNumber: "asc" }, + select: { + ...vehicleSummarySelect, + dailyDistances: { where: { date }, select: { distanceCovered: true } }, + dailyTrips: { where: { date }, select: { tripCount: true } }, + feedback: { orderBy: { createdAt: "desc" }, take: 5, select: feedbackSelect }, + }, + }); + + return vehicles.map(({ dailyDistances, dailyTrips, feedback, ...vehicle }) => ({ + ...vehicle, + date: date.toISOString().slice(0, 10), + distanceKm: metresToKm(dailyDistances[0]?.distanceCovered ?? 0), + trips: dailyTrips[0]?.tripCount ?? 0, + recentFeedback: feedback, + })); +} + +async function periodSeries(vehicleIds: number[], period: Period, now: Date) { + const since = periodRangeStart(period, now); + const where = { vehicleId: { in: vehicleIds }, date: { gte: since } }; + const [distances, trips] = await Promise.all([ + db.dailyDistance.findMany({ where, select: { date: true, distanceCovered: true } }), + db.dailyTrip.findMany({ where, select: { date: true, tripCount: true } }), + ]); + return { + distance: bucketByPeriod( + distances.map((d) => ({ date: d.date, value: d.distanceCovered })), + period, + ).map((p) => ({ ...p, total: metresToKm(p.total) })), + trips: bucketByPeriod( + trips.map((t) => ({ date: t.date, value: t.tripCount })), + period, + ), + }; +} + +export async function vehicleChartData(ownerId: number, vehicleId: number, period: Period, now = new Date()) { + const vehicle = await db.vehicle.findFirst({ where: { id: vehicleId, ...ownedBy(ownerId) }, select: { id: true } }); + if (!vehicle) throw notFound("Vehicle not found or you do not have permission to view it."); + return periodSeries([vehicle.id], period, now); +} + +/** `{ [period]: { distance (km), trips } }` across the whole fleet — used for the CSV report. */ +export async function fleetAggregate(ownerId: number, period: Period, now = new Date()) { + const vehicleIds = await ownedVehicleIds(ownerId); + if (vehicleIds.length === 0) return {}; + const { distance, trips } = await periodSeries(vehicleIds, period, now); + return combineSeries(distance, trips); +} + +export function combineSeries( + distance: { period: string; total: number }[], + trips: { period: string; total: number }[], +): Record { + const periods = [...new Set([...distance.map((d) => d.period), ...trips.map((t) => t.period)])].sort(); + return Object.fromEntries( + periods.map((p) => [ + p, + { + distance: distance.find((d) => d.period === p)?.total ?? 0, + trips: trips.find((t) => t.period === p)?.total ?? 0, + }, + ]), + ); +} diff --git a/src/server/modules/stages/stages.service.ts b/src/server/modules/stages/stages.service.ts new file mode 100644 index 0000000..3af7054 --- /dev/null +++ b/src/server/modules/stages/stages.service.ts @@ -0,0 +1,39 @@ +import "server-only"; +import { db } from "@/server/db"; +import { badRequest, notFound } from "@/server/http/errors"; + +const stageSelect = { id: true, name: true, latitude: true, longitude: true } as const; + +export const listStages = () => db.stage.findMany({ orderBy: { name: "asc" }, select: stageSelect }); + +async function requireStage(stageId: number) { + const stage = await db.stage.findUnique({ where: { id: stageId }, select: stageSelect }); + if (!stage) throw notFound("Stage not found"); + return stage; +} + +export async function getSubscriptionStatus(userId: number, stageId: number) { + const count = await db.stageSubscription.count({ where: { userId, stageId } }); + return { isSubscribed: count > 0 }; +} + +export async function subscribe(userId: number, stageId: number) { + const user = await db.user.findUnique({ where: { id: userId }, select: { phoneNumber: true } }); + if (!user) throw notFound("User not found"); + if (!user.phoneNumber) { + throw badRequest("Please add a phone number to your profile to subscribe for alerts."); + } + const stage = await requireStage(stageId); + await db.stageSubscription.upsert({ + where: { userId_stageId: { userId, stageId } }, + create: { userId, stageId }, + update: {}, + }); + return { message: `Successfully subscribed to alerts for ${stage.name}.` }; +} + +export async function unsubscribe(userId: number, stageId: number) { + const stage = await requireStage(stageId); + await db.stageSubscription.deleteMany({ where: { userId, stageId } }); + return { message: `Successfully unsubscribed from alerts for ${stage.name}.` }; +} diff --git a/src/server/modules/vehicles/vehicles.ts b/src/server/modules/vehicles/vehicles.ts new file mode 100644 index 0000000..c35c1d6 --- /dev/null +++ b/src/server/modules/vehicles/vehicles.ts @@ -0,0 +1,76 @@ +import "server-only"; +import type { Prisma } from "@/generated/prisma/client"; +import { Role } from "@/generated/prisma/enums"; +import { db } from "@/server/db"; +import { describeRoute } from "@/server/domain/routes"; +import { badRequest, notFound } from "@/server/http/errors"; +import { getLocationName } from "@/server/integrations/google-maps"; +import { getVehicleLocations } from "@/server/integrations/firebase"; + +/** Vehicles are soft-deleted; every query must go through this filter. */ +export const activeVehicle = { deletedAt: null } satisfies Prisma.VehicleWhereInput; + +export const vehicleSummarySelect = { + id: true, + carId: true, + plateNumber: true, + ownerId: true, + isAtStage: true, + createdAt: true, + updatedAt: true, +} satisfies Prisma.VehicleSelect; + +export type VehicleSummary = Prisma.VehicleGetPayload<{ select: typeof vehicleSummarySelect }>; + +export async function createVehicle(input: { carId: string; plateNumber: string; ownerId: number | null }) { + if (input.ownerId != null) { + const owner = await db.user.findUnique({ where: { id: input.ownerId }, select: { role: true } }); + if (!owner) throw notFound("Owner not found"); + if (owner.role !== Role.MatAdmin) throw badRequest("Vehicles can only be owned by Matatu admins."); + } + return db.vehicle.create({ data: input, select: vehicleSummarySelect }); +} + +export type LiveVehicleRow = { + id: number; + plateNumber: string; + status: "Online" | "Offline"; + gpsStatus: string | null; + location: string; + latitude: number | null; + longitude: number | null; + route: string; +}; + +/** Live status table rows (admin dashboard and Matatu-admin dashboard). */ +export async function liveVehicleStatus(where: Prisma.VehicleWhereInput = {}): Promise { + const [vehicles, locations] = await Promise.all([ + db.vehicle.findMany({ + where: { ...activeVehicle, ...where }, + orderBy: { plateNumber: "asc" }, + select: { + id: true, + carId: true, + plateNumber: true, + stages: { select: { stage: { select: { name: true } } } }, + }, + }), + getVehicleLocations(), + ]); + + return Promise.all( + vehicles.map(async (vehicle) => { + const location = locations.get(vehicle.carId); + return { + id: vehicle.id, + plateNumber: vehicle.plateNumber, + status: location ? "Online" : "Offline", + gpsStatus: location?.gpsStatus ?? null, + location: location ? await getLocationName(location) : "Offline", + latitude: location?.latitude ?? null, + longitude: location?.longitude ?? null, + route: describeRoute(vehicle.stages.map((s) => s.stage.name)), + } satisfies LiveVehicleRow; + }), + ); +} diff --git a/src/server/validation/common.ts b/src/server/validation/common.ts new file mode 100644 index 0000000..8ce9364 --- /dev/null +++ b/src/server/validation/common.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +/** Lower-cased, trimmed email (express-validator's normalizeEmail did the lower-casing before). */ +export const email = (message = "Please provide a valid email") => + z.string({ error: message }).trim().toLowerCase().pipe(z.email(message)); + +/** bcrypt only looks at the first 72 bytes, so longer passwords give a false sense of security. */ +export const newPassword = z + .string({ error: "Password must be at least 6 characters long" }) + .min(6, "Password must be at least 6 characters long") + .max(72, "Password must be at most 72 characters long"); + +export const personName = (message = "Name is required") => + z.string({ error: message }).trim().min(1, message).max(100, "Name is too long"); + +/** Positive integer from a path segment, query string or JSON (accepts "12" or 12). */ +export const id = (label = "ID") => + z.coerce + .number({ error: `${label} must be a positive integer` }) + .int(`${label} must be a positive integer`) + .positive(`${label} must be a positive integer`); + +export const pagination = (defaultLimit = 25, maxLimit = 100) => + z.object({ + limit: z.coerce.number().int().positive().max(maxLimit).catch(defaultLimit).default(defaultLimit), + offset: z.coerce.number().int().min(0).catch(0).default(0), + }); + +export const otpCode = z + .string({ error: "Please enter a valid 6-digit code." }) + .trim() + .regex(/^\d{6}$/, "Please enter a valid 6-digit code."); From f520ce7566bfcdf0f6bbae7a6a29bf16fa8121c7 Mon Sep 17 00:00:00 2001 From: cliffkoome Date: Mon, 19 Oct 2026 20:49:48 +0300 Subject: [PATCH 4/6] Port the pages to React client components Same markup and styles as the old pages (CSS scoped per page), a typed API client with automatic token refresh, role guards, and fixes for responses the old pages misread (paginated lists, feedback feed). The vehicle stats date picker now works. --- src/app/admin/page.tsx | 132 ++++ src/app/admin/stages/page.tsx | 361 +++++++++ src/app/admin/users/page.tsx | 273 +++++++ src/app/auth/success/page.tsx | 35 + src/app/feedback/page.tsx | 144 ++++ src/app/forgot-password/page.tsx | 16 + src/app/forgot-password/reset-form.tsx | 157 ++++ src/app/globals.css | 40 + src/app/home-client.tsx | 147 ++++ src/app/layout.tsx | 28 + src/app/login/login-form.tsx | 201 +++++ src/app/login/page.tsx | 16 + src/app/mat-admin/page.tsx | 101 +++ src/app/mat-admin/vehicles/page.tsx | 139 ++++ src/app/mat-admin/vehicles/vehicle-chart.tsx | 116 +++ src/app/not-found.tsx | 15 + src/app/page.tsx | 6 + src/app/profile/page.tsx | 256 +++++++ src/app/signup/page.tsx | 94 +++ src/app/stages/[stageId]/page.tsx | 166 +++++ src/app/stages/page.tsx | 63 ++ src/components/dashboard-header.tsx | 102 +++ src/components/icon.tsx | 8 + src/components/live-vehicle-table.tsx | 66 ++ src/components/passenger-header.tsx | 68 ++ src/lib/client/api.ts | 122 ++++ src/lib/client/csv.ts | 17 + src/lib/client/hooks.ts | 64 ++ src/lib/client/session.ts | 40 + src/lib/client/types.ts | 82 +++ src/styles/pages/admin-stages.css | 536 ++++++++++++++ src/styles/pages/admin-users.css | 725 +++++++++++++++++++ src/styles/pages/admin.css | 500 +++++++++++++ src/styles/pages/auth.css | 262 +++++++ src/styles/pages/feedback.css | 303 ++++++++ src/styles/pages/home.css | 336 +++++++++ src/styles/pages/mat-admin-vehicles.css | 423 +++++++++++ src/styles/pages/mat-admin.css | 254 +++++++ src/styles/pages/profile.css | 408 +++++++++++ src/styles/pages/signup.css | 166 +++++ src/styles/pages/stage-info.css | 264 +++++++ src/styles/pages/stages.css | 243 +++++++ 42 files changed, 7495 insertions(+) create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/admin/stages/page.tsx create mode 100644 src/app/admin/users/page.tsx create mode 100644 src/app/auth/success/page.tsx create mode 100644 src/app/feedback/page.tsx create mode 100644 src/app/forgot-password/page.tsx create mode 100644 src/app/forgot-password/reset-form.tsx create mode 100644 src/app/globals.css create mode 100644 src/app/home-client.tsx create mode 100644 src/app/layout.tsx create mode 100644 src/app/login/login-form.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/app/mat-admin/page.tsx create mode 100644 src/app/mat-admin/vehicles/page.tsx create mode 100644 src/app/mat-admin/vehicles/vehicle-chart.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/profile/page.tsx create mode 100644 src/app/signup/page.tsx create mode 100644 src/app/stages/[stageId]/page.tsx create mode 100644 src/app/stages/page.tsx create mode 100644 src/components/dashboard-header.tsx create mode 100644 src/components/icon.tsx create mode 100644 src/components/live-vehicle-table.tsx create mode 100644 src/components/passenger-header.tsx create mode 100644 src/lib/client/api.ts create mode 100644 src/lib/client/csv.ts create mode 100644 src/lib/client/hooks.ts create mode 100644 src/lib/client/session.ts create mode 100644 src/lib/client/types.ts create mode 100644 src/styles/pages/admin-stages.css create mode 100644 src/styles/pages/admin-users.css create mode 100644 src/styles/pages/admin.css create mode 100644 src/styles/pages/auth.css create mode 100644 src/styles/pages/feedback.css create mode 100644 src/styles/pages/home.css create mode 100644 src/styles/pages/mat-admin-vehicles.css create mode 100644 src/styles/pages/mat-admin.css create mode 100644 src/styles/pages/profile.css create mode 100644 src/styles/pages/signup.css create mode 100644 src/styles/pages/stage-info.css create mode 100644 src/styles/pages/stages.css diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..dae634b --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,132 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; +import "@/styles/pages/admin.css"; +import { ADMIN_LINKS, DashboardHeader } from "@/components/dashboard-header"; +import { Icon } from "@/components/icon"; +import { LiveVehicleTable } from "@/components/live-vehicle-table"; +import { api } from "@/lib/client/api"; +import { usePolling, useProfile, useRoleGuard } from "@/lib/client/hooks"; +import type { FeedbackItem, LiveVehicleRow, Page, Stage } from "@/lib/client/types"; + +const REFRESH_MS = 60_000; + +export default function AdminDashboardPage() { + const ready = useRoleGuard("Admin"); + const profile = useProfile(ready); + const [vehicles, setVehicles] = useState(null); + const [vehiclesError, setVehiclesError] = useState(false); + const [stageCount, setStageCount] = useState(null); + const [feedback, setFeedback] = useState(null); + const [feedbackError, setFeedbackError] = useState(false); + + const loadDashboard = useCallback(async () => { + try { + const [data, stages] = await Promise.all([ + api<{ vehicles: LiveVehicleRow[] }>("/api/admin/dashboard-data"), + api("/api/stages"), + ]); + setVehicles(data.vehicles); + setStageCount(stages.length); + setVehiclesError(false); + } catch { + setVehiclesError(true); + } + }, []); + + usePolling(loadDashboard, REFRESH_MS, ready); + + useEffect(() => { + if (!ready) return; + api>("/api/admin/feedback?limit=10") + .then((r) => setFeedback(r.feedback)) + .catch(() => setFeedbackError(true)); + }, [ready]); + + const online = vehicles?.filter((v) => v.status === "Online").length; + + return ( +

    + Admin Dashboard · MatConnect +
    + + +
    +
    +

    Admin Dashboard

    +

    Welcome back! Here's an overview of the system.

    +
    + +
    + + + +
    + +
    +
    +
    +
    +

    Live Vehicle Status

    + + Manage + +
    + +
    +
    + + +
    +
    +
    +
    + ); +} + +function Stat({ icon, tone, label, value }: { icon: string; tone: string; label: string; value?: number }) { + return ( +
    +
    + +
    +
    +

    {label}

    + {value ?? "--"} +
    +
    + ); +} diff --git a/src/app/admin/stages/page.tsx b/src/app/admin/stages/page.tsx new file mode 100644 index 0000000..71c99e8 --- /dev/null +++ b/src/app/admin/stages/page.tsx @@ -0,0 +1,361 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react"; +import "@/styles/pages/admin-stages.css"; +import { ADMIN_LINKS, DashboardHeader } from "@/components/dashboard-header"; +import { Icon } from "@/components/icon"; +import { api, errorMessage } from "@/lib/client/api"; +import { useProfile, useRoleGuard } from "@/lib/client/hooks"; +import type { Page, StageWithVehicles, VehicleSummary } from "@/lib/client/types"; + +type MatAdmin = { id: number; name: string }; + +export default function ManageStagesPage() { + const ready = useRoleGuard("Admin"); + const profile = useProfile(ready); + + const [stages, setStages] = useState(null); + const [vehicles, setVehicles] = useState([]); + const [matAdmins, setMatAdmins] = useState([]); + const [loadError, setLoadError] = useState(null); + const [selectedId, setSelectedId] = useState(null); + const [assignId, setAssignId] = useState(""); + + const [stageModal, setStageModal] = useState(false); + const [stageForm, setStageForm] = useState({ name: "", latitude: "", longitude: "" }); + const [vehicleModal, setVehicleModal] = useState(false); + const [vehicleForm, setVehicleForm] = useState({ plateNumber: "", carId: "", ownerId: "" }); + + const load = useCallback( + () => + Promise.all([ + api>("/api/admin/stages?limit=100"), + api>("/api/admin/vehicles?limit=100"), + api("/api/admin/mat-admins"), + ]).then( + ([s, v, m]) => { + setStages(s.stages); + setVehicles(v.vehicles); + setMatAdmins(m); + setLoadError(null); + }, + (err: unknown) => setLoadError(errorMessage(err)), + ), + [], + ); + + useEffect(() => { + if (ready) load(); + }, [ready, load]); + + const selected = stages?.find((s) => s.id === selectedId) ?? null; + const assignable = useMemo(() => { + const assigned = new Set(selected?.vehicles.map((v) => v.id)); + return vehicles.filter((v) => !assigned.has(v.id)); + }, [selected, vehicles]); + + const patchStage = (id: number, update: (s: StageWithVehicles) => StageWithVehicles) => + setStages((list) => list?.map((s) => (s.id === id ? update(s) : s)) ?? null); + + async function addStage(e: FormEvent) { + e.preventDefault(); + try { + const created = await api("/api/admin/stages", { + method: "POST", + body: { name: stageForm.name, latitude: Number(stageForm.latitude), longitude: Number(stageForm.longitude) }, + }); + setStages((list) => [...(list ?? []), created].sort((a, b) => a.name.localeCompare(b.name))); + setSelectedId(created.id); + setStageModal(false); + setStageForm({ name: "", latitude: "", longitude: "" }); + } catch (err) { + alert(`Error creating stage: ${errorMessage(err)}`); + } + } + + async function addVehicle(e: FormEvent) { + e.preventDefault(); + try { + const created = await api("/api/admin/vehicles", { + method: "POST", + body: { + plateNumber: vehicleForm.plateNumber, + carId: vehicleForm.carId, + ownerId: vehicleForm.ownerId ? Number(vehicleForm.ownerId) : null, + }, + }); + setVehicles((list) => [...list, created].sort((a, b) => a.plateNumber.localeCompare(b.plateNumber))); + setVehicleModal(false); + setVehicleForm({ plateNumber: "", carId: "", ownerId: "" }); + } catch (err) { + alert(`Error creating vehicle: ${errorMessage(err)}`); + } + } + + async function assign() { + if (!selected || !assignId) return; + const vehicle = vehicles.find((v) => v.id === Number(assignId)); + try { + await api("/api/admin/stages/assign-vehicle", { + method: "POST", + body: { stageId: selected.id, vehicleId: Number(assignId) }, + }); + if (vehicle) patchStage(selected.id, (s) => ({ ...s, vehicles: [...s.vehicles, vehicle] })); + setAssignId(""); + } catch (err) { + alert(`Error assigning vehicle: ${errorMessage(err)}`); + } + } + + async function unassign(vehicleId: number) { + if (!selected) return; + try { + await api(`/api/admin/stages/${selected.id}/vehicles/${vehicleId}`, { method: "DELETE" }); + patchStage(selected.id, (s) => ({ ...s, vehicles: s.vehicles.filter((v) => v.id !== vehicleId) })); + } catch (err) { + alert(`Error removing vehicle: ${errorMessage(err)}`); + } + } + + async function deleteVehicle(vehicle: VehicleSummary) { + if (!confirm(`Delete vehicle ${vehicle.plateNumber}? It will be removed from every stage.`)) return; + try { + await api(`/api/admin/vehicles/${vehicle.id}`, { method: "DELETE" }); + await load(); + } catch (err) { + alert(`Error deleting vehicle: ${errorMessage(err)}`); + } + } + + return ( +
    + Manage Stages · MatConnect +
    + + +
    +
    +
    +

    Stages

    +
    +
    +
      + {loadError ? ( +

      Failed to load data: {loadError}

      + ) : !stages ? ( +

      Loading stages...

      + ) : stages.length === 0 ? ( +

      No stages found. Add one to get started.

      + ) : ( + stages.map((stage) => ( +
    • setSelectedId(stage.id)} + onKeyDown={(e) => e.key === "Enter" && setSelectedId(stage.id)} + tabIndex={0} + > +
      + +
      +

      {stage.name}

      + +
    • + )) + )} +
    +
    +
    + +
    +
    + +
    +
    +

    {selected?.name ?? "Select a Stage"}

    +

    + {selected ? ( + <> + Lat: {selected.latitude}, Long: {selected.longitude} + + ) : ( + "Details will appear here." + )} +

    +
    +
    +

    Assigned Vehicles

    +
      + {!selected ? ( +

      Select a stage to see assigned vehicles.

      + ) : selected.vehicles.length === 0 ? ( +

      No vehicles assigned to this stage.

      + ) : ( + selected.vehicles.map((vehicle) => ( +
    • +
      + +

      {vehicle.plateNumber}

      +
      +
      + + +
      +
    • + )) + )} +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + + {vehicleModal && ( + setVehicleModal(false)}> +
    +
    +
    + + setVehicleForm({ ...vehicleForm, plateNumber: e.target.value })} + /> +
    +
    + + setVehicleForm({ ...vehicleForm, carId: e.target.value })} + /> +
    +
    + + +
    +
    +
    + + +
    +
    +
    + )} + + {stageModal && ( + setStageModal(false)}> +
    +
    +
    + + setStageForm({ ...stageForm, name: e.target.value })} + /> +
    +
    +
    + + setStageForm({ ...stageForm, latitude: e.target.value })} + /> +
    +
    + + setStageForm({ ...stageForm, longitude: e.target.value })} + /> +
    +
    +
    +
    + + +
    +
    +
    + )} +
    + ); +} + +function Modal({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) { + return ( +
    e.target === e.currentTarget && onClose()}> +
    +
    +

    {title}

    + +
    + {children} +
    +
    + ); +} diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx new file mode 100644 index 0000000..5afee5f --- /dev/null +++ b/src/app/admin/users/page.tsx @@ -0,0 +1,273 @@ +"use client"; + +import { useCallback, useEffect, useState, type FormEvent } from "react"; +import "@/styles/pages/admin-users.css"; +import { ADMIN_LINKS, DashboardHeader } from "@/components/dashboard-header"; +import { Icon } from "@/components/icon"; +import { api, errorMessage } from "@/lib/client/api"; +import { useProfile, useRoleGuard } from "@/lib/client/hooks"; +import type { Page, Role, UserRow } from "@/lib/client/types"; + +const ROLE_FILTERS: { label: string; role: Role | "" }[] = [ + { label: "All", role: "" }, + { label: "Admins", role: "Admin" }, + { label: "MatAdmins", role: "MatAdmin" }, + { label: "Passengers", role: "Passenger" }, +]; + +const emptyForm = { name: "", email: "", password: "", role: "MatAdmin" as "MatAdmin" | "Admin" }; + +export default function ManageUsersPage() { + const ready = useRoleGuard("Admin"); + const profile = useProfile(ready); + + const [query, setQuery] = useState(""); + const [debounced, setDebounced] = useState(""); + const [role, setRole] = useState(""); + const [users, setUsers] = useState(null); + const [total, setTotal] = useState(0); + const [error, setError] = useState(false); + + const [modalOpen, setModalOpen] = useState(false); + const [form, setForm] = useState(emptyForm); + const [showPassword, setShowPassword] = useState(false); + const [saving, setSaving] = useState(false); + + useEffect(() => { + const t = setTimeout(() => setDebounced(query), 300); + return () => clearTimeout(t); + }, [query]); + + const fetchUsers = useCallback(() => { + const params = new URLSearchParams({ q: debounced, limit: "100" }); + if (role) params.set("role", role); + return api>(`/api/admin/users?${params}`); + }, [debounced, role]); + + const apply = (res: Page<"users", UserRow>) => { + setUsers(res.users); + setTotal(res.total); + setError(false); + }; + + const load = async () => { + try { + apply(await fetchUsers()); + } catch { + setError(true); + } + }; + + useEffect(() => { + if (!ready) return; + fetchUsers() + .then(apply) + .catch(() => setError(true)); + }, [ready, fetchUsers]); + + async function toggleBlock(user: UserRow) { + const block = !user.disabled; + if (!confirm(`Are you sure you want to ${block ? "block" : "unblock"} ${user.name}?`)) return; + try { + await api(`/api/admin/users/${user.id}`, { method: "PUT", body: { disabled: block } }); + await load(); + } catch (err) { + alert(`Error updating user: ${errorMessage(err)}`); + } + } + + async function remove(user: UserRow) { + if (!confirm(`Permanently delete ${user.name}? This action cannot be undone.`)) return; + try { + await api(`/api/admin/users/${user.id}`, { method: "DELETE" }); + setUsers((list) => list?.filter((u) => u.id !== user.id) ?? null); + setTotal((t) => t - 1); + } catch (err) { + alert(`Error deleting user: ${errorMessage(err)}`); + } + } + + async function createUser(e: FormEvent) { + e.preventDefault(); + setSaving(true); + try { + await api("/api/admin/users", { method: "POST", body: form }); + closeModal(); + await load(); + } catch (err) { + alert(`Error creating user: ${errorMessage(err)}`); + } finally { + setSaving(false); + } + } + + const closeModal = () => { + setModalOpen(false); + setForm(emptyForm); + setShowPassword(false); + }; + + return ( +
    + Manage Users · MatConnect +
    + + +
    +
    +
    +

    System Users{users ? ` (${total})` : ""}

    +
    +
    + + setQuery(e.target.value)} + aria-label="Search users" + /> +
    +
    + {ROLE_FILTERS.map((f) => ( + + ))} +
    + +
    +
    +
    + + + + + + + + + + + + {error ? ( + + + + ) : !users ? ( + + + + ) : users.length === 0 ? ( + + + + ) : ( + users.map((user) => ( + + + + + + + + )) + )} + +
    NameEmailRoleStatusActions
    + Could not load users. +
    Loading users...
    No users found.
    {user.name}{user.email} + {user.role} + + + {user.disabled ? "Blocked" : "Active"} + + + + +
    +
    +
    +
    +
    + + {modalOpen && ( +
    e.target === e.currentTarget && closeModal()}> +
    +
    +

    Add New User

    + +
    +
    +
    +
    + + setForm({ ...form, name: e.target.value })} /> +
    +
    + + setForm({ ...form, email: e.target.value })} + /> +
    +
    + +
    + setForm({ ...form, password: e.target.value })} + /> + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    + )} +
    + ); +} diff --git a/src/app/auth/success/page.tsx b/src/app/auth/success/page.tsx new file mode 100644 index 0000000..6883084 --- /dev/null +++ b/src/app/auth/success/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; +import { homePathFor, setSession } from "@/lib/client/session"; +import type { Role } from "@/lib/client/types"; + +const ROLES: Role[] = ["Passenger", "Admin", "MatAdmin"]; + +/** + * Landing page after Google sign-in. The server puts the access token in the URL *fragment* + * (#accessToken=…&role=…). The old page read the query string instead, so this flow was broken. + */ +export default function AuthSuccessPage() { + useEffect(() => { + const params = new URLSearchParams(window.location.hash.slice(1)); + const accessToken = params.get("accessToken"); + const role = params.get("role") as Role | null; + // Remove the token from the address bar / history straight away. + window.history.replaceState(null, "", window.location.pathname); + + if (accessToken && role && ROLES.includes(role)) { + setSession(accessToken, role); + window.location.replace(homePathFor(role)); + } else { + window.location.replace("/login?error=auth_failed"); + } + }, []); + + return ( +
    + Signing in… · MatConnect +

    Please wait, you are being redirected…

    +
    + ); +} diff --git a/src/app/feedback/page.tsx b/src/app/feedback/page.tsx new file mode 100644 index 0000000..81e4d12 --- /dev/null +++ b/src/app/feedback/page.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { Fragment, useEffect, useState, type FormEvent } from "react"; +import "@/styles/pages/feedback.css"; +import { PassengerHeader } from "@/components/passenger-header"; +import { api, errorMessage } from "@/lib/client/api"; +import { useProfile, useRoleGuard } from "@/lib/client/hooks"; + +type VehicleOption = { id: number; plateNumber: string }; + +export default function FeedbackPage() { + const ready = useRoleGuard("Passenger"); + const profile = useProfile(ready); + const [vehicles, setVehicles] = useState(null); + const [loadError, setLoadError] = useState(false); + const [vehicleId, setVehicleId] = useState(""); + const [rating, setRating] = useState(0); + const [comment, setComment] = useState(""); + const [busy, setBusy] = useState(false); + const [notice, setNotice] = useState<{ text: string; ok: boolean } | null>(null); + + useEffect(() => { + if (!ready) return; + api("/api/feedback/vehicles") + .then(setVehicles) + .catch(() => setLoadError(true)); + }, [ready]); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + if (!vehicleId) return setNotice({ text: "Please select a vehicle.", ok: false }); + if (!rating) return setNotice({ text: "Please provide a star rating.", ok: false }); + setBusy(true); + try { + const res = await api<{ message: string }>("/api/feedback", { + method: "POST", + body: { vehicleId: Number(vehicleId), rating, comment }, + }); + setNotice({ text: res.message, ok: true }); + setVehicleId(""); + setRating(0); + setComment(""); + } catch (error) { + setNotice({ text: `Error submitting feedback: ${errorMessage(error)}`, ok: false }); + } finally { + setBusy(false); + } + } + + return ( +
    + Feedback · MatConnect +
    + + +
    +
    +
    +

    Your Feedback Matters

    +

    Help us improve your matatu experience. Share your thoughts on your recent ride.

    +
    +
    +
    + + +
    +
    + + {/* Visual order 1→5 comes from `flex-direction: row-reverse` in the stylesheet. */} +
    + {[5, 4, 3, 2, 1].map((value) => ( + // Inputs and labels must stay direct siblings for the stylesheet's `label:hover ~ label`. + + setRating(value)} + /> + + + ))} +
    +
    +
    + +