From edfdeb5117318db7d4ae7af9a72a26b04b7c43d1 Mon Sep 17 00:00:00 2001 From: Deco Vander Date: Wed, 2 Jul 2025 23:27:22 -0400 Subject: [PATCH] Initial commit: ICE Watch Michigan community safety tool - Node.js/Express backend with SQLite database - Interactive map with real-time location tracking - MapBox API integration for fast geocoding - Admin panel for content moderation - 24-hour auto-expiring reports - Deployment scripts for Debian 12 ARM64 - Caddy reverse proxy with automatic HTTPS --- .env.example | 16 + .gitignore | 28 + README.md | 139 ++ package-lock.json | 2705 ++++++++++++++++++++++++++++++++++++++ package.json | 28 + public/admin.html | 228 ++++ public/admin.js | 304 +++++ public/app-google.js | 433 ++++++ public/app-mapbox.js | 431 ++++++ public/app.js | 408 ++++++ public/index.html | 70 + public/style.css | 137 ++ scripts/Caddyfile | 38 + scripts/deploy.sh | 41 + scripts/icewatch.service | 24 + server.js | 293 +++++ 16 files changed, 5323 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/admin.html create mode 100644 public/admin.js create mode 100644 public/app-google.js create mode 100644 public/app-mapbox.js create mode 100644 public/app.js create mode 100644 public/index.html create mode 100644 public/style.css create mode 100644 scripts/Caddyfile create mode 100644 scripts/deploy.sh create mode 100644 scripts/icewatch.service create mode 100644 server.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a0227ff --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# ICE Watch Environment Variables +# Copy this file to .env and fill in your actual values + +# MapBox API Configuration (Required for fast geocoding) +# Get your free token at: https://account.mapbox.com/access-tokens/ +MAPBOX_ACCESS_TOKEN=pk.your_mapbox_token_here + +# Admin Configuration +# Change this to a secure password for admin panel access +ADMIN_PASSWORD=your_secure_password_here + +# Optional: Google Maps fallback (if you have it) +# GOOGLE_MAPS_API_KEY=your_google_key_here + +# Server Configuration +PORT=3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26ba780 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Environment variables +.env +.env.local +.env.production + +# Database +*.db +*.sqlite +*.sqlite3 + +# Node modules +node_modules/ + +# Logs +*.log +npm-debug.log* + +# OS generated files +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ + +# Temporary files +*.tmp +*.temp diff --git a/README.md b/README.md new file mode 100644 index 0000000..ffd8273 --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +# ICE Watch Michigan + +A community-driven web application for tracking ICE activity locations in Michigan. Reports automatically expire after 24 hours to maintain current information. + +## Features + +- πŸ—ΊοΈ **Interactive Map** - Real-time location tracking centered on Grand Rapids +- ⚑ **Fast Geocoding** - Lightning-fast address lookup with MapBox API +- πŸ”„ **Auto-Expiration** - Reports automatically removed after 24 hours +- πŸ‘¨β€πŸ’Ό **Admin Panel** - Manage and moderate location reports +- πŸ“± **Responsive Design** - Works on desktop and mobile devices +- πŸ”’ **Privacy-Focused** - No user tracking, community safety oriented + +## Quick Start + +### Prerequisites +- Node.js 18+ +- MapBox API token (free tier available) + +### Local Development + +1. **Clone the repository:** + ```bash + git clone https://github.com/yourusername/icewatch.git + cd icewatch + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` + +3. **Configure environment variables:** + ```bash + cp .env.example .env + # Edit .env with your MapBox token + ``` + +4. **Start the server:** + ```bash + npm start + ``` + +5. **Visit the application:** + ``` + http://localhost:3000 + ``` + +## Environment Variables + +```bash +# Required for fast geocoding +MAPBOX_ACCESS_TOKEN=pk.your_mapbox_token_here + +# Admin panel access +ADMIN_PASSWORD=your_secure_password + +# Server configuration +PORT=3000 +``` + +## Deployment + +### Automated Deployment (Debian 12 ARM64) + +1. **Run the deployment script on your server:** + ```bash + curl -sSL https://raw.githubusercontent.com/yourusername/icewatch/main/scripts/deploy.sh | bash + ``` + +2. **Deploy your application:** + ```bash + git clone https://github.com/yourusername/icewatch.git /opt/icewatch + cd /opt/icewatch + npm install + ``` + +3. **Configure environment:** + ```bash + cp .env.example .env + nano .env # Add your API keys + ``` + +4. **Start services:** + ```bash + sudo systemctl enable icewatch + sudo systemctl start icewatch + sudo systemctl enable caddy + sudo systemctl start caddy + ``` + +### Manual Deployment + +See `docs/deployment.md` for detailed manual deployment instructions. + +## API Endpoints + +- `GET /api/locations` - Get active location reports +- `POST /api/locations` - Submit new location report +- `GET /api/config` - Get API configuration +- `GET /admin` - Admin panel (password protected) + +## Technology Stack + +- **Backend:** Node.js, Express.js, SQLite +- **Frontend:** Vanilla JavaScript, Leaflet.js +- **Geocoding:** MapBox API (with Nominatim fallback) +- **Reverse Proxy:** Caddy (automatic HTTPS) +- **Database:** SQLite (lightweight, serverless) + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## Security + +- API keys are stored in environment variables +- Admin routes are password protected +- Database queries use parameterized statements +- HTTPS enforced in production + +## License + +MIT License - see LICENSE file for details + +## Support + +This is a community safety tool. For issues or questions: +- Create a GitHub issue +- Check existing documentation +- Review security guidelines + +--- + +**⚠️ Safety Notice:** This tool is for community awareness. Always prioritize personal safety and know your rights. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2648dab --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2705 @@ +{ + "name": "icewatch", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "icewatch", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.0.1", + "express": "^4.18.2", + "node-cron": "^3.0.3", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "nodemon": "^3.0.1" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/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", + "optional": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 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", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz", + "integrity": "sha512-GLjkduuAL7IMJg/ZnOPm9AnWKJ82mSE2tzXLaJ/6hD6DhwGfZaXG77oB8qbReyiczNxnbxQKyh0OE5mXq0bAHA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/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", + "optional": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/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": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/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", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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", + "optional": true + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/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/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-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==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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", + "engines": { + "node": ">=0.10.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", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "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", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "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, + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/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, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/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" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", + "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/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", + "optional": true + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "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/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-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9479a43 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "icewatch", + "version": "1.0.0", + "description": "ICE location tracking website for Michigan", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.0.1", + "express": "^4.18.2", + "node-cron": "^3.0.3", + "sqlite3": "^5.1.6" + }, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "keywords": [ + "ice", + "tracking", + "michigan", + "map" + ], + "author": "Your Name", + "license": "MIT" +} diff --git a/public/admin.html b/public/admin.html new file mode 100644 index 0000000..98274e5 --- /dev/null +++ b/public/admin.html @@ -0,0 +1,228 @@ + + + + + + ICE Watch Admin + + + + +
+ + + + +
+
+

🚨 ICE Watch Admin Panel

+
+ + +
+
+ +
+
+
0
+
Total Reports
+
+
+
0
+
Active (24hrs)
+
+
+
0
+
Expired
+
+
+ +
+

All Location Reports

+ + + + + + + + + + + + + + + + +
IDStatusAddressDescriptionReportedActions
Loading...
+
+
+
+ + + + diff --git a/public/admin.js b/public/admin.js new file mode 100644 index 0000000..725701a --- /dev/null +++ b/public/admin.js @@ -0,0 +1,304 @@ +document.addEventListener('DOMContentLoaded', () => { + let authToken = localStorage.getItem('adminToken'); + let allLocations = []; + let editingRowId = null; + + const loginSection = document.getElementById('login-section'); + const adminSection = document.getElementById('admin-section'); + const loginForm = document.getElementById('login-form'); + const loginMessage = document.getElementById('login-message'); + const logoutBtn = document.getElementById('logout-btn'); + const refreshBtn = document.getElementById('refresh-btn'); + const locationsTableBody = document.getElementById('locations-tbody'); + + // Check if already logged in + if (authToken) { + showAdminSection(); + loadLocations(); + } + + // Login form handler + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const password = document.getElementById('password').value; + + try { + const response = await fetch('/api/admin/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }) + }); + + const data = await response.json(); + + if (response.ok) { + authToken = data.token; + localStorage.setItem('adminToken', authToken); + showAdminSection(); + loadLocations(); + } else { + showMessage(loginMessage, data.error, 'error'); + } + } catch (error) { + showMessage(loginMessage, 'Login failed. Please try again.', 'error'); + } + }); + + // Logout handler + logoutBtn.addEventListener('click', () => { + authToken = null; + localStorage.removeItem('adminToken'); + showLoginSection(); + }); + + // Refresh button handler + refreshBtn.addEventListener('click', () => { + loadLocations(); + }); + + function showLoginSection() { + loginSection.style.display = 'block'; + adminSection.style.display = 'none'; + document.getElementById('password').value = ''; + hideMessage(loginMessage); + } + + function showAdminSection() { + loginSection.style.display = 'none'; + adminSection.style.display = 'block'; + } + + function showMessage(element, message, type) { + element.textContent = message; + element.className = `message ${type}`; + element.style.display = 'block'; + setTimeout(() => hideMessage(element), 5000); + } + + function hideMessage(element) { + element.style.display = 'none'; + } + + async function loadLocations() { + try { + const response = await fetch('/api/admin/locations', { + headers: { 'Authorization': `Bearer ${authToken}` } + }); + + if (response.status === 401) { + // Token expired or invalid + authToken = null; + localStorage.removeItem('adminToken'); + showLoginSection(); + return; + } + + if (!response.ok) { + throw new Error('Failed to load locations'); + } + + allLocations = await response.json(); + updateStats(); + renderLocationsTable(); + } catch (error) { + console.error('Error loading locations:', error); + locationsTableBody.innerHTML = 'Error loading locations'; + } + } + + function updateStats() { + const now = new Date(); + const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + + const activeLocations = allLocations.filter(location => + new Date(location.created_at) > twentyFourHoursAgo + ); + + document.getElementById('total-count').textContent = allLocations.length; + document.getElementById('active-count').textContent = activeLocations.length; + document.getElementById('expired-count').textContent = allLocations.length - activeLocations.length; + } + + function renderLocationsTable() { + if (allLocations.length === 0) { + locationsTableBody.innerHTML = 'No locations found'; + return; + } + + const now = new Date(); + const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + + locationsTableBody.innerHTML = allLocations.map(location => { + const isActive = new Date(location.created_at) > twentyFourHoursAgo; + const createdDate = new Date(location.created_at); + const formattedDate = createdDate.toLocaleString(); + + return ` + + ${location.id} + + + ${isActive ? 'ACTIVE' : 'EXPIRED'} + + + ${location.address} + ${location.description || ''} + ${getTimeAgo(location.created_at)} + +
+ + +
+ + + `; + }).join(''); + } + + function renderEditRow(location) { + const row = document.querySelector(`tr[data-id="${location.id}"]`); + const now = new Date(); + const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + const isActive = new Date(location.created_at) > twentyFourHoursAgo; + const createdDate = new Date(location.created_at); + const formattedDate = createdDate.toLocaleString(); + + row.innerHTML = ` + ${location.id} + + + ${isActive ? 'ACTIVE' : 'EXPIRED'} + + + + + + + + + ${getTimeAgo(location.created_at)} + +
+ + +
+ + `; + row.className = 'edit-row'; + } + + window.editLocation = (id) => { + if (editingRowId) { + cancelEdit(); + } + + editingRowId = id; + const location = allLocations.find(loc => loc.id === id); + if (location) { + renderEditRow(location); + } + }; + + window.cancelEdit = () => { + editingRowId = null; + renderLocationsTable(); + }; + + window.saveLocation = async (id) => { + const address = document.getElementById(`edit-address-${id}`).value.trim(); + const description = document.getElementById(`edit-description-${id}`).value.trim(); + + if (!address) { + alert('Address is required'); + return; + } + + try { + // Geocode the address if it was changed + let latitude = null; + let longitude = null; + + const originalLocation = allLocations.find(loc => loc.id === id); + if (address !== originalLocation.address) { + try { + const geoResponse = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`); + const geoData = await geoResponse.json(); + + if (geoData && geoData.length > 0) { + latitude = parseFloat(geoData[0].lat); + longitude = parseFloat(geoData[0].lon); + } + } catch (geoError) { + console.warn('Geocoding failed, keeping original coordinates'); + latitude = originalLocation.latitude; + longitude = originalLocation.longitude; + } + } else { + latitude = originalLocation.latitude; + longitude = originalLocation.longitude; + } + + const response = await fetch(`/api/admin/locations/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify({ address, latitude, longitude, description }) + }); + + if (response.ok) { + editingRowId = null; + loadLocations(); // Reload to get updated data + } else { + const error = await response.json(); + alert(`Error updating location: ${error.error}`); + } + } catch (error) { + console.error('Error saving location:', error); + alert('Error saving location. Please try again.'); + } + }; + + window.deleteLocation = async (id) => { + if (!confirm('Are you sure you want to delete this location report?')) { + return; + } + + try { + const response = await fetch(`/api/admin/locations/${id}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${authToken}` } + }); + + if (response.ok) { + loadLocations(); // Reload the table + } else { + const error = await response.json(); + alert(`Error deleting location: ${error.error}`); + } + } catch (error) { + console.error('Error deleting location:', error); + alert('Error deleting location. Please try again.'); + } + }; + + function getTimeAgo(timestamp) { + const now = new Date(); + const reportTime = new Date(timestamp); + const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60)); + + if (diffInMinutes < 1) return 'just now'; + if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`; + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`; + + const diffInDays = Math.floor(diffInHours / 24); + if (diffInDays < 7) return `${diffInDays} day${diffInDays !== 1 ? 's' : ''} ago`; + + return reportTime.toLocaleDateString(); + } +}); diff --git a/public/app-google.js b/public/app-google.js new file mode 100644 index 0000000..89012ef --- /dev/null +++ b/public/app-google.js @@ -0,0 +1,433 @@ +document.addEventListener('DOMContentLoaded', async () => { + const map = L.map('map').setView([42.9634, -85.6681], 10); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 18, + }).addTo(map); + + // Get API configuration + let config = { hasGoogleMaps: false, googleMapsApiKey: null }; + try { + const configResponse = await fetch('/api/config'); + config = await configResponse.json(); + console.log('πŸ”§ API Configuration:', config.hasGoogleMaps ? 'Google Maps enabled' : 'Using Nominatim fallback'); + } catch (error) { + console.warn('Failed to load API configuration, using fallback'); + } + + const locationForm = document.getElementById('location-form'); + const messageDiv = document.getElementById('message'); + const submitBtn = document.getElementById('submit-btn'); + const submitText = document.getElementById('submit-text'); + const submitLoading = document.getElementById('submit-loading'); + const addressInput = document.getElementById('address'); + const autocompleteList = document.getElementById('autocomplete-list'); + + let autocompleteTimeout; + let selectedIndex = -1; + let autocompleteResults = []; + let currentMarkers = []; + let updateInterval; + let googleAutocompleteService = null; + let googleGeocoderService = null; + + // Initialize Google Services if API key is available + if (config.hasGoogleMaps) { + try { + // Load Google Maps API + await loadGoogleMapsAPI(config.googleMapsApiKey); + googleAutocompleteService = new google.maps.places.AutocompleteService(); + googleGeocoderService = new google.maps.Geocoder(); + console.log('βœ… Google Maps services initialized'); + } catch (error) { + console.warn('❌ Failed to initialize Google Maps, falling back to Nominatim'); + } + } + + function loadGoogleMapsAPI(apiKey) { + return new Promise((resolve, reject) => { + if (window.google) { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`; + script.async = true; + script.defer = true; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + const clearMarkers = () => { + currentMarkers.forEach(marker => { + map.removeLayer(marker); + }); + currentMarkers = []; + }; + + const showMarkers = locations => { + clearMarkers(); + + locations.forEach(({ latitude, longitude, address, description, created_at }) => { + if (latitude && longitude) { + const timeAgo = getTimeAgo(created_at); + const marker = L.marker([latitude, longitude]) + .addTo(map) + .bindPopup(`${address}
${description || 'No additional details'}
Reported ${timeAgo}`); + currentMarkers.push(marker); + } + }); + }; + + const getTimeAgo = (timestamp) => { + const now = new Date(); + const reportTime = new Date(timestamp); + const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60)); + + if (diffInMinutes < 1) return 'just now'; + if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`; + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`; + + return 'over a day ago'; + }; + + const refreshLocations = () => { + fetch('/api/locations') + .then(res => res.json()) + .then(locations => { + showMarkers(locations); + const countElement = document.getElementById('location-count'); + countElement.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`; + + const now = new Date(); + const timeStr = now.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + countElement.title = `Last updated: ${timeStr}`; + }) + .catch(err => { + console.error('Error fetching locations:', err); + document.getElementById('location-count').textContent = 'Error loading locations'; + }); + }; + + refreshLocations(); + updateInterval = setInterval(refreshLocations, 30000); + + // Google Maps Geocoding (Fast!) + const googleGeocode = async (address) => { + const startTime = performance.now(); + console.log(`πŸš€ Google Geocoding: "${address}"`); + + return new Promise((resolve, reject) => { + googleGeocoderService.geocode({ + address: address, + componentRestrictions: { country: 'US', administrativeArea: 'MI' }, + region: 'us' + }, (results, status) => { + const time = (performance.now() - startTime).toFixed(2); + + if (status === 'OK' && results.length > 0) { + const result = results[0]; + console.log(`βœ… Google geocoding successful in ${time}ms`); + console.log(`πŸ“ Found: ${result.formatted_address}`); + + resolve({ + lat: result.geometry.location.lat(), + lon: result.geometry.location.lng(), + display_name: result.formatted_address + }); + } else { + console.log(`❌ Google geocoding failed: ${status} (${time}ms)`); + reject(new Error(`Google geocoding failed: ${status}`)); + } + }); + }); + }; + + // Nominatim Fallback (Slower but free) + const nominatimGeocode = async (address) => { + const startTime = performance.now(); + console.log(`🐌 Nominatim fallback: "${address}"`); + + const searches = [ + address, + `${address}, Michigan`, + `${address}, MI`, + `${address}, Grand Rapids, MI` + ]; + + for (const query of searches) { + try { + const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1&countrycodes=us`); + const data = await response.json(); + + if (data && data.length > 0) { + const time = (performance.now() - startTime).toFixed(2); + console.log(`βœ… Nominatim successful in ${time}ms`); + return data[0]; + } + } catch (error) { + console.warn(`Nominatim query failed: ${query}`); + } + } + + const time = (performance.now() - startTime).toFixed(2); + console.error(`πŸ’₯ All geocoding failed after ${time}ms`); + throw new Error('Address not found'); + }; + + // Main geocoding function + const geocodeAddress = async (address) => { + if (googleGeocoderService) { + try { + return await googleGeocode(address); + } catch (error) { + console.warn('Google geocoding failed, trying Nominatim fallback'); + return await nominatimGeocode(address); + } + } else { + return await nominatimGeocode(address); + } + }; + + // Google Places Autocomplete (Super fast!) + const googleAutocomplete = async (query) => { + const startTime = performance.now(); + console.log(`⚑ Google Autocomplete: "${query}"`); + + return new Promise((resolve) => { + googleAutocompleteService.getPlacePredictions({ + input: query, + componentRestrictions: { country: 'us' }, + types: ['address'], + location: new google.maps.LatLng(42.9634, -85.6681), + radius: 50000 // 50km around Grand Rapids + }, (predictions, status) => { + const time = (performance.now() - startTime).toFixed(2); + + if (status === google.maps.places.PlacesServiceStatus.OK && predictions) { + const michiganResults = predictions.filter(p => + p.description.toLowerCase().includes('mi,') || + p.description.toLowerCase().includes('michigan') + ); + + console.log(`⚑ Google autocomplete: ${michiganResults.length} results in ${time}ms`); + resolve(michiganResults.slice(0, 5).map(p => ({ + display_name: p.description, + place_id: p.place_id + }))); + } else { + console.log(`❌ Google autocomplete failed: ${status} (${time}ms)`); + resolve([]); + } + }); + }); + }; + + // Nominatim Autocomplete Fallback + const nominatimAutocomplete = async (query) => { + console.log(`🐌 Nominatim autocomplete fallback: "${query}"`); + + try { + const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&countrycodes=us&addressdetails=1`); + const data = await response.json(); + + return data.filter(item => + item.display_name.toLowerCase().includes('michigan') || + item.display_name.toLowerCase().includes(', mi,') + ).slice(0, 5); + } catch (error) { + console.error('Nominatim autocomplete failed:', error); + return []; + } + }; + + // Main autocomplete function + const fetchAddressSuggestions = async (query) => { + if (query.length < 3) { + hideAutocomplete(); + return; + } + + let results = []; + + if (googleAutocompleteService) { + try { + results = await googleAutocomplete(query); + } catch (error) { + console.warn('Google autocomplete failed, trying Nominatim'); + } + } + + if (results.length === 0) { + results = await nominatimAutocomplete(query); + } + + autocompleteResults = results; + showAutocomplete(autocompleteResults); + }; + + // Form submission + locationForm.addEventListener('submit', e => { + e.preventDefault(); + + const address = document.getElementById('address').value; + const description = document.getElementById('description').value; + + if (!address) return; + + submitBtn.disabled = true; + submitText.style.display = 'none'; + submitLoading.style.display = 'inline'; + + geocodeAddress(address) + .then(result => { + const { lat, lon } = result; + + return fetch('/api/locations', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + address, + latitude: parseFloat(lat), + longitude: parseFloat(lon), + description, + }), + }); + }) + .then(res => res.json()) + .then(location => { + refreshLocations(); + messageDiv.textContent = 'Location reported successfully!'; + messageDiv.className = 'message success'; + locationForm.reset(); + }) + .catch(err => { + console.error('Error reporting location:', err); + messageDiv.textContent = 'Error reporting location.'; + messageDiv.className = 'message error'; + }) + .finally(() => { + submitBtn.disabled = false; + submitText.style.display = 'inline'; + submitLoading.style.display = 'none'; + messageDiv.style.display = 'block'; + setTimeout(() => { + messageDiv.style.display = 'none'; + }, 3000); + }); + }); + + // Autocomplete UI functions (same as before) + const showAutocomplete = (results) => { + if (results.length === 0) { + hideAutocomplete(); + return; + } + + autocompleteList.innerHTML = ''; + selectedIndex = -1; + + results.forEach((result, index) => { + const item = document.createElement('div'); + item.className = 'autocomplete-item'; + item.textContent = result.display_name; + item.addEventListener('click', () => selectAddress(result)); + autocompleteList.appendChild(item); + }); + + autocompleteList.style.display = 'block'; + }; + + const hideAutocomplete = () => { + autocompleteList.style.display = 'none'; + selectedIndex = -1; + }; + + const selectAddress = (result) => { + addressInput.value = result.display_name; + hideAutocomplete(); + addressInput.focus(); + }; + + const updateSelection = (direction) => { + const items = autocompleteList.querySelectorAll('.autocomplete-item'); + + if (items.length === 0) return; + + items[selectedIndex]?.classList.remove('selected'); + + if (direction === 'down') { + selectedIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0; + } else { + selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1; + } + + items[selectedIndex].classList.add('selected'); + items[selectedIndex].scrollIntoView({ block: 'nearest' }); + }; + + // Event listeners + addressInput.addEventListener('input', (e) => { + clearTimeout(autocompleteTimeout); + const query = e.target.value.trim(); + + if (query.length < 3) { + hideAutocomplete(); + return; + } + + autocompleteTimeout = setTimeout(() => { + fetchAddressSuggestions(query); + }, 200); // Faster debounce with Google APIs + }); + + addressInput.addEventListener('keydown', (e) => { + const isListVisible = autocompleteList.style.display === 'block'; + + switch (e.key) { + case 'ArrowDown': + if (isListVisible) { + e.preventDefault(); + updateSelection('down'); + } + break; + case 'ArrowUp': + if (isListVisible) { + e.preventDefault(); + updateSelection('up'); + } + break; + case 'Enter': + if (isListVisible && selectedIndex >= 0) { + e.preventDefault(); + selectAddress(autocompleteResults[selectedIndex]); + } + break; + case 'Escape': + hideAutocomplete(); + break; + } + }); + + document.addEventListener('click', (e) => { + if (!e.target.closest('.autocomplete-container')) { + hideAutocomplete(); + } + }); + + window.addEventListener('beforeunload', () => { + if (updateInterval) { + clearInterval(updateInterval); + } + }); +}); diff --git a/public/app-mapbox.js b/public/app-mapbox.js new file mode 100644 index 0000000..c7cd7eb --- /dev/null +++ b/public/app-mapbox.js @@ -0,0 +1,431 @@ +document.addEventListener('DOMContentLoaded', async () => { + const map = L.map('map').setView([42.9634, -85.6681], 10); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 18, + }).addTo(map); + + // Get API configuration + let config = { hasMapbox: false, mapboxAccessToken: null }; + try { + const configResponse = await fetch('/api/config'); + config = await configResponse.json(); + console.log('πŸ”§ API Configuration:', config.hasMapbox ? 'MapBox enabled' : 'Using Nominatim fallback'); + } catch (error) { + console.warn('Failed to load API configuration, using fallback'); + } + + const locationForm = document.getElementById('location-form'); + const messageDiv = document.getElementById('message'); + const submitBtn = document.getElementById('submit-btn'); + const submitText = document.getElementById('submit-text'); + const submitLoading = document.getElementById('submit-loading'); + const addressInput = document.getElementById('address'); + const autocompleteList = document.getElementById('autocomplete-list'); + + let autocompleteTimeout; + let selectedIndex = -1; + let autocompleteResults = []; + let currentMarkers = []; + let updateInterval; + + const clearMarkers = () => { + currentMarkers.forEach(marker => { + map.removeLayer(marker); + }); + currentMarkers = []; + }; + + const showMarkers = locations => { + clearMarkers(); + + locations.forEach(({ latitude, longitude, address, description, created_at }) => { + if (latitude && longitude) { + const timeAgo = getTimeAgo(created_at); + const marker = L.marker([latitude, longitude]) + .addTo(map) + .bindPopup(`${address}
${description || 'No additional details'}
Reported ${timeAgo}`); + currentMarkers.push(marker); + } + }); + }; + + const getTimeAgo = (timestamp) => { + const now = new Date(); + const reportTime = new Date(timestamp); + const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60)); + + if (diffInMinutes < 1) return 'just now'; + if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`; + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`; + + return 'over a day ago'; + }; + + const refreshLocations = () => { + fetch('/api/locations') + .then(res => res.json()) + .then(locations => { + showMarkers(locations); + const countElement = document.getElementById('location-count'); + countElement.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`; + + const now = new Date(); + const timeStr = now.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + countElement.title = `Last updated: ${timeStr}`; + }) + .catch(err => { + console.error('Error fetching locations:', err); + document.getElementById('location-count').textContent = 'Error loading locations'; + }); + }; + + refreshLocations(); + updateInterval = setInterval(refreshLocations, 30000); + + // MapBox Geocoding (Fast and Accurate!) + const mapboxGeocode = async (address) => { + const startTime = performance.now(); + console.log(`πŸš€ MapBox Geocoding: "${address}"`); + + try { + const encodedAddress = encodeURIComponent(address); + const response = await fetch( + `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodedAddress}.json?` + + `access_token=${config.mapboxAccessToken}&` + + `country=US&` + + `region=MI&` + + `proximity=-85.6681,42.9634&` + // Grand Rapids coordinates + `limit=1` + ); + + if (!response.ok) { + throw new Error(`MapBox API error: ${response.status}`); + } + + const data = await response.json(); + const time = (performance.now() - startTime).toFixed(2); + + if (data.features && data.features.length > 0) { + const result = data.features[0]; + console.log(`βœ… MapBox geocoding successful in ${time}ms`); + console.log(`πŸ“ Found: ${result.place_name}`); + + return { + lat: result.center[1], // MapBox returns [lng, lat] + lon: result.center[0], + display_name: result.place_name + }; + } else { + console.log(`❌ MapBox geocoding: no results (${time}ms)`); + throw new Error('No results found'); + } + } catch (error) { + const time = (performance.now() - startTime).toFixed(2); + console.warn(`🚫 MapBox geocoding failed (${time}ms):`, error); + throw error; + } + }; + + // Nominatim Fallback (Slower but free) + const nominatimGeocode = async (address) => { + const startTime = performance.now(); + console.log(`🐌 Nominatim fallback: "${address}"`); + + const searches = [ + address, + `${address}, Michigan`, + `${address}, MI`, + `${address}, Grand Rapids, MI` + ]; + + for (const query of searches) { + try { + const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1&countrycodes=us`); + const data = await response.json(); + + if (data && data.length > 0) { + const time = (performance.now() - startTime).toFixed(2); + console.log(`βœ… Nominatim successful in ${time}ms`); + return data[0]; + } + } catch (error) { + console.warn(`Nominatim query failed: ${query}`); + } + } + + const time = (performance.now() - startTime).toFixed(2); + console.error(`πŸ’₯ All geocoding failed after ${time}ms`); + throw new Error('Address not found'); + }; + + // Main geocoding function + const geocodeAddress = async (address) => { + if (config.hasMapbox) { + try { + return await mapboxGeocode(address); + } catch (error) { + console.warn('MapBox geocoding failed, trying Nominatim fallback'); + return await nominatimGeocode(address); + } + } else { + return await nominatimGeocode(address); + } + }; + + // MapBox Autocomplete (Lightning Fast!) + const mapboxAutocomplete = async (query) => { + const startTime = performance.now(); + console.log(`⚑ MapBox Autocomplete: "${query}"`); + + try { + const encodedQuery = encodeURIComponent(query); + const response = await fetch( + `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodedQuery}.json?` + + `access_token=${config.mapboxAccessToken}&` + + `country=US&` + + `region=MI&` + + `proximity=-85.6681,42.9634&` + // Grand Rapids coordinates + `types=address,poi&` + + `autocomplete=true&` + + `limit=5` + ); + + if (!response.ok) { + throw new Error(`MapBox API error: ${response.status}`); + } + + const data = await response.json(); + const time = (performance.now() - startTime).toFixed(2); + + if (data.features && data.features.length > 0) { + // Filter for Michigan results + const michiganResults = data.features.filter(feature => + feature.place_name.toLowerCase().includes('michigan') || + feature.place_name.toLowerCase().includes(', mi,') || + feature.context?.some(ctx => ctx.short_code === 'us-mi') + ); + + console.log(`⚑ MapBox autocomplete: ${michiganResults.length} results in ${time}ms`); + + return michiganResults.slice(0, 5).map(feature => ({ + display_name: feature.place_name, + mapbox_id: feature.id + })); + } else { + console.log(`❌ MapBox autocomplete: no results (${time}ms)`); + return []; + } + } catch (error) { + const time = (performance.now() - startTime).toFixed(2); + console.warn(`🚫 MapBox autocomplete failed (${time}ms):`, error); + return []; + } + }; + + // Nominatim Autocomplete Fallback + const nominatimAutocomplete = async (query) => { + console.log(`🐌 Nominatim autocomplete fallback: "${query}"`); + + try { + const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=5&countrycodes=us&addressdetails=1`); + const data = await response.json(); + + return data.filter(item => + item.display_name.toLowerCase().includes('michigan') || + item.display_name.toLowerCase().includes(', mi,') + ).slice(0, 5); + } catch (error) { + console.error('Nominatim autocomplete failed:', error); + return []; + } + }; + + // Main autocomplete function + const fetchAddressSuggestions = async (query) => { + if (query.length < 3) { + hideAutocomplete(); + return; + } + + let results = []; + + if (config.hasMapbox) { + try { + results = await mapboxAutocomplete(query); + } catch (error) { + console.warn('MapBox autocomplete failed, trying Nominatim'); + } + } + + if (results.length === 0) { + results = await nominatimAutocomplete(query); + } + + autocompleteResults = results; + showAutocomplete(autocompleteResults); + }; + + // Form submission + locationForm.addEventListener('submit', e => { + e.preventDefault(); + + const address = document.getElementById('address').value; + const description = document.getElementById('description').value; + + if (!address) return; + + submitBtn.disabled = true; + submitText.style.display = 'none'; + submitLoading.style.display = 'inline'; + + geocodeAddress(address) + .then(result => { + const { lat, lon } = result; + + return fetch('/api/locations', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + address, + latitude: parseFloat(lat), + longitude: parseFloat(lon), + description, + }), + }); + }) + .then(res => res.json()) + .then(location => { + refreshLocations(); + messageDiv.textContent = 'Location reported successfully!'; + messageDiv.className = 'message success'; + locationForm.reset(); + }) + .catch(err => { + console.error('Error reporting location:', err); + messageDiv.textContent = 'Error reporting location.'; + messageDiv.className = 'message error'; + }) + .finally(() => { + submitBtn.disabled = false; + submitText.style.display = 'inline'; + submitLoading.style.display = 'none'; + messageDiv.style.display = 'block'; + setTimeout(() => { + messageDiv.style.display = 'none'; + }, 3000); + }); + }); + + // Autocomplete UI functions + const showAutocomplete = (results) => { + if (results.length === 0) { + hideAutocomplete(); + return; + } + + autocompleteList.innerHTML = ''; + selectedIndex = -1; + + results.forEach((result, index) => { + const item = document.createElement('div'); + item.className = 'autocomplete-item'; + item.textContent = result.display_name; + item.addEventListener('click', () => selectAddress(result)); + autocompleteList.appendChild(item); + }); + + autocompleteList.style.display = 'block'; + }; + + const hideAutocomplete = () => { + autocompleteList.style.display = 'none'; + selectedIndex = -1; + }; + + const selectAddress = (result) => { + addressInput.value = result.display_name; + hideAutocomplete(); + addressInput.focus(); + }; + + const updateSelection = (direction) => { + const items = autocompleteList.querySelectorAll('.autocomplete-item'); + + if (items.length === 0) return; + + items[selectedIndex]?.classList.remove('selected'); + + if (direction === 'down') { + selectedIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0; + } else { + selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1; + } + + items[selectedIndex].classList.add('selected'); + items[selectedIndex].scrollIntoView({ block: 'nearest' }); + }; + + // Event listeners + addressInput.addEventListener('input', (e) => { + clearTimeout(autocompleteTimeout); + const query = e.target.value.trim(); + + if (query.length < 3) { + hideAutocomplete(); + return; + } + + // Ultra-fast debounce with MapBox + autocompleteTimeout = setTimeout(() => { + fetchAddressSuggestions(query); + }, config.hasMapbox ? 150 : 300); + }); + + addressInput.addEventListener('keydown', (e) => { + const isListVisible = autocompleteList.style.display === 'block'; + + switch (e.key) { + case 'ArrowDown': + if (isListVisible) { + e.preventDefault(); + updateSelection('down'); + } + break; + case 'ArrowUp': + if (isListVisible) { + e.preventDefault(); + updateSelection('up'); + } + break; + case 'Enter': + if (isListVisible && selectedIndex >= 0) { + e.preventDefault(); + selectAddress(autocompleteResults[selectedIndex]); + } + break; + case 'Escape': + hideAutocomplete(); + break; + } + }); + + document.addEventListener('click', (e) => { + if (!e.target.closest('.autocomplete-container')) { + hideAutocomplete(); + } + }); + + window.addEventListener('beforeunload', () => { + if (updateInterval) { + clearInterval(updateInterval); + } + }); +}); diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..8e4f13d --- /dev/null +++ b/public/app.js @@ -0,0 +1,408 @@ +document.addEventListener('DOMContentLoaded', () => { + const map = L.map('map').setView([42.9634, -85.6681], 10); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 18, + }).addTo(map); + + const locationForm = document.getElementById('location-form'); + const messageDiv = document.getElementById('message'); + const submitBtn = document.getElementById('submit-btn'); + const submitText = document.getElementById('submit-text'); + const submitLoading = document.getElementById('submit-loading'); + const addressInput = document.getElementById('address'); + const autocompleteList = document.getElementById('autocomplete-list'); + + let autocompleteTimeout; + let selectedIndex = -1; + let autocompleteResults = []; + let currentMarkers = []; + let updateInterval; + + const clearMarkers = () => { + currentMarkers.forEach(marker => { + map.removeLayer(marker); + }); + currentMarkers = []; + }; + + const showMarkers = locations => { + // Clear existing markers first + clearMarkers(); + + locations.forEach(({ latitude, longitude, address, description, created_at }) => { + if (latitude && longitude) { + const timeAgo = getTimeAgo(created_at); + const marker = L.marker([latitude, longitude]) + .addTo(map) + .bindPopup(`${address}
${description || 'No additional details'}
Reported ${timeAgo}`); + currentMarkers.push(marker); + } + }); + }; + + const getTimeAgo = (timestamp) => { + const now = new Date(); + const reportTime = new Date(timestamp); + const diffInMinutes = Math.floor((now - reportTime) / (1000 * 60)); + + if (diffInMinutes < 1) return 'just now'; + if (diffInMinutes < 60) return `${diffInMinutes} minute${diffInMinutes !== 1 ? 's' : ''} ago`; + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`; + + return 'over a day ago'; + }; + + const refreshLocations = () => { + fetch('/api/locations') + .then(res => res.json()) + .then(locations => { + showMarkers(locations); + const countElement = document.getElementById('location-count'); + countElement.textContent = `${locations.length} active report${locations.length !== 1 ? 's' : ''}`; + + // Add visual indicator of last update + const now = new Date(); + const timeStr = now.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + countElement.title = `Last updated: ${timeStr}`; + }) + .catch(err => { + console.error('Error fetching locations:', err); + document.getElementById('location-count').textContent = 'Error loading locations'; + }); + }; + + // Initial load + refreshLocations(); + + // Set up real-time updates every 30 seconds + updateInterval = setInterval(refreshLocations, 30000); + + locationForm.addEventListener('submit', e => { + e.preventDefault(); + + const address = document.getElementById('address').value; + const description = document.getElementById('description').value; + + if (!address) return; + + submitBtn.disabled = true; + submitText.style.display = 'none'; + submitLoading.style.display = 'inline'; + + // Try multiple geocoding strategies for intersections + const tryGeocode = async (query) => { + const cleanQuery = query.trim(); + const startTime = performance.now(); + + console.log(`πŸ” Starting geocoding for: "${cleanQuery}"`); + + // Generate multiple search variations + const searches = [ + cleanQuery, // Original query + cleanQuery.replace(' & ', ' and '), // Replace & with 'and' + cleanQuery.replace(' and ', ' & '), // Replace 'and' with & + cleanQuery.replace(' at ', ' & '), // Replace 'at' with & + `${cleanQuery}, Michigan`, // Add Michigan if not present + `${cleanQuery}, MI`, // Add MI if not present + ]; + + // Add street type variations + const streetVariations = []; + searches.forEach(search => { + streetVariations.push(search); + streetVariations.push(search.replace(' St ', ' Street ')); + streetVariations.push(search.replace(' Street ', ' St ')); + streetVariations.push(search.replace(' Ave ', ' Avenue ')); + streetVariations.push(search.replace(' Avenue ', ' Ave ')); + streetVariations.push(search.replace(' Rd ', ' Road ')); + streetVariations.push(search.replace(' Road ', ' Rd ')); + streetVariations.push(search.replace(' Blvd ', ' Boulevard ')); + streetVariations.push(search.replace(' Boulevard ', ' Blvd ')); + }); + + // Remove duplicates + const uniqueSearches = [...new Set(streetVariations)]; + console.log(`πŸ“‹ Generated ${uniqueSearches.length} search variations`); + + let attemptCount = 0; + for (const searchQuery of uniqueSearches) { + attemptCount++; + const queryStartTime = performance.now(); + + try { + console.log(`🌐 Attempt ${attemptCount}: "${searchQuery}"`); + const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&extratags=1&q=${encodeURIComponent(searchQuery)}`); + const data = await response.json(); + const queryTime = (performance.now() - queryStartTime).toFixed(2); + + if (data && data.length > 0) { + // Prefer results that are in Michigan/Grand Rapids + const michiganResults = data.filter(item => + item.display_name.toLowerCase().includes('michigan') || + item.display_name.toLowerCase().includes('grand rapids') || + item.display_name.toLowerCase().includes(', mi') + ); + + const result = michiganResults.length > 0 ? michiganResults[0] : data[0]; + const totalTime = (performance.now() - startTime).toFixed(2); + + console.log(`βœ… Geocoding successful in ${totalTime}ms (query: ${queryTime}ms)`); + console.log(`πŸ“ Found: ${result.display_name}`); + console.log(`🎯 Coordinates: ${result.lat}, ${result.lon}`); + + return result; + } else { + console.log(`❌ No results found (${queryTime}ms)`); + } + } catch (error) { + const queryTime = (performance.now() - queryStartTime).toFixed(2); + console.warn(`🚫 Geocoding failed for: "${searchQuery}" (${queryTime}ms)`, error); + } + } + + // If intersection search fails, try individual streets for approximate location + if (cleanQuery.includes('&') || cleanQuery.includes(' and ')) { + console.log(`πŸ”„ Trying fallback strategy for intersection`); + const streets = cleanQuery.split(/\s+(?:&|and)\s+/i); + if (streets.length >= 2) { + const firstStreet = streets[0].trim() + ', Grand Rapids, MI'; + const fallbackStartTime = performance.now(); + + try { + console.log(`🌐 Fallback: "${firstStreet}"`); + const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(firstStreet)}`); + const data = await response.json(); + const fallbackTime = (performance.now() - fallbackStartTime).toFixed(2); + + if (data && data.length > 0) { + const totalTime = (performance.now() - startTime).toFixed(2); + console.log(`⚠️ Using approximate location from first street (${totalTime}ms total)`); + console.log(`πŸ“ Approximate: ${data[0].display_name}`); + return data[0]; + } else { + console.log(`❌ Fallback failed - no results (${fallbackTime}ms)`); + } + } catch (error) { + const fallbackTime = (performance.now() - fallbackStartTime).toFixed(2); + console.warn(`🚫 Fallback search failed: "${firstStreet}" (${fallbackTime}ms)`, error); + } + } + } + + const totalTime = (performance.now() - startTime).toFixed(2); + console.error(`πŸ’₯ All geocoding strategies failed after ${totalTime}ms`); + throw new Error('Address not found with any search strategy'); + }; + + tryGeocode(address) + .then(result => { + const { lat, lon } = result; + + return fetch('/api/locations', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + address, + latitude: parseFloat(lat), + longitude: parseFloat(lon), + description, + }), + }); + }) + .then(res => res.json()) + .then(location => { + // Immediately refresh all locations to show the new one + refreshLocations(); + messageDiv.textContent = 'Location reported successfully!'; + messageDiv.className = 'message success'; + locationForm.reset(); + }) + .catch(err => { + console.error('Error reporting location:', err); + messageDiv.textContent = 'Error reporting location.'; + messageDiv.className = 'message error'; + }) + .finally(() => { + submitBtn.disabled = false; + submitText.style.display = 'inline'; + submitLoading.style.display = 'none'; + messageDiv.style.display = 'block'; + setTimeout(() => { + messageDiv.style.display = 'none'; + }, 3000); + }); + }); + + // Autocomplete functionality + const fetchAddressSuggestions = async (query) => { + if (query.length < 3) { + hideAutocomplete(); + return; + } + + try { + // Create search variations for better intersection handling + const searchQueries = [ + query, + query.replace(' & ', ' and '), + query.replace(' and ', ' & '), + `${query}, Grand Rapids, MI`, + `${query}, Michigan` + ]; + + let allResults = []; + + // Try each search variation + for (const searchQuery of searchQueries) { + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery)}&limit=3&countrycodes=us&addressdetails=1` + ); + const data = await response.json(); + + if (data && data.length > 0) { + allResults = allResults.concat(data); + } + } catch (error) { + console.warn(`Autocomplete search failed for: ${searchQuery}`); + } + } + + // Filter and deduplicate results + const michiganResults = allResults.filter(item => { + return item.display_name.toLowerCase().includes('michigan') || + item.display_name.toLowerCase().includes(', mi,') || + item.display_name.toLowerCase().includes('grand rapids'); + }); + + // Remove duplicates based on display_name + const uniqueResults = michiganResults.filter((item, index, arr) => + arr.findIndex(other => other.display_name === item.display_name) === index + ); + + autocompleteResults = uniqueResults.slice(0, 5); + showAutocomplete(autocompleteResults); + } catch (error) { + console.error('Error fetching address suggestions:', error); + hideAutocomplete(); + } + }; + + const showAutocomplete = (results) => { + if (results.length === 0) { + hideAutocomplete(); + return; + } + + autocompleteList.innerHTML = ''; + selectedIndex = -1; + + results.forEach((result, index) => { + const item = document.createElement('div'); + item.className = 'autocomplete-item'; + item.textContent = result.display_name; + item.addEventListener('click', () => selectAddress(result)); + autocompleteList.appendChild(item); + }); + + autocompleteList.style.display = 'block'; + }; + + const hideAutocomplete = () => { + autocompleteList.style.display = 'none'; + selectedIndex = -1; + }; + + const selectAddress = (result) => { + addressInput.value = result.display_name; + hideAutocomplete(); + addressInput.focus(); + }; + + const updateSelection = (direction) => { + const items = autocompleteList.querySelectorAll('.autocomplete-item'); + + if (items.length === 0) return; + + // Remove previous selection + items[selectedIndex]?.classList.remove('selected'); + + // Update index + if (direction === 'down') { + selectedIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0; + } else { + selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1; + } + + // Add new selection + items[selectedIndex].classList.add('selected'); + items[selectedIndex].scrollIntoView({ block: 'nearest' }); + }; + + // Event listeners for autocomplete + addressInput.addEventListener('input', (e) => { + clearTimeout(autocompleteTimeout); + const query = e.target.value.trim(); + + if (query.length < 3) { + hideAutocomplete(); + return; + } + + // Debounce the API calls + autocompleteTimeout = setTimeout(() => { + fetchAddressSuggestions(query); + }, 300); + }); + + addressInput.addEventListener('keydown', (e) => { + const isListVisible = autocompleteList.style.display === 'block'; + + switch (e.key) { + case 'ArrowDown': + if (isListVisible) { + e.preventDefault(); + updateSelection('down'); + } + break; + case 'ArrowUp': + if (isListVisible) { + e.preventDefault(); + updateSelection('up'); + } + break; + case 'Enter': + if (isListVisible && selectedIndex >= 0) { + e.preventDefault(); + selectAddress(autocompleteResults[selectedIndex]); + } + break; + case 'Escape': + hideAutocomplete(); + break; + } + }); + + // Hide autocomplete when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('.autocomplete-container')) { + hideAutocomplete(); + } + }); + + // Cleanup interval when page is unloaded + window.addEventListener('beforeunload', () => { + if (updateInterval) { + clearInterval(updateInterval); + } + }); +}); + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..bd6f6ae --- /dev/null +++ b/public/index.html @@ -0,0 +1,70 @@ + + + + + + ICE Watch Michigan + + + + +
+
+

🚨 ICE Watch Michigan

+

Community-reported ICE activity locations (auto-expire after 24 hours)

+
+ +
+
+

Report ICE Activity

+
+
+ +
+ +
+
+ Examples: "123 Main St, City" or "Main St & Oak Ave, City" or "CVS Pharmacy, City" +
+ +
+ + +
+ + +
+ +
+
+ +
+

Current Reports

+
+
+

πŸ”΄ Red markers: ICE activity reported

+

⏰ Auto-cleanup: Reports disappear after 24 hours

+

Loading locations...

+
+
+
+ +
+

Safety Notice: This is a community tool for awareness. Stay safe and know your rights.

+
+ This website is for informational purposes only. Verify information independently. + Reports are automatically deleted after 24 hours. +
+
+
+ + + + + diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..abe4d15 --- /dev/null +++ b/public/style.css @@ -0,0 +1,137 @@ +body { + font-family: Arial, sans-serif; + line-height: 1.6; + margin: 0; + padding: 0; + background-color: #f4f4f9; +} + +.container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + padding-bottom: 20px; +} + +.map-section, .form-section { + margin-bottom: 20px; + padding: 15px; + background-color: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.map-section { + height: auto; + min-height: 650px; + padding-bottom: 20px; +} + +#map { + width: 100%; + height: 600px; + border-radius: 8px; + margin-bottom: 15px; +} + +.form-group { + margin-bottom: 10px; +} + +input[type="text"], textarea { + width: calc(100% - 20px); + padding: 8px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.autocomplete-container { + position: relative; + width: 100%; +} + +.autocomplete-list { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 1px solid #ddd; + border-top: none; + border-radius: 0 0 4px 4px; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + display: none; +} + +.autocomplete-item { + padding: 10px; + cursor: pointer; + border-bottom: 1px solid #eee; +} + +.autocomplete-item:hover, +.autocomplete-item.selected { + background-color: #f8f9fa; +} + +.autocomplete-item:last-child { + border-bottom: none; +} + +.input-help { + color: #666; + font-size: 0.85em; + margin-top: 4px; + display: block; +} + +button[type="submit"] { + background-color: #007bff; + color: white; + border: none; + padding: 10px 20px; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.2s ease-in; +} + +button[type="submit"]:hover { + background-color: #0056b3; +} + +.message { + margin-top: 10px; + padding: 10px; + display: none; + border-radius: 4px; +} + +.success { + background-color: #d4edda; + color: #155724; +} + +.error { + background-color: #f8d7da; + color: #721c24; +} + +footer { + text-align: center; + padding: 30px 20px; + margin-top: 30px; + border-top: 1px solid #ddd; + clear: both; +} + +disclaimer { + font-size: 0.8em; + color: #777; +} diff --git a/scripts/Caddyfile b/scripts/Caddyfile new file mode 100644 index 0000000..cd9f5b7 --- /dev/null +++ b/scripts/Caddyfile @@ -0,0 +1,38 @@ +# ICE Watch Caddy Configuration +# Replace yourdomain.com with your actual domain + +yourdomain.com { + # Reverse proxy to Node.js app + reverse_proxy localhost:3000 + + # Security headers + header { + # Enable HSTS + Strict-Transport-Security max-age=31536000; + # Prevent clickjacking + X-Frame-Options DENY + # Prevent content type sniffing + X-Content-Type-Options nosniff + # XSS protection + X-XSS-Protection "1; mode=block" + # Referrer policy + Referrer-Policy strict-origin-when-cross-origin + } + + # Gzip compression + encode gzip + + # Rate limiting (optional) + # rate_limit { + # zone static_ip_10rs { + # key {remote_host} + # events 10 + # window 1s + # } + # } +} + +# Optional: Redirect www to non-www +www.yourdomain.com { + redir https://yourdomain.com{uri} permanent +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..94d4e53 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# ICE Watch Deployment Script for Debian 12 ARM64 +# Run this script on your server: drone@91.99.139.235 + +set -e + +echo "πŸš€ Starting ICE Watch deployment..." + +# Update system +echo "πŸ“¦ Updating system packages..." +sudo apt update && sudo apt upgrade -y + +# Install Node.js (ARM64 compatible) +echo "πŸ“¦ Installing Node.js..." +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt install -y nodejs build-essential + +# Install Caddy for reverse proxy +echo "πŸ“¦ Installing Caddy..." +sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list +sudo apt update +sudo apt install caddy + +# Create app directory +echo "πŸ“ Setting up app directory..." +sudo mkdir -p /opt/icewatch +sudo chown $USER:$USER /opt/icewatch + +# Navigate to app directory +cd /opt/icewatch + +echo "βœ… Server setup complete!" +echo "Next steps:" +echo "1. Upload your app files to /opt/icewatch" +echo "2. Run: npm install" +echo "3. Configure your .env file" +echo "4. Set up systemd service" +echo "5. Configure Caddy" diff --git a/scripts/icewatch.service b/scripts/icewatch.service new file mode 100644 index 0000000..a618528 --- /dev/null +++ b/scripts/icewatch.service @@ -0,0 +1,24 @@ +[Unit] +Description=ICE Watch Michigan - Community Safety Tool +After=network.target +Wants=network.target + +[Service] +Type=simple +User=icewatch +Group=icewatch +WorkingDirectory=/opt/icewatch +ExecStart=/usr/bin/node server.js +Restart=always +RestartSec=5 +Environment=NODE_ENV=production + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/icewatch + +[Install] +WantedBy=multi-user.target diff --git a/server.js b/server.js new file mode 100644 index 0000000..716afb6 --- /dev/null +++ b/server.js @@ -0,0 +1,293 @@ +require('dotenv').config({ path: '.env.local' }); +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +const cron = require('node-cron'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(express.json()); +app.use(express.static('public')); + +// Database setup +const db = new sqlite3.Database('icewatch.db'); + +console.log('Database connection established'); + +// Initialize database +db.serialize(() => { + db.run(`CREATE TABLE IF NOT EXISTS locations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + address TEXT NOT NULL, + latitude REAL, + longitude REAL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, (err) => { + if (err) { + console.error('Error initializing database:', err); + } else { + console.log('Database initialized successfully'); + } + }); +}); + +// Clean up expired locations (older than 24 hours) +const cleanupExpiredLocations = () => { + console.log('Running cleanup of expired locations'); + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + db.run('DELETE FROM locations WHERE created_at < ?', [twentyFourHoursAgo], function(err) { + if (err) { + console.error('Error cleaning up expired locations:', err); + } else { + console.log(`Cleaned up ${this.changes} expired locations`); + } + }); +}; + +// Run cleanup every hour +console.log('Scheduling hourly cleanup task'); +cron.schedule('0 * * * *', cleanupExpiredLocations); + +// Configuration +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123'; // Change this! +const MAPBOX_ACCESS_TOKEN = process.env.MAPBOX_ACCESS_TOKEN || null; // Set this for better performance +const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || null; // Fallback option + +const authenticateAdmin = (req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const token = authHeader.substring(7); + if (token !== ADMIN_PASSWORD) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + next(); +}; + +// API Routes + +// Get API configuration +app.get('/api/config', (req, res) => { + console.log('πŸ“‘ API Config requested'); + console.log('MapBox token present:', !!MAPBOX_ACCESS_TOKEN); + console.log('MapBox token starts with pk:', MAPBOX_ACCESS_TOKEN?.startsWith('pk.')); + + res.json({ + mapboxAccessToken: MAPBOX_ACCESS_TOKEN, + hasMapbox: !!MAPBOX_ACCESS_TOKEN, + googleMapsApiKey: GOOGLE_MAPS_API_KEY, + hasGoogleMaps: !!GOOGLE_MAPS_API_KEY + }); +}); + +// Admin login +app.post('/api/admin/login', (req, res) => { + console.log('Admin login attempt'); + const { password } = req.body; + + if (password === ADMIN_PASSWORD) { + console.log('Admin login successful'); + res.json({ token: ADMIN_PASSWORD, message: 'Login successful' }); + } else { + console.warn('Admin login failed: invalid password'); + res.status(401).json({ error: 'Invalid password' }); + } +}); + +// Get all active locations (within 24 hours) +app.get('/api/locations', (req, res) => { + console.log('Fetching active locations'); + const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + + db.all( + 'SELECT * FROM locations WHERE created_at > ? ORDER BY created_at DESC', + [twentyFourHoursAgo], + (err, rows) => { + if (err) { + console.error('Error fetching locations:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + console.log(`Fetched ${rows.length} active locations`); + res.json(rows); + } + ); +}); + +// Add a new location +app.post('/api/locations', (req, res) => { + const { address, latitude, longitude, description } = req.body; + console.log(`Attempt to add new location: ${address}`); + + if (!address) { + console.warn('Failed to add location: Address is required'); + res.status(400).json({ error: 'Address is required' }); + return; + } + + db.run( + 'INSERT INTO locations (address, latitude, longitude, description) VALUES (?, ?, ?, ?)', + [address, latitude, longitude, description], + function(err) { + if (err) { + console.error('Error inserting location:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + + console.log(`Location added successfully: ${address}`); + res.json({ + id: this.lastID, + address, + latitude, + longitude, + description, + created_at: new Date().toISOString() + }); + } + ); +}); + +// Admin Routes + +// Get all locations for admin (including expired ones) +app.get('/api/admin/locations', authenticateAdmin, (req, res) => { + db.all( + 'SELECT id, address, description, latitude, longitude, created_at FROM locations ORDER BY created_at DESC', + [], + (err, rows) => { + if (err) { + console.error('Error fetching all locations:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + + // Process and clean data before sending + const locations = rows.map(row => ({ + id: row.id, + address: row.address, + description: row.description || '', + latitude: row.latitude, + longitude: row.longitude, + created_at: row.created_at, + isActive: new Date(row.created_at) > new Date(Date.now() - 24 * 60 * 60 * 1000) + })); + + res.json(locations); + } + ); +}); + +// Update a location (admin only) +app.put('/api/admin/locations/:id', authenticateAdmin, (req, res) => { + const { id } = req.params; + const { address, latitude, longitude, description } = req.body; + + if (!address) { + res.status(400).json({ error: 'Address is required' }); + return; + } + + db.run( + 'UPDATE locations SET address = ?, latitude = ?, longitude = ?, description = ? WHERE id = ?', + [address, latitude, longitude, description, id], + function(err) { + if (err) { + console.error('Error updating location:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + + if (this.changes === 0) { + res.status(404).json({ error: 'Location not found' }); + return; + } + + res.json({ message: 'Location updated successfully' }); + } + ); +}); + +// Delete a location (admin authentication required) +app.delete('/api/admin/locations/:id', authenticateAdmin, (req, res) => { + const { id } = req.params; + + db.run('DELETE FROM locations WHERE id = ?', [id], function(err) { + if (err) { + console.error('Error deleting location:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + + if (this.changes === 0) { + res.status(404).json({ error: 'Location not found' }); + return; + } + + res.json({ message: 'Location deleted successfully' }); + }); +}); + +// Legacy delete route (keeping for backwards compatibility) +app.delete('/api/locations/:id', (req, res) => { + const { id } = req.params; + + db.run('DELETE FROM locations WHERE id = ?', [id], function(err) { + if (err) { + console.error('Error deleting location:', err); + res.status(500).json({ error: 'Internal server error' }); + return; + } + + if (this.changes === 0) { + res.status(404).json({ error: 'Location not found' }); + return; + } + + res.json({ message: 'Location deleted successfully' }); + }); +}); + +// Serve the main page +app.get('/', (req, res) => { + console.log('Serving the main page'); + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// Serve the admin page +app.get('/admin', (req, res) => { + console.log('Serving the admin page'); + res.sendFile(path.join(__dirname, 'public', 'admin.html')); +}); + +// Start server +app.listen(PORT, () => { + console.log('======================================='); + console.log('ICE Watch server successfully started'); + console.log(`Listening on port ${PORT}`); + console.log(`Visit http://localhost:${PORT} to view the website`); + console.log('======================================='); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + console.log('\nShutting down server...'); + db.close((err) => { + if (err) { + console.error('Error closing database:', err); + } else { + console.log('Database connection closed.'); + } + process.exit(0); + }); +});