diff --git a/.env b/.env deleted file mode 100644 index fa574f5..0000000 --- a/.env +++ /dev/null @@ -1,181 +0,0 @@ -################################################################# -## DISCORD BOT SETTINGS ## -################################################################# - -# --- Required --- -DISCORD_TOKEN=your-discord-bot-token-here -CLIENT_ID=your-discord-client-id - -# --- Audio Storage Mode --- -# Options: local (stores in /audio) | s3 (Amazon S3 bucket) -STORAGE_MODE=local - -# --- S3 Settings (only used if STORAGE_MODE=s3) --- -S3_ENDPOINT=https://s3.example.com/ -S3_BUCKET_NAME=scanner-map-bucket -S3_ACCESS_KEY_ID=your-s3-key-id -S3_SECRET_ACCESS_KEY=your-s3-secret-key - - -################################################################# -## SERVER & NETWORK SETTINGS ## -################################################################# - -# Port for SDRTrunk/TrunkRecorder uploads -BOT_PORT=3306 - -# Port for web interface/API server -WEBSERVER_PORT=8080 - -# Public domain or IP for generating playback/share links -PUBLIC_DOMAIN=scannermap.net - -# Timezone for logs & timestamps (use IANA format, e.g. "US/Eastern" or "America/New_York") -TIMEZONE=US/Eastern - - -################################################################# -## AUTHENTICATION & API KEY SETTINGS ## -################################################################# - -# API keys for inbound SDRTrunk uploads -API_KEY_FILE=data/apikeys.json - -# Enable password protection on the web interface -ENABLE_AUTH=false -WEBSERVER_PASSWORD=changeme - - -################################################################# -## GEOCODING & LOCATION SETTINGS ## -################################################################# - -# --- Geocoding Providers (REQUIRED: Set at least one) --- -# These APIs are used for address autocomplete in the web interface and geocoding validation -# You must provide at least one API key for the system to work properly - -# Google Maps API Key -# - Get your key: https://console.cloud.google.com/apis/credentials -# - Enable: Maps JavaScript API, Places API, Geocoding API -GOOGLE_MAPS_API_KEY= - -# LocationIQ API Key -# - Get your key: https://locationiq.com/register -LOCATIONIQ_API_KEY= - -# Default hints to help geocoder resolve incomplete addresses -GEOCODING_CITY="Silver Spring" -GEOCODING_STATE=MD -GEOCODING_COUNTRY=US - -# Restrict matches to specific counties / cities -GEOCODING_TARGET_COUNTIES="Montgomery County" -TARGET_CITIES_LIST=Ashton-Sandy Spring,Aspen Hill,Bethesda,...etc - - -################################################################# -## TRANSCRIPTION SETTINGS ## -################################################################# - -# Provider: local | remote | openai | icad -TRANSCRIPTION_MODE=local - -# --- Local (if TRANSCRIPTION_MODE=local) --- -TRANSCRIPTION_DEVICE=cuda # cuda | cpu - -# --- Remote Faster-Whisper (if TRANSCRIPTION_MODE=remote) --- -FASTER_WHISPER_SERVER_URL=http://127.0.0.1:9912 -WHISPER_MODEL=large-v3-turbo - -# --- ICAD (if TRANSCRIPTION_MODE=icad) --- -ICAD_URL=http://127.0.0.1:9912 -ICAD_API_KEY=your-icad-api-key -ICAD_PROFILE=large|test - -# --- OpenAI Transcription (if TRANSCRIPTION_MODE=openai) --- -OPENAI_API_KEY=your-openai-api-key -OPENAI_TRANSCRIPTION_MODEL=whisper-1 - -# The sampling temperature, between 0 and 1. -# Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. -# If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. -OPENAI_TRANSCRIPTION_TEMPERATURE=0 - -# Custom prompt to improve scanner audio transcription quality -OPENAI_TRANSCRIPTION_PROMPT="Scanner audio: police, fire, EMS radio communications. Transcribe addresses, unit numbers, and emergency details accurately leave black or grabbled audio blank." - -################################################################# -## AI ADDRESS EXTRACTION & SUMMARIES ## -################################################################# - -# AI Provider: ollama | openai -AI_PROVIDER=openai - -# --- Ollama (local LLM) --- -OLLAMA_URL=http://localhost:11434 -OLLAMA_MODEL=llama3.1:8b - -# --- OpenAI (cloud LLM) --- -# Uses same OPENAI_API_KEY above -OPENAI_MODEL=gpt-4o-mini - -# --- Summaries --- -SUMMARY_LOOKBACK_HOURS=1 -ASK_AI_LOOKBACK_HOURS=8 - - -################################################################# -## TALK GROUP MAPPINGS ## -################################################################# - -ENABLE_MAPPED_TALK_GROUPS=true -MAPPED_TALK_GROUPS=4005,4000,6000,6005,6010 - -# Examples -TALK_GROUP_6010="Silver Spring / Montgomery County MD" -TALK_GROUP_4005="Silver Spring / Montgomery County MD" -TALK_GROUP_6000="Any town in Montgomery County MD" - - -################################################################# -## TWO-TONE DETECTION SETTINGS ## -################################################################# - -ENABLE_TWO_TONE_MODE=false -TWO_TONE_TALK_GROUPS=4005,4000 -TWO_TONE_QUEUE_SIZE=1 - -TONE_DETECTION_TYPE=auto - -# --- Two-tone params --- -TWO_TONE_MIN_TONE_LENGTH=0.7 -TWO_TONE_MAX_TONE_LENGTH=3.0 -TWO_TONE_BW_HZ=50 -TWO_TONE_MIN_PAIR_SEPARATION_HZ=100 - -# --- Pulsed tone params --- -PULSED_MIN_CYCLES=3 -PULSED_MIN_ON_MS=50 -PULSED_MAX_ON_MS=500 -PULSED_MIN_OFF_MS=25 -PULSED_MAX_OFF_MS=800 -PULSED_BANDWIDTH_HZ=50 - -# --- Long tone params --- -LONG_TONE_MIN_LENGTH=0.5 -LONG_TONE_BANDWIDTH_HZ=75 - -# --- General detection --- -TONE_DETECTION_THRESHOLD=0.3 -TONE_FREQUENCY_BAND=300,1500 -TONE_TIME_RESOLUTION_MS=15 - - -################################################################# -## MODE COMBINATIONS (INFO) ## -################################################################# -# 1. MAPPED ONLY: ENABLE_MAPPED_TALK_GROUPS=true, ENABLE_TWO_TONE_MODE=false -# 2. TWO-TONE ONLY: ENABLE_MAPPED_TALK_GROUPS=false, ENABLE_TWO_TONE_MODE=true -# 3. HYBRID: Both true → mapped + tone-based extra -# 4. DISABLED: Both false → transcription only -################################################################# diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6198102 --- /dev/null +++ b/.env.example @@ -0,0 +1,69 @@ +DISCORD_TOKEN= +BOT_PORT=3306 +WEBSERVER_PORT=3001 +PUBLIC_DOMAIN=localhost +TIMEZONE=US/Eastern + +API_KEY_FILE=data/apikeys.json +ENABLE_AUTH=false +SESSION_DURATION_DAYS=7 +MAX_SESSIONS_PER_USER=5 + +GOOGLE_MAPS_API_KEY= +LOCATIONIQ_API_KEY= + +STORAGE_MODE=local +S3_ENDPOINT= +S3_BUCKET_NAME= +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= + +AI_PROVIDER=ollama +OPENAI_API_KEY= +OPENAI_MODEL=gpt-4o-mini +OLLAMA_URL=http://localhost:11434 +OLLAMA_MODEL=llama3.1:8b + +TRANSCRIPTION_MODE=local +FASTER_WHISPER_SERVER_URL=http://localhost:8000 +WHISPER_MODEL=large-v3 +TRANSCRIPTION_DEVICE=cpu +PYTHON_COMMAND=python +AUTO_UPDATE_PYTHON_PACKAGES=true + +ICAD_URL= +ICAD_PROFILE= +ICAD_API_KEY= + +OPENAI_TRANSCRIPTION_PROMPT= +OPENAI_TRANSCRIPTION_MODEL= +OPENAI_TRANSCRIPTION_TEMPERATURE= + +MAPPED_TALK_GROUPS= +ENABLE_MAPPED_TALK_GROUPS=true +SUMMARY_LOOKBACK_HOURS=1 +ASK_AI_LOOKBACK_HOURS=8 +MAX_CONCURRENT_TRANSCRIPTIONS=3 + +# TrunkRecorder radio config sync (optional) +TRUNK_RECORDER_CONFIG_PATH= +AUTO_SYNC_TRUNK_RECORDER=false +AUTO_LEARN_TALKGROUPS=true + +ENABLE_TWO_TONE_MODE=false +TWO_TONE_TALK_GROUPS= +TWO_TONE_QUEUE_SIZE=1 +TONE_DETECTION_TYPE= +TWO_TONE_MIN_TONE_LENGTH= +TWO_TONE_MAX_TONE_LENGTH= +PULSED_MIN_CYCLES= +PULSED_MIN_ON_MS= +PULSED_MAX_ON_MS= +PULSED_MIN_OFF_MS= +PULSED_MAX_OFF_MS= +PULSED_BANDWIDTH_HZ= +LONG_TONE_MIN_LENGTH= +LONG_TONE_BANDWIDTH_HZ= +TONE_DETECTION_THRESHOLD= +TONE_FREQUENCY_BAND= +TONE_TIME_RESOLUTION_MS= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..38cfa87 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - dev + - beta + +jobs: + smoke: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Syntax check + run: npm run check:syntax + + - name: Unit tests + run: npm test diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..88b01e7 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,67 @@ +name: Docker Publish + +on: + push: + branches: + - beta + tags: + - 'v*' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + matrix: + include: + - variant: core + dockerfile: docker/Dockerfile + tag_suffix: core + - variant: whisper + dockerfile: docker/Dockerfile.whisper + tag_suffix: whisper + - variant: qwen + dockerfile: docker/Dockerfile.qwen + tag_suffix: qwen + - variant: tone + dockerfile: docker/Dockerfile.tone + tag_suffix: tone + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}},suffix=-${{ matrix.tag_suffix }} + type=semver,pattern={{major}}.{{minor}},suffix=-${{ matrix.tag_suffix }} + type=raw,value=${{ matrix.tag_suffix }},enable={{is_default_branch}} + type=raw,value=beta,enable=${{ github.ref_name == 'beta' && matrix.tag_suffix == 'core' }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + push: ${{ github.event_name != 'workflow_dispatch' || github.ref_type == 'tag' || github.ref_name == 'beta' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd6c180 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.env +.venv/ +node_modules/ +audio/ +data/ +logs/ +combined.log +error.log +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 +*.pyc +__pycache__/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f22a4e5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing to Scanner Map + +Scanner Map uses a **dual-remote workflow**: daily development on the [Dadud fork](https://github.com/Dadud/Scanner-map) (`origin`), and periodic promotion to upstream [poisonednumber/Scanner-map](https://github.com/poisonednumber/Scanner-map) (`upstream`) via the `beta` branch only. + +## Repository roles + +| Remote | Repository | Who pushes | Purpose | +|--------|------------|------------|---------| +| `origin` | Dadud/Scanner-map | Contributors | Daily dev, feature branches, integration | +| `upstream` | poisonednumber/Scanner-map | `beta` only (when ready) | Public pre-release / integration testing | +| `upstream/main` | poisonednumber | **No direct pushes** | Stable release; merge from `beta` via PR | + +## Branch model + +| Branch | Where | Purpose | +|--------|-------|---------| +| `cursor/-bfe4` or `feature/` | `origin` | Short-lived feature work | +| `dev` | `origin` | **Default integration branch** — always runnable, CI green | +| `main` | `origin` | Fork stable snapshot | +| `beta` | `upstream` | Public integration; Docker `:beta` tag | +| `main` | `upstream` | Protected stable release | + +**Do not** open routine PRs to `upstream/main` for WIP features. Fold work into `origin/dev`, then promote to `upstream/beta` when ready. + +## One-time clone setup + +```bash +git clone https://github.com/Dadud/Scanner-map.git +cd Scanner-map +git remote add upstream https://github.com/poisonednumber/Scanner-map.git +git fetch origin upstream +``` + +Treat `upstream` as read-only except for the `beta` promotion described below. + +## Daily development + +```bash +git fetch origin upstream +git checkout dev +git pull origin dev +git checkout -b feature/my-change # or cursor/my-change-bfe4 for agents +``` + +Verify locally: + +```bash +npm install +npm test +npm run check:syntax +npm run check:config # when .env is present +``` + +Open a PR on **Dadud (`origin`)** with base branch **`dev`**. + +Cloud agents: use branch prefix `cursor/-bfe4`, PR to `origin/dev` only — never push feature branches to upstream. + +## Promoting to upstream `beta` + +When integration on `origin/dev` is ready for wider testing: + +### Preconditions + +- [ ] `npm test` and `npm run check:syntax` pass on `dev` +- [ ] README / docker docs match what beta ships +- [ ] `/settings` loads and populates (including with `ENABLE_AUTH=true`) +- [ ] Talkgroup import path documented (CSV + optional TR bundle) +- [ ] Short changelog snippet for beta release notes + +### Commands + +```bash +git fetch upstream origin +git checkout dev +git pull origin dev +git push upstream dev:beta +# If beta has diverged intentionally: +# git push upstream dev:beta --force-with-lease +``` + +**Never** run `git push upstream main`. + +## Releasing upstream `main` (stable) + +After `beta` is validated: + +1. Open PR **`beta` → `main`** on poisonednumber +2. Tag release `vX.Y.Z` +3. GHCR publishes `:core`, `:whisper`, `:qwen`, `:latest` from the tag +4. Sync fork: `git fetch upstream && git merge upstream/main` into `origin/main` + +## Syncing from upstream + +After upstream changes land: + +```bash +git fetch upstream +git checkout dev +git merge upstream/beta # day-to-day integration +# or merge upstream/main # after a release +git push origin dev +``` + +## URLs during development + +| URL | Purpose | +|-----|---------| +| `http://localhost:3000/` | Map UI | +| `http://localhost:3000/settings#radio` | Server settings — talkgroup import | +| `http://localhost:3000/setup` | First-run setup wizard | + +Map **Map Preferences** (in-app modal) is separate from **Server Settings** (`/settings`). + +## Branch protection (upstream) + +See [docs/github/BRANCH_PROTECTION.md](docs/github/BRANCH_PROTECTION.md) for the recommended GitHub settings on poisonednumber/Scanner-map. + +## Questions + +| Question | Answer | +|----------|--------| +| Where do I code? | Dadud fork, branches off `dev` | +| What do I push daily? | `origin` only | +| What goes to upstream? | `beta` branch only, when you choose | +| What about upstream `main`? | Protected; PR from `beta` at release time | diff --git a/README.md b/README.md index 63fd3c5..8ed48cc 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,111 @@ -# Scanner Map [![Discord](https://img.shields.io/badge/Discord-Join%20Now-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/X7vej75zZy) - - -A **real-time mapping system** for radio calls. -Ingests calls from SDRTrunk, TrunkRecorder, or any **rdio-scanner compatible endpoint**, then: - -- Transcribes audio (local or cloud AI) -- Extracts and geocodes locations -- Displays calls on an interactive map with **playback** and **Discord integration** - -434934279-4f51548f-e33f-4807-a11d-d91f3a6b4db1(1) - ---- - -## 🔥 Recent Updates - -- **Admin-restricted marker editing** — Map marker editing now locked behind admin user when authentication is enabled -- **Purge calls from map** — New admin-only feature to remove calls by talkgroup category and time range, includes undo button to restore accidentally purged calls -- Full **one-command integration** (no multiple terminals) -- Auto-generated API keys & admin users -- Improved **AI summaries & Ask AI** features -- New **S3 audio storage option** -- **OpenAI transcription prompting** — configure custom prompts in `.env` to fine‑tune transcription behavior -- **Two-tone detection** — powered by [icad-tone-detection](https://github.com/TheGreatCodeholio/icad-tone-detection). - - Detects fire/EMS tones in radio calls - - Optionally restrict address extraction to toned calls only, or combine tone + address detection for greater accuracy -- **ICAD Transcribe integration** — thanks to [TheGreatCodeholio/icad_transcribe](https://github.com/TheGreatCodeholio/icad_transcribe) for providing advanced radio-optimized transcription support - ---- - -## ✨ Features - -### 🚀 Core -- **One-command startup:** `node bot.js` -- **Automatic setup:** database, API keys, talkgroups, admin accounts -- **Integrated services:** Discord bot + webserver run together - -### 🗺️ Mapping -- Real-time calls displayed on a Leaflet map -- Marker clustering, heatmaps, day/night/satellite views -- Call details with transcript + audio playback -- Call filtering and marker editing (admin-restricted when auth enabled) -- **Call purging:** Admin-only bulk removal with undo functionality - -### 🎤 Transcription -- **Local:** `faster-whisper` (CPU or NVIDIA GPU) -- **Remote:** via [speaches](https://github.com/speaches-ai/speaches) or custom servers -- **OpenAI Whisper API** with support for custom prompts -- **ICAD Transcribe** for radio-optimized results - -### 🤖 AI Enhancements -- Address extraction + geocoding (Google Maps or LocationIQ) -- AI summaries of recent transmissions -- "Ask AI" chat about call history -- Optional two‑tone detection for toned call filtering - -### 🎮 Discord Integration -- Auto-post transcriptions by talkgroup -- Keyword alerts -- AI summaries with refresh buttons -- Optional: live audio in voice channels - -### 🔒 Security -- Optional user authentication -- Auto-generated API keys -- Secure session management -- Admin-only controls for sensitive operations - ---- - -## 📦 Installation - -Supports **Windows 10/11** and **Debian/Ubuntu Linux**. -Installation scripts handle dependencies, configuration, and setup. - -### Prerequisites -- SDRTrunk, TrunkRecorder, or rdio-scanner configured -- Talkgroup export from RadioReference (Premium subscription recommended) -- API key for **Google Maps** or **LocationIQ** -- (Optional) NVIDIA GPU for local transcription -- (Optional) Discord Bot application -- (Optional) Remote transcription server (e.g., [speaches](https://github.com/speaches-ai/speaches) or ICAD) - -### Quick Start +# Scanner Map + +Real-time mapping of radio calls: ingest from TrunkRecorder, SDRTrunk, icad, rdio-scanner, or Discord; transcribe with OpenAI, iCAD, or local Whisper/Qwen3-ASR; display on an interactive map with optional Discord/TalkGroup notifications. + +## Quick start (Docker — recommended) + ```bash -# Linux -sudo bash linux_install_scanner_map.sh +git clone https://github.com/Dadud/Scanner-map.git +cd Scanner-map/docker +cp .env.example .env +# Edit .env: DISCORD_TOKEN, API_KEY, at least one geocoding key, etc. +docker compose up -d +``` + +Open **http://localhost:3000** (map), **http://localhost:3000/settings** (server admin console), and **http://localhost:3000/setup** (first-run wizard). + +**Beta testers (upstream):** `ghcr.io/poisonednumber/scanner-map:beta` — see [CONTRIBUTING.md](CONTRIBUTING.md) for the `dev` → `beta` promotion workflow. -# Windows (PowerShell as Admin) -.\install_scanner_map.ps1 +### Compose profiles + +| Profile | Use case | +|---------|----------| +| *(default)* | Core app only — use OpenAI or remote iCAD for transcription | +| `local-whisper` | Add Faster-Whisper sidecar (`LOCAL_TRANSCRIPTION=true`) | +| `local-qwen` | Add Qwen3-ASR sidecar (`LOCAL_TRANSCRIPTION_BACKEND=qwen3-asr`) | +| `gpu` | NVIDIA runtime for local models (see `docker-compose.gpu.yml`) | + +```bash +docker compose --profile local-whisper up -d +docker compose -f docker-compose.yml -f docker-compose.gpu.yml --profile gpu --profile local-qwen up -d ``` -Then: +### Unraid + +Import templates from [`unraid/`](unraid/) — see [unraid/README-unraid.md](unraid/README-unraid.md). + +## Native install (development) + +```bash +npm install +npm run setup # guided wizard → .env +npm run doctor # verify dependencies +npm start # bot + webserver +``` + +Python deps for local transcription: + ```bash -cd scanner-map -source .venv/bin/activate # Linux -node bot.js +npm run install:python-deps -- --backend whisper # or qwen, tone, all ``` ---- +## Configuration -## ⚙️ Configuration +- **First run:** `/setup` wizard when `ENABLE_SETUP=true` (default). +- **Ongoing:** `/settings` server admin console — General, Discord, Dispatch, Talkgroups & Radio, Transcription, Storage & AI, Geocoding, Diagnostics. +- **Talkgroups:** Server Settings → Talkgroups & Radio → **Import talkgroups.csv** (RadioReference format) or full TrunkRecorder config bundle. +- **Environment:** see `docker/.env.example` and [Configuration Reference](#configuration-reference). -All main settings are in `.env`. Key options: +| URL | Purpose | +|-----|---------| +| `/` | Map UI — **Map Preferences** in the Account menu | +| `/settings#radio` | Server settings — talkgroup import | +| `/setup` | First-run setup wizard | -- `DISCORD_TOKEN` — your bot token -- `Maps_API_KEY` / `LOCATIONIQ_API_KEY` — geocoding provider -- `MAPPED_TALK_GROUPS` — talkgroups to monitor -- `TRANSCRIPTION_MODE` — `local`, `remote`, `openai`, or `icad` -- `STORAGE_MODE` — `local` or `s3` -- `OPENAI_PROMPT` — (if using OpenAI) provide a custom transcription prompt -- `ENABLE_TONE_DETECTION` — enable/disable two‑tone detection +Key transcription settings: -Other files to edit: -- `public/config.js` ← map defaults (center, zoom, icons, etc.) -- `data/apikeys.json` ← auto-generated on first run +| Setting | Description | +|---------|-------------| +| `TRANSCRIPTION_MODE` | `local`, `remote`, `openai`, or `icad` | +| `LOCAL_TRANSCRIPTION_BACKEND` | `faster-whisper` (default) or `qwen3-asr` | +| `QWEN_ASR_MODEL` | e.g. `Qwen/Qwen3-ASR-0.6B` | +| `ENABLE_TONE_DETECTION` | Two-tone / pager detection (optional Python deps) | ---- +## Architecture -## 📡 Connecting Your Radio Software +- **`bot.js`** — Discord bot, audio ingestion, transcription orchestration. +- **`webserver.js`** — Map UI, REST API, geocode proxy (API keys never sent to browser). +- **`src/`** — Config, DB migrations, settings service, job persistence, ingestion adapters. +- **`transcription/`** — Python router (`transcribe.py`) with pluggable backends. -- **SDRTrunk:** Configure Streaming → Rdio Scanner endpoint -- **TrunkRecorder:** Add an `uploadServer` entry pointing to `http://:/api/call-upload` -- **rdio-scanner downstream:** Add server + API key +Upstream modular stack: [poisonednumber/Scanner-map PRs #9–#14](https://github.com/poisonednumber/Scanner-map/pulls). ---- +## Security -## 💻 System Requirements -- OS: Windows 10/11 or Debian/Ubuntu -- CPU: Modern multi-core -- RAM: 16GB+ recommended -- GPU: (Optional) NVIDIA CUDA (8GB+ VRAM recommended) -- Storage: SSD (5—10GB for models + audio) +- API keys stored as HMAC fingerprints (fast validation path). +- Geocoding keys proxied server-side when configured. +- Optional `ENABLE_AUTH` for `/audio/:id` and admin routes. +- Rate limits on upload and audio endpoints. + +## Development + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the `dev` / `beta` / `main` workflow. + +```bash +npm test +npm run check:syntax +npm run migrate +``` ---- +## Configuration reference -## 🛠 Troubleshooting -- Logs: `combined.log` and `error.log` -- Check `.env` values (especially API keys and modes) -- Verify dependencies: Node, Python, FFmpeg, CUDA (if using GPU) -- Ensure correct geocoding.js (Google vs LocationIQ) +See `docker/.env.example` for the full list. Required for most deployments: ---- +- `DISCORD_TOKEN`, `CLIENT_ID` — Discord bot +- `API_KEY` — upload authentication +- `GOOGLE_MAPS_API_KEY` or `LOCATIONIQ_API_KEY` — geocoding +- `OPENAI_API_KEY` — if using OpenAI transcription/summary -## 🤝 Contributing -Pull requests and issue reports are welcome. +## License -## 📬 Support -- Open a GitHub Issue -- Contact **poisonednumber** on Discord +See repository license file. diff --git a/bot.js b/bot.js index 28df574..c3b590c 100644 --- a/bot.js +++ b/bot.js @@ -1,6 +1,30 @@ // bot.js - Main Discord bot application with integrated webserver and initialization require('dotenv').config(); +const { loadConfig } = require('./src/config'); +const { applyMigrations } = require('./src/db/migrations'); +const { normalizeIncomingCall } = require('./src/ingestion/normalizeCall'); +const { getRuntimeConfig, getSetupStatus } = require('./src/settings/settingsService'); +const { + JOB_TYPES, + createProcessingJob, + markJobCompleted, + markJobFailed, + markJobProcessing +} = require('./src/jobs/processingJobs'); +const { + buildFingerprintIndex, + validateApiKeyFast, + attachFingerprintToNewKey, +} = require('./src/auth/apiKeyValidation'); +const { applyRuntimeConfigToProcessEnv } = require('./src/settings/runtimeEnv'); +const { + importTrunkRecorderConfig, + importTrunkRecorderConfigBundle, + upsertTalkGroupFromUpload, +} = require('./src/radio/trunkRecorderSync'); +const { reimportFromSavedState } = require('./src/radio/importState'); +const { importTalkgroupsFromCsv } = require('./src/radio/talkgroupImport'); // Get environment variables first, before any usage const { @@ -69,49 +93,30 @@ const { TONE_TIME_RESOLUTION_MS } = process.env; -// --- VALIDATE AI-RELATED ENV VARS --- -if (!AI_PROVIDER) { - console.error("FATAL: AI_PROVIDER is not set in the .env file. Please specify 'ollama' or 'openai'."); - process.exit(1); -} - -if (AI_PROVIDER.toLowerCase() === 'openai') { - if (!OPENAI_API_KEY || !OPENAI_MODEL) { - console.error("FATAL: AI_PROVIDER is 'openai', but OPENAI_API_KEY or OPENAI_MODEL is missing in the .env file."); - process.exit(1); - } -} else if (AI_PROVIDER.toLowerCase() === 'ollama') { - if (!OLLAMA_URL || !OLLAMA_MODEL) { - console.error("FATAL: AI_PROVIDER is 'ollama', but OLLAMA_URL or OLLAMA_MODEL is missing in the .env file."); - process.exit(1); +const startupConfig = loadConfig(process.env); +if (!startupConfig.isValid) { + console.warn('WARNING: Configuration has issues. Setup mode will remain available:'); + for (const error of startupConfig.errors) { + console.warn(`- ${error.key}: ${error.message}`); } -} else { - console.error(`FATAL: Invalid AI_PROVIDER specified in .env file: '${AI_PROVIDER}'. Must be 'openai' or 'ollama'.`); - process.exit(1); } -// --- END VALIDATION --- // --- VALIDATE TRANSCRIPTION-RELATED ENV VARS --- const effectiveTranscriptionMode = TRANSCRIPTION_MODE || 'local'; // Keep this to ensure a default if (!['local', 'remote', 'openai', 'icad'].includes(effectiveTranscriptionMode)) { - console.error(`FATAL: Invalid TRANSCRIPTION_MODE specified in .env file: '${TRANSCRIPTION_MODE}'. Must be 'local', 'remote', 'openai', or 'icad'.`); - process.exit(1); + console.warn(`WARNING: Invalid TRANSCRIPTION_MODE specified in .env file: '${TRANSCRIPTION_MODE}'. Use /setup to choose local, remote, openai, or icad.`); } if (effectiveTranscriptionMode === 'local' && !TRANSCRIPTION_DEVICE) { - console.error("FATAL: TRANSCRIPTION_MODE is 'local', but TRANSCRIPTION_DEVICE is missing in the .env file. Please set it to 'cuda' for a GPU or 'cpu' for CPU."); - process.exit(1); + console.warn("WARNING: TRANSCRIPTION_MODE is 'local', but TRANSCRIPTION_DEVICE is missing. Use /setup to choose cpu or cuda."); } if (effectiveTranscriptionMode === 'remote' && !FASTER_WHISPER_SERVER_URL) { - console.error("FATAL: TRANSCRIPTION_MODE is 'remote', but FASTER_WHISPER_SERVER_URL is missing in the .env file."); - process.exit(1); + console.warn("WARNING: TRANSCRIPTION_MODE is 'remote', but FASTER_WHISPER_SERVER_URL is missing. Use /setup to configure it."); } if (effectiveTranscriptionMode === 'openai' && !OPENAI_API_KEY) { - console.error("FATAL: TRANSCRIPTION_MODE is 'openai', but OPENAI_API_KEY is missing in the .env file. This is required for OpenAI transcriptions."); - process.exit(1); + console.warn("WARNING: TRANSCRIPTION_MODE is 'openai', but OPENAI_API_KEY is missing. Use /setup to configure it."); } if (effectiveTranscriptionMode === 'icad' && !ICAD_URL) { - console.error("FATAL: TRANSCRIPTION_MODE is 'icad', but ICAD_URL is missing in the .env file. Please set it to your ICAD API endpoint URL."); - process.exit(1); + console.warn("WARNING: TRANSCRIPTION_MODE is 'icad', but ICAD_URL is missing. Use /setup to configure it."); } // --- END VALIDATION --- @@ -146,19 +151,19 @@ const LOOKBACK_HOURS = isNaN(parsedLookbackHours) ? 1 : parsedLookbackHours; const LOOKBACK_PERIOD = LOOKBACK_HOURS * 60 * 60 * 1000; // Convert hours to milliseconds // Parse the string into an array -const MAPPED_TALK_GROUPS = mappedTalkGroupsString +let MAPPED_TALK_GROUPS = mappedTalkGroupsString ? mappedTalkGroupsString.split(',').map(id => id.trim()) : []; -const IS_MAPPED_TALK_GROUPS_ENABLED = ENABLE_MAPPED_TALK_GROUPS.toLowerCase() === 'true'; +let IS_MAPPED_TALK_GROUPS_ENABLED = ENABLE_MAPPED_TALK_GROUPS.toLowerCase() === 'true'; // Parse Two-Tone configuration -const IS_TWO_TONE_MODE_ENABLED = ENABLE_TWO_TONE_MODE.toLowerCase() === 'true'; -const TWO_TONE_TALK_GROUPS = twoToneTalkGroupsString +let IS_TWO_TONE_MODE_ENABLED = ENABLE_TWO_TONE_MODE?.toLowerCase() === 'true'; +let TWO_TONE_TALK_GROUPS = twoToneTalkGroupsString ? twoToneTalkGroupsString.split(',').map(id => id.trim()) : []; -const TWO_TONE_QUEUE_SIZE_VALUE = parseInt(TWO_TONE_QUEUE_SIZE, 10) || 1; +let TWO_TONE_QUEUE_SIZE_VALUE = parseInt(TWO_TONE_QUEUE_SIZE, 10) || 1; // Validate required two-tone environment variables if two-tone mode is enabled -if (ENABLE_TWO_TONE_MODE && ENABLE_TWO_TONE_MODE.toLowerCase() === 'true') { +if (ENABLE_TWO_TONE_MODE?.toLowerCase() === 'true') { const requiredTwoToneVars = [ 'ENABLE_TWO_TONE_MODE', 'TWO_TONE_TALK_GROUPS', 'TWO_TONE_QUEUE_SIZE', 'TONE_DETECTION_TYPE', 'TWO_TONE_MIN_TONE_LENGTH', 'TWO_TONE_MAX_TONE_LENGTH', @@ -170,13 +175,11 @@ if (ENABLE_TWO_TONE_MODE && ENABLE_TWO_TONE_MODE.toLowerCase() === 'true') { const missingVars = requiredTwoToneVars.filter(varName => !process.env[varName]); if (missingVars.length > 0) { - console.error(`FATAL: Two-tone mode is enabled but missing required environment variables: ${missingVars.join(', ')}`); - console.error('Please add these variables to your .env file. See TWO_TONE_ENV_ADDITIONS.txt for the complete list.'); - process.exit(1); + console.warn(`WARNING: Two-tone mode is enabled but missing required environment variables: ${missingVars.join(', ')}. Use /setup or .env to complete this before enabling bot services.`); } } -const TWO_TONE_CONFIG = { +let TWO_TONE_CONFIG = { detectionType: TONE_DETECTION_TYPE, minToneLength: parseFloat(TWO_TONE_MIN_TONE_LENGTH), maxToneLength: parseFloat(TWO_TONE_MAX_TONE_LENGTH), @@ -193,9 +196,37 @@ const TWO_TONE_CONFIG = { timeResolutionMs: parseInt(TONE_TIME_RESOLUTION_MS, 10) }; +function refreshRuntimeGlobalsFromEnv(env = process.env) { + MAPPED_TALK_GROUPS = env.MAPPED_TALK_GROUPS + ? env.MAPPED_TALK_GROUPS.split(',').map((id) => id.trim()).filter(Boolean) + : []; + IS_MAPPED_TALK_GROUPS_ENABLED = (env.ENABLE_MAPPED_TALK_GROUPS || 'true').toLowerCase() === 'true'; + IS_TWO_TONE_MODE_ENABLED = (env.ENABLE_TWO_TONE_MODE || 'false').toLowerCase() === 'true'; + TWO_TONE_TALK_GROUPS = env.TWO_TONE_TALK_GROUPS + ? env.TWO_TONE_TALK_GROUPS.split(',').map((id) => id.trim()).filter(Boolean) + : []; + TWO_TONE_QUEUE_SIZE_VALUE = parseInt(env.TWO_TONE_QUEUE_SIZE, 10) || 1; + TWO_TONE_CONFIG = { + detectionType: env.TONE_DETECTION_TYPE, + minToneLength: parseFloat(env.TWO_TONE_MIN_TONE_LENGTH), + maxToneLength: parseFloat(env.TWO_TONE_MAX_TONE_LENGTH), + pulsedMinCycles: parseInt(env.PULSED_MIN_CYCLES, 10), + pulsedMinOnMs: parseInt(env.PULSED_MIN_ON_MS, 10), + pulsedMaxOnMs: parseInt(env.PULSED_MAX_ON_MS, 10), + pulsedMinOffMs: parseInt(env.PULSED_MIN_OFF_MS, 10), + pulsedMaxOffMs: parseInt(env.PULSED_MAX_OFF_MS, 10), + pulsedBandwidthHz: parseInt(env.PULSED_BANDWIDTH_HZ, 10), + longToneMinLength: parseFloat(env.LONG_TONE_MIN_LENGTH), + longToneBandwidthHz: parseInt(env.LONG_TONE_BANDWIDTH_HZ, 10), + detectionThreshold: parseFloat(env.TONE_DETECTION_THRESHOLD), + frequencyBand: env.TONE_FREQUENCY_BAND, + timeResolutionMs: parseInt(env.TONE_TIME_RESOLUTION_MS, 10), + }; +} + // Parse MAX_CONCURRENT_TRANSCRIPTIONS from env or use default -const parsedMaxConcurrent = parseInt(MAX_CONCURRENT_TRANSCRIPTIONS, 10); -const MAX_CONCURRENT_TRANSCRIPTIONS_VALUE = !isNaN(parsedMaxConcurrent) && parsedMaxConcurrent > 0 ? parsedMaxConcurrent : 3; +let parsedMaxConcurrent = parseInt(MAX_CONCURRENT_TRANSCRIPTIONS, 10); +let MAX_CONCURRENT_TRANSCRIPTIONS_VALUE = !isNaN(parsedMaxConcurrent) && parsedMaxConcurrent > 0 ? parsedMaxConcurrent : 3; // High-volume system optimizations const MAX_QUEUE_SIZE = 50; // Limit queue size to prevent memory issues @@ -392,8 +423,8 @@ function cleanStaleQueueEntries() { function detectTwoTone(audioFilePath, transcriptionId, talkGroupID, callback) { // For non-local modes, use a separate Python process for tone detection - if (effectiveTranscriptionMode !== 'local') { - logger.info(`Using standalone tone detection for ${effectiveTranscriptionMode} mode`); + if (activeTranscriptionMode !== 'local') { + logger.info(`Using standalone tone detection for ${activeTranscriptionMode} mode`); return detectTwoToneStandalone(audioFilePath, transcriptionId, talkGroupID, callback); } @@ -411,6 +442,15 @@ function detectTwoTone(audioFilePath, transcriptionId, talkGroupID, callback) { callback, startTime: Date.now() }); + + setTimeout(() => { + const pending = pendingToneDetections.get(requestId); + if (pending) { + pendingToneDetections.delete(requestId); + logger.warn(`Tone detection TTL expired for request ${requestId}`); + if (pending.callback) pending.callback(false, new Error('Tone detection timeout')); + } + }, 60000); logger.info(`Starting tone detection for ID ${transcriptionId} (request: ${requestId})`); @@ -818,24 +858,7 @@ const logger = winston.createLogger({ // --- NEW: Add S3 Client Setup --- const AWS = require('aws-sdk'); -let s3 = null; -if (STORAGE_MODE === 's3') { - if (!S3_ENDPOINT || !S3_BUCKET_NAME || !S3_ACCESS_KEY_ID || !S3_SECRET_ACCESS_KEY) { - logger.error('FATAL: STORAGE_MODE is s3, but required S3 environment variables are missing! Check bot .env'); - process.exit(1); // Exit if S3 config is incomplete - } - AWS.config.update({ - accessKeyId: S3_ACCESS_KEY_ID, - secretAccessKey: S3_SECRET_ACCESS_KEY, - endpoint: S3_ENDPOINT, - s3ForcePathStyle: true, // Necessary for MinIO/non-AWS S3 - signatureVersion: 'v4' - }); - s3 = new AWS.S3(); - logger.info(`[Bot] Storage mode set to S3. Endpoint: ${S3_ENDPOINT}, Bucket: ${S3_BUCKET_NAME}`); -} else { - logger.info('[Bot] Storage mode set to local.'); -} +logger.info(`[Bot] Startup storage mode from .env: ${STORAGE_MODE || 'local'}. Runtime settings may override this after database initialization.`); // --- END S3 Client Setup --- // --- INITIALIZATION FUNCTIONS --- @@ -918,19 +941,19 @@ function ensureApiKey() { // Create a default API key const defaultKey = uuidv4(); const hashedKey = bcrypt.hashSync(defaultKey, 10); - const initialApiKeys = [{ - key: hashedKey, - name: 'Default', + const initialApiKeys = [attachFingerprintToNewKey({ + key: hashedKey, + name: 'Default', disabled: false, created_at: new Date().toISOString(), - description: 'Auto-generated API key for first boot' - }]; + description: 'Auto-generated API key for first boot', + }, defaultKey)]; fs.writeFileSync(API_KEY_FILE, JSON.stringify(initialApiKeys, null, 2)); - logger.info(`Created default API key (ID: ${initialApiKeys[0].id})`); + logger.info('Created default API key (ID: Default)'); logger.info(`API key saved to: ${API_KEY_FILE}`); - logger.warn('IMPORTANT: Save this API key as it won\'t be shown again!'); + logger.warn('IMPORTANT: Save the generated API key from setup — it will not be logged again.'); resolve(defaultKey); } else { logger.info('API key file already exists.'); @@ -944,111 +967,93 @@ function ensureApiKey() { } // Function to initialize database tables -function initializeDatabase() { - return new Promise((resolve, reject) => { - logger.info('Initializing database tables...'); - - db.serialize(() => { - let tablesCreated = 0; - let totalTables = ENABLE_AUTH?.toLowerCase() === 'true' ? 7 : 5; - - const tableCreated = (err, tableName) => { - if (err) { - logger.error(`Error creating ${tableName} table:`, err); - reject(err); - return; - } - tablesCreated++; - if (tablesCreated === totalTables) { - logger.info('Database tables initialized successfully.'); - resolve(); - } - }; +async function initializeDatabase() { + logger.info('Initializing database tables...'); + const applied = await applyMigrations(db, { + enableAuth: ENABLE_AUTH?.toLowerCase() === 'true' + }); - db.run(`CREATE TABLE IF NOT EXISTS transcriptions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - talk_group_id TEXT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - transcription TEXT, - audio_file_path TEXT, - address TEXT, - lat REAL, - lon REAL, - category TEXT - )`, (err) => tableCreated(err, 'transcriptions')); - - db.run(`CREATE TABLE IF NOT EXISTS global_keywords ( - keyword TEXT UNIQUE, - talk_group_id TEXT - )`, (err) => tableCreated(err, 'global_keywords')); - - db.run(`CREATE TABLE IF NOT EXISTS talk_groups ( - id TEXT PRIMARY KEY, - hex TEXT, - alpha_tag TEXT, - mode TEXT, - description TEXT, - tag TEXT, - county TEXT - )`, (err) => tableCreated(err, 'talk_groups')); - - db.run(`CREATE TABLE IF NOT EXISTS frequencies ( - id INTEGER PRIMARY KEY, - frequency TEXT, - description TEXT - )`, (err) => tableCreated(err, 'frequencies')); - - db.run(`CREATE TABLE IF NOT EXISTS audio_files ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - transcription_id INTEGER, - audio_data BLOB, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(transcription_id) REFERENCES transcriptions(id) - )`, (err) => tableCreated(err, 'audio_files')); - - // Authentication tables (if auth is enabled) - if (ENABLE_AUTH?.toLowerCase() === 'true') { - db.run(`CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - salt TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - )`, (err) => tableCreated(err, 'users')); - - db.run(`CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - token TEXT UNIQUE NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - expires_at DATETIME NOT NULL, - last_activity DATETIME DEFAULT CURRENT_TIMESTAMP, - ip_address TEXT, - user_agent TEXT, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - )`, (err) => tableCreated(err, 'sessions')); - } + if (applied.length > 0) { + logger.info(`Applied database migrations: ${applied.join(', ')}`); + } else { + logger.info('Database schema already up to date.'); + } +} - // Create indexes for commonly queried columns - db.run(`CREATE INDEX IF NOT EXISTS idx_transcriptions_timestamp ON transcriptions(timestamp)`, (err) => { - if (err) logger.warn('Error creating timestamp index:', err.message); - }); - db.run(`CREATE INDEX IF NOT EXISTS idx_transcriptions_coords ON transcriptions(lat, lon) WHERE lat IS NOT NULL`, (err) => { - if (err) logger.warn('Error creating coords index:', err.message); - }); - db.run(`CREATE INDEX IF NOT EXISTS idx_transcriptions_talkgroup ON transcriptions(talk_group_id)`, (err) => { - if (err) logger.warn('Error creating talkgroup index:', err.message); - }); - db.run(`CREATE INDEX IF NOT EXISTS idx_transcriptions_category ON transcriptions(category)`, (err) => { - if (err) logger.warn('Error creating category index:', err.message); - }); - db.run(`CREATE INDEX IF NOT EXISTS idx_audio_transcription ON audio_files(transcription_id)`, (err) => { - if (err) logger.warn('Error creating audio index:', err.message); - }); - }); +async function getBotRuntimeConfig() { + return getRuntimeConfig(db, process.env); +} + +async function getBotTranscriptionConfig() { + const runtime = await getBotRuntimeConfig(); + const mode = (runtime.settings.transcriptionMode || TRANSCRIPTION_MODE || 'local').toLowerCase(); + + return { + mode: ['local', 'remote', 'openai', 'icad'].includes(mode) ? mode : 'local', + device: (runtime.settings.transcriptionDevice || TRANSCRIPTION_DEVICE || 'cpu').toLowerCase(), + fasterWhisperServerUrl: runtime.settings.fasterWhisperServerUrl || FASTER_WHISPER_SERVER_URL || '', + whisperModel: runtime.settings.whisperModel || WHISPER_MODEL || 'large-v3', + openaiApiKey: runtime.secrets.openaiApiKey || OPENAI_API_KEY || '', + openaiTranscriptionPrompt: runtime.settings.openaiTranscriptionPrompt || OPENAI_TRANSCRIPTION_PROMPT || '', + openaiTranscriptionModel: runtime.settings.openaiTranscriptionModel || OPENAI_TRANSCRIPTION_MODEL || 'whisper-1', + openaiTranscriptionTemperature: runtime.settings.openaiTranscriptionTemperature || OPENAI_TRANSCRIPTION_TEMPERATURE || '0.0', + icadUrl: runtime.settings.icadUrl || ICAD_URL || '', + icadProfile: runtime.settings.icadProfile || ICAD_PROFILE || 'whisper-1', + icadApiKey: runtime.secrets.icadApiKey || ICAD_API_KEY || '' + }; +} + +async function getBotStorageConfig() { + const runtime = await getBotRuntimeConfig(); + const mode = (runtime.settings.storageMode || STORAGE_MODE || 'local').toLowerCase(); + + return { + mode: mode === 's3' ? 's3' : 'local', + s3Endpoint: runtime.settings.s3Endpoint || S3_ENDPOINT || '', + s3BucketName: runtime.settings.s3BucketName || S3_BUCKET_NAME || '', + s3AccessKeyId: runtime.secrets.s3AccessKeyId || S3_ACCESS_KEY_ID || '', + s3SecretAccessKey: runtime.secrets.s3SecretAccessKey || S3_SECRET_ACCESS_KEY || '' + }; +} + +function createS3Client(storageConfig) { + return new AWS.S3({ + accessKeyId: storageConfig.s3AccessKeyId, + secretAccessKey: storageConfig.s3SecretAccessKey, + endpoint: storageConfig.s3Endpoint, + s3ForcePathStyle: true, + signatureVersion: 'v4' }); } +function isS3Ready(storageConfig) { + return Boolean( + storageConfig.mode === 's3' && + storageConfig.s3Endpoint && + storageConfig.s3BucketName && + storageConfig.s3AccessKeyId && + storageConfig.s3SecretAccessKey + ); +} + +function getToneAudioPath(storageConfig, audioFilePath) { + if (storageConfig.mode === 's3' && storageConfig.s3Endpoint && storageConfig.s3BucketName) { + return `https://${storageConfig.s3Endpoint.replace('https://', '').replace('http://', '')}/${storageConfig.s3BucketName}/${audioFilePath}`; + } + return path.join(__dirname, 'audio', audioFilePath); +} + +async function getPublicAudioUrl(audioId) { + let publicDomain = PUBLIC_DOMAIN || 'localhost'; + try { + const runtime = await getBotRuntimeConfig(); + publicDomain = runtime.settings.publicDomain || publicDomain; + } catch (error) { + logger.warn(`Could not load runtime public domain; falling back to startup config: ${error.message}`); + } + return `http://${publicDomain}/audio/${audioId}`; +} + // Function to create admin user if authentication is enabled function createAdminUser() { return new Promise((resolve, reject) => { @@ -1093,7 +1098,7 @@ function createAdminUser() { reject(err); } else { logger.info('Created admin user for webserver authentication.'); - logger.info('Created admin user for webserver authentication.'); + logger.info(`Admin credentials: username=admin, password=${WEBSERVER_PASSWORD}`); resolve(); } } @@ -1133,12 +1138,39 @@ async function initializeBot() { // Step 1: Initialize database await initializeDatabase(); + const runtime = await getBotRuntimeConfig(); + applyRuntimeConfigToProcessEnv(runtime, process.env); + refreshRuntimeGlobalsFromEnv(process.env); + parsedMaxConcurrent = parseInt(process.env.MAX_CONCURRENT_TRANSCRIPTIONS, 10); + MAX_CONCURRENT_TRANSCRIPTIONS_VALUE = !isNaN(parsedMaxConcurrent) && parsedMaxConcurrent > 0 + ? parsedMaxConcurrent + : 3; + + if ((runtime.settings.autoSyncTrunkRecorderOnStartup || 'false').toLowerCase() === 'true') { + try { + if (runtime.settings.trunkRecorderConfigPath) { + const summary = await importTrunkRecorderConfig(db, runtime.settings.trunkRecorderConfigPath); + logger.info(`TrunkRecorder auto-sync imported ${summary.talkgroupsImported} talkgroups and ${summary.channelsImported} channels`); + } else { + const result = await reimportFromSavedState(db, { + importTalkgroupsFromCsv, + importTrunkRecorderConfigBundle, + importTrunkRecorderConfig, + }); + logger.info(`Radio auto-sync (${result.importMode}) completed: ${JSON.stringify(result.summary)}`); + } + } catch (error) { + logger.warn(`Radio auto-sync failed: ${error.message}`); + } + } + // Step 2: Ensure API key exists const newApiKey = await ensureApiKey(); if (newApiKey) { // Log the new API key one more time for visibility console.log('='.repeat(60)); - console.log('NEW API KEY GENERATED'); + console.log('NEW API KEY GENERATED:'); + console.log(newApiKey); console.log('Please save this key - it will not be shown again!'); console.log('='.repeat(60)); } @@ -1163,16 +1195,23 @@ async function initializeBot() { // Step 6: Create admin user for webserver if auth is enabled await createAdminUser(); - // Step 7: Start bot services (Discord and Express API) - await startBotServices(); - - // Step 8: Start webserver last - if (WEBSERVER_PORT && (GOOGLE_MAPS_API_KEY || LOCATIONIQ_API_KEY)) { + // Step 7: Start webserver before Discord so setup can run even when bot settings are incomplete + if (WEBSERVER_PORT) { await startWebserver(); } else { - logger.warn('Webserver not started: WEBSERVER_PORT or geocoding API key (GOOGLE_MAPS_API_KEY or LOCATIONIQ_API_KEY) not configured'); + logger.warn('Webserver not started: WEBSERVER_PORT is not configured'); } + // Step 8: In setup mode, keep the browser console available without forcing Discord login + const setupStatus = await getSetupStatus(db, process.env); + if (setupStatus.setupRequired) { + logger.warn(`Setup is incomplete (${setupStatus.missing.join(', ') || 'unknown requirements'}). Discord bot services will start after setup is completed and the app is restarted.`); + return true; + } + + // Step 9: Start bot services (Discord and Express API) + await startBotServices(); + logger.info('Bot initialization completed successfully!'); return true; } catch (error) { @@ -1217,6 +1256,15 @@ const { extractAddress, geocodeAddress, hyperlinkAddress, loadTalkGroups } = req // Express app setup const app = express(); +const rateLimit = require('express-rate-limit'); +const uploadRateLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 120, + standardHeaders: true, + legacyHeaders: false, + message: 'Too many uploads, please try again later.', +}); +app.use('/api/call-upload', uploadRateLimiter); const PORT_NUM = parseInt(PORT, 10); // Discord client setup @@ -1232,10 +1280,14 @@ const client = new Client({ // Global variables let alertChannel; const UPLOAD_DIR = path.join(__dirname, 'audio'); -let transcriptionQueue = []; +let transcriptionQueue = createTranscriptionQueue(); +let apiKeyFingerprintIndex = new Map(); +let activeTranscriptions = 0; +let isBootComplete = false; const messageCache = new Map(); // Stores the latest message for each channel const MESSAGE_COOLDOWN = 15000; // 15 seconds in milliseconds let transcriptionProcess = null; +let activeTranscriptionMode = effectiveTranscriptionMode; let isProcessingTranscription = false; let currentTranscriptionId = null; // Track current transcription for timeout let transcriptionTimeout = null; // Timeout for current transcription @@ -1259,7 +1311,6 @@ const db = new sqlite3.Database('./botdata.db', (err) => { process.exit(1); } else { logger.info('Connected to SQLite database.'); - // Enable WAL mode for safer concurrent access from webserver.js db.run('PRAGMA journal_mode = WAL;'); db.run('PRAGMA busy_timeout = 5000;'); // Trigger initialization after database connection @@ -1277,31 +1328,33 @@ const loadApiKeys = () => { if (fs.existsSync(API_KEY_FILE)) { const data = fs.readFileSync(API_KEY_FILE, 'utf8'); apiKeys = JSON.parse(data); + apiKeyFingerprintIndex = buildFingerprintIndex(apiKeys); logger.info(`Loaded ${apiKeys.length} API keys.`); } else { logger.warn('API key file not found. This should have been created during initialization.'); apiKeys = []; + apiKeyFingerprintIndex = new Map(); } } catch (err) { logger.error('Error loading API keys:', err); apiKeys = []; + apiKeyFingerprintIndex = new Map(); } }; // Helper Functions const validateApiKey = async (key) => { - //logger.info(`Validating API key: ${key.substring(0, 3)}...`); - for (let apiKey of apiKeys) { - if (!apiKey.disabled) { - const match = await bcrypt.compare(key, apiKey.key); - if (match) { - //logger.info('API key validation successful'); - return apiKey; - } + const match = await validateApiKeyFast(key, apiKeys, apiKeyFingerprintIndex); + if (match && match.fingerprint && !match._fingerprintPersisted) { + match._fingerprintPersisted = true; + try { + fs.writeFileSync(API_KEY_FILE, JSON.stringify(apiKeys, null, 2)); + } catch (err) { + logger.warn('Could not persist API key fingerprint cache:', err.message); } } - logger.error('API key validation failed'); - return null; + if (!match) logger.error('API key validation failed'); + return match; }; const generateCustomFilename = (fields, originalFilename) => { @@ -1473,18 +1526,24 @@ app.post('/api/call-upload', (req, res) => { logger.info(`Received SDRTrunk audio: ${customFilename}`); + const normalizedCall = normalizeIncomingCall({ + source: 'sdrtrunk', + fields, + fileInfo + }); + handleNewAudio({ filename: customFilename, path: saveTo, - talkGroupID: fields.talkgroup, - systemName: fields.systemLabel, - talkGroupName: fields.talkgroupLabel, - dateTime: fields.dateTime, // Pass the original fields.dateTime for SDRTrunk - source: fields.source, - talkerAlias: fields.talkerAlias, // Add talkerAlias field from SDRTrunk - frequency: fields.frequency, - talkGroupGroup: fields.talkgroupGroup, - isTrunkRecorder: false + talkGroupID: normalizedCall.talkGroupID, + systemName: normalizedCall.systemName, + talkGroupName: normalizedCall.talkGroupName, + dateTime: normalizedCall.dateTime, + source: normalizedCall.source, + talkerAlias: normalizedCall.talkerAlias, + frequency: normalizedCall.frequency, + talkGroupGroup: normalizedCall.talkGroupGroup, + isTrunkRecorder: normalizedCall.isTrunkRecorder }); return sendResponse(200, 'Call imported successfully.'); @@ -1798,17 +1857,26 @@ app.post('/api/call-upload', (req, res) => { // Log fields before passing to handleNewAudio logger.info(`[UPLOAD] Preparing to call handleNewAudio, fields.srcList=${fields.srcList ? (typeof fields.srcList === 'string' ? `string(${fields.srcList.length} chars)` : `object`) : 'null/undefined'}, fields.freqList=${fields.freqList ? 'exists' : 'null/undefined'}`); + const normalizedCall = normalizeIncomingCall({ + source: inferredSourceSystem === 'rdio-scanner' ? 'rdio-scanner' : 'trunk-recorder', + fields: { + ...fields, + dateTime: Math.floor(callDateTime.getTime() / 1000) + }, + fileInfo + }); + handleNewAudio({ filename: customFilename, path: saveTo, - talkGroupID: fields.talkgroup, - systemName: fields.systemLabel, - talkGroupName: fields.talkgroupLabel, + talkGroupID: normalizedCall.talkGroupID, + systemName: normalizedCall.systemName, + talkGroupName: normalizedCall.talkGroupName, dateTime: Math.floor(callDateTime.getTime() / 1000), // Pass Unix timestamp (seconds) - source: fields.source, - talkerAlias: fields.talkerAlias, // <-- OTA alias from Trunk Recorder - frequency: fields.frequency, - talkGroupGroup: fields.talkgroupGroup, + source: normalizedCall.source, + talkerAlias: normalizedCall.talkerAlias, // <-- OTA alias from Trunk Recorder + frequency: normalizedCall.frequency, + talkGroupGroup: normalizedCall.talkGroupGroup, // Detect TrunkRecorder more reliably: check for TrunkRecorder-specific fields isTrunkRecorder: inferredSourceSystem === 'TrunkRecorder' || (fields.srcList && fields.srcList.trim() !== '' && fields.srcList.trim() !== '[]') || @@ -1876,12 +1944,12 @@ app.get('/audio/:id', (req, res) => { // Function to start the transcription process // Function to start the transcription process async function startTranscriptionProcess() { - // *** ADD THIS CHECK AT THE TOP *** - if (effectiveTranscriptionMode !== 'local') { + const transcriptionConfig = await getBotTranscriptionConfig(); + activeTranscriptionMode = transcriptionConfig.mode; + if (transcriptionConfig.mode !== 'local') { logger.info('Transcription mode is not local, skipping Python process start.'); - return; // Don't start if mode is remote + return; } - // *** END ADDED CHECK *** // Clean up existing process if it exists if (transcriptionProcess) { @@ -2227,7 +2295,7 @@ async function startTranscriptionProcess() { transcriptionProcess.on('error', (err) => { logger.error(`Failed to start local transcription process: ${err.message}`); cleanupTranscriptionProcess(); - if (effectiveTranscriptionMode === 'local') { + if (activeTranscriptionMode === 'local') { logger.info('Will attempt to restart local transcription process in 10 seconds due to spawn error...'); setTimeout(startTranscriptionProcess, 10000); } @@ -2405,7 +2473,7 @@ async function startTranscriptionProcess() { cleanupTranscriptionProcess(); // Only restart if not too many recent failures - if (effectiveTranscriptionMode === 'local') { + if (activeTranscriptionMode === 'local') { if (code === null) { // For null exit codes (startup crashes), wait longer and provide guidance logger.error('STARTUP CRASH DETECTED - Will NOT automatically restart to prevent loop'); @@ -2477,7 +2545,7 @@ function cleanupTranscriptionProcess() { } } } - transcriptionQueue = []; // Clear the queue + transcriptionQueue.clear(); } // Reset all state variables @@ -2524,14 +2592,14 @@ function startProcessHealthCheck() { if (timeSinceActivity > 600000 && queueSize > 0) { // 10 minutes + queue items = real problem logger.error(`Transcription process appears stuck (no activity for 10 minutes with ${queueSize} items queued). Restarting...`); cleanupTranscriptionProcess(); - if (effectiveTranscriptionMode === 'local') { + if (activeTranscriptionMode === 'local') { setTimeout(startTranscriptionProcess, 5000); } return; } else if (timeSinceActivity > 1800000) { // 30 minutes with no activity at all (safety net) logger.warn(`Very long radio silence detected (30+ minutes). Performing health check restart as precaution...`); cleanupTranscriptionProcess(); - if (effectiveTranscriptionMode === 'local') { + if (activeTranscriptionMode === 'local') { setTimeout(startTranscriptionProcess, 5000); } return; @@ -2557,7 +2625,7 @@ function startProcessHealthCheck() { if (queueSize > 15 && !isProcessingTranscription && timeSinceActivity > 300000) { // 5 minutes + 15+ items = real stuck logger.error(`Queue definitely stuck with ${queueSize} items and no processing for 5 minutes. Force restarting transcription process...`); cleanupTranscriptionProcess(); - if (effectiveTranscriptionMode === 'local') { + if (activeTranscriptionMode === 'local') { setTimeout(startTranscriptionProcess, 2000); } } @@ -2624,7 +2692,7 @@ function processNextTranscription() { logger.error(`Transcription timeout for ID ${currentTranscriptionId}. Restarting process...`); // Force restart the process on timeout cleanupTranscriptionProcess(); - if (effectiveTranscriptionMode === 'local') { + if (activeTranscriptionMode === 'local') { setTimeout(startTranscriptionProcess, 5000); } }, TRANSCRIPTION_TIMEOUT_MS); @@ -2658,8 +2726,10 @@ function processNextTranscription() { // *** NEW FUNCTION for Remote Transcription *** async function transcribeAudioRemotely(filePath, callback) { + const transcriptionConfig = await getBotTranscriptionConfig(); + // Ensure URL is configured for remote mode - if (!FASTER_WHISPER_SERVER_URL) { + if (!transcriptionConfig.fasterWhisperServerUrl) { logger.error('FATAL: FASTER_WHISPER_SERVER_URL is not configured for remote mode.'); if (callback) callback(""); // Fail gracefully return; @@ -2691,14 +2761,14 @@ async function transcribeAudioRemotely(filePath, callback) { const form = new FormData(); form.append('file', fs.createReadStream(filePath)); // Append model if specified in environment - if (WHISPER_MODEL) { - form.append('model', WHISPER_MODEL); - logger.info(`Requesting remote model: ${WHISPER_MODEL}`); + if (transcriptionConfig.whisperModel) { + form.append('model', transcriptionConfig.whisperModel); + logger.info(`Requesting remote model: ${transcriptionConfig.whisperModel}`); } // Add language parameter if needed // form.append('language', 'en'); - const apiEndpoint = `${FASTER_WHISPER_SERVER_URL}/v1/audio/transcriptions`; + const apiEndpoint = `${transcriptionConfig.fasterWhisperServerUrl}/v1/audio/transcriptions`; const filenameForLog = path.basename(filePath); logger.info(`Sending remote transcription request for ${filenameForLog} to ${apiEndpoint}`); @@ -2760,8 +2830,10 @@ async function transcribeAudioRemotely(filePath, callback) { } async function transcribeWithOpenAIAPI(filePath, callback) { + const transcriptionConfig = await getBotTranscriptionConfig(); + // Check for API Key - if (!OPENAI_API_KEY) { + if (!transcriptionConfig.openaiApiKey) { logger.error('FATAL: TRANSCRIPTION_MODE is openai, but OPENAI_API_KEY is not configured.'); if (callback) callback(""); // Fail gracefully return; @@ -2779,21 +2851,21 @@ async function transcribeWithOpenAIAPI(filePath, callback) { form.append('file', fs.createReadStream(filePath)); // Use the model from environment variable, fallback to whisper-1 if not set - const modelToUse = OPENAI_TRANSCRIPTION_MODEL || 'whisper-1'; + const modelToUse = transcriptionConfig.openaiTranscriptionModel; form.append('model', modelToUse); // Force language to English for better scanner audio transcription form.append('language', 'en'); // Add temperature parameter for transcription consistency (if supported) - const temperature = OPENAI_TRANSCRIPTION_TEMPERATURE || '0.0'; + const temperature = transcriptionConfig.openaiTranscriptionTemperature; form.append('temperature', temperature); const filenameForLog = path.basename(filePath); // Add custom prompt if configured to improve scanner audio transcription - if (OPENAI_TRANSCRIPTION_PROMPT) { - form.append('prompt', OPENAI_TRANSCRIPTION_PROMPT); + if (transcriptionConfig.openaiTranscriptionPrompt) { + form.append('prompt', transcriptionConfig.openaiTranscriptionPrompt); logger.info(`Using custom OpenAI transcription prompt for ${filenameForLog}`); } @@ -2812,7 +2884,7 @@ async function transcribeWithOpenAIAPI(filePath, callback) { method: 'POST', body: form, headers: { - 'Authorization': `Bearer ${OPENAI_API_KEY}`, + 'Authorization': `Bearer ${transcriptionConfig.openaiApiKey}`, ...form.getHeaders() }, signal: controller.signal @@ -2850,8 +2922,10 @@ async function transcribeWithOpenAIAPI(filePath, callback) { } async function transcribeWithICADAPI(filePath, callback) { + const transcriptionConfig = await getBotTranscriptionConfig(); + // Check for ICAD URL - if (!ICAD_URL) { + if (!transcriptionConfig.icadUrl) { logger.error('FATAL: TRANSCRIPTION_MODE is icad, but ICAD_URL is not configured.'); if (callback) callback(""); // Fail gracefully return; @@ -2869,7 +2943,7 @@ async function transcribeWithICADAPI(filePath, callback) { form.append('file', fs.createReadStream(filePath)); // Set model based on ICAD_PROFILE if provided, otherwise use default - const modelToUse = ICAD_PROFILE || 'whisper-1'; + const modelToUse = transcriptionConfig.icadProfile; form.append('model', modelToUse); // Add standard OpenAI Whisper API parameters that ICAD should understand @@ -2879,9 +2953,9 @@ async function transcribeWithICADAPI(filePath, callback) { // Explicitly disable clip_timestamps to override any profile settings form.append('clip_timestamps', ''); - const apiEndpoint = `${ICAD_URL}/v1/audio/transcriptions`; + const apiEndpoint = `${transcriptionConfig.icadUrl}/v1/audio/transcriptions`; const filenameForLog = path.basename(filePath); - const authStatus = ICAD_API_KEY ? 'with authentication' : 'without authentication'; + const authStatus = transcriptionConfig.icadApiKey ? 'with authentication' : 'without authentication'; logger.info(`Sending ICAD transcription request for ${filenameForLog} to ${apiEndpoint} using model/profile: ${modelToUse} (${authStatus})`); const controller = new AbortController(); @@ -2895,8 +2969,8 @@ async function transcribeWithICADAPI(filePath, callback) { }; // Add authorization header if ICAD_API_KEY is provided - if (ICAD_API_KEY) { - headers['Authorization'] = `Bearer ${ICAD_API_KEY}`; + if (transcriptionConfig.icadApiKey) { + headers['Authorization'] = `Bearer ${transcriptionConfig.icadApiKey}`; } const response = await fetch(apiEndpoint, { @@ -2966,11 +3040,33 @@ function handleNewAudio(audioData) { phase2_tdma, color_code } = audioData; + + const safelyUpdateProcessingJob = (actionDescription, updateFn) => { + updateFn().catch((jobError) => { + logger.warn(`Could not ${actionDescription}: ${jobError.message}`); + }); + }; // Log srcList for debugging logger.info(`[handleNewAudio] Received audio data for ${filename}, srcList=${srcList ? (typeof srcList === 'string' ? `string(${srcList.substring(0, 100)}...)` : `object`) : 'null/undefined'}, isTrunkRecorder=${isTrunkRecorder}`); - // --- Enhanced: Parse Frequencies for Errors/Spikes (Trunk Recorder) --- + getBotRuntimeConfig() + .then((runtime) => { + if ((runtime.settings.autoLearnTalkGroupsFromUploads || 'true').toLowerCase() !== 'true') return null; + return upsertTalkGroupFromUpload(db, { + talkgroup: talkGroupID, + talkgroupLabel: talkGroupName, + talkgroupGroup: talkGroupGroup, + systemLabel: systemName, + phase2_tdma: phase2_tdma, + }); + }) + .then((learned) => { + if (learned) logger.info(`Learned talkgroup ${talkGroupID} from upload metadata`); + }) + .catch((error) => logger.warn(`Talkgroup auto-learn skipped: ${error.message}`)); + + // --- Enhanced: Parse Frequencies for Errors/Spikes (Trunk Recorder) --- let totalErrors = 0; let totalSpikes = 0; let signalQuality = null; @@ -3038,7 +3134,7 @@ function handleNewAudio(audioData) { } // Read file into buffer (This is needed for DB blob AND for S3->Local transcription) - fs.readFile(tempPath, (err, fileBuffer) => { + fs.readFile(tempPath, async (err, fileBuffer) => { if (err) { logger.error(`Error reading audio file ${tempPath}:`, err); // Clean up temp file if read fails @@ -3048,10 +3144,12 @@ function handleNewAudio(audioData) { return; } + const storageConfig = await getBotStorageConfig(); + // --- Start DB Operations --- Miminized changes here // Determine the storage path/key based on STORAGE_MODE let storagePath; - if (STORAGE_MODE === 's3') { + if (storageConfig.mode === 's3') { // For S3, we store the filename as the key (assuming it's unique enough) // You might want a more structured path like 'audio/YYYY/MM/DD/filename' storagePath = filename; @@ -3076,7 +3174,7 @@ function handleNewAudio(audioData) { db.run( `INSERT INTO transcriptions (talk_group_id, timestamp, transcription, audio_file_path, address, lat, lon) VALUES (?, ?, ?, ?, NULL, NULL, NULL)`, [talkGroupID, unixTimestampSeconds, '', storagePath], // Use the Unix timestamp - function (err) { + async function (err) { if (err) { logger.error(`Error inserting initial transcription record for ${filename}:`, err); // If DB insert fails, delete the temp file @@ -3087,10 +3185,28 @@ function handleNewAudio(audioData) { } const transcriptionId = this.lastID; // Get the ID from the database insert + let transcriptionJobId = null; + const transcriptionConfig = await getBotTranscriptionConfig(); logger.info(`Created transcription record ID ${transcriptionId} using storage path: ${storagePath}`); + try { + transcriptionJobId = await createProcessingJob(db, { + transcriptionId, + jobType: JOB_TYPES.TRANSCRIPTION, + payload: { + filename, + talkGroupID, + transcriptionMode: transcriptionConfig.mode, + storageMode: storageConfig.mode + } + }); + logger.info(`Created transcription job ${transcriptionJobId} for transcription ID ${transcriptionId}`); + } catch (jobError) { + logger.warn(`Could not create transcription job for ID ${transcriptionId}: ${jobError.message}`); + } + // Conditionally insert audio blob for Listen Live feature (local storage only) - if (STORAGE_MODE !== 's3') { + if (storageConfig.mode !== 's3') { db.run( `INSERT INTO audio_files (transcription_id, audio_data) VALUES (?, ?)`, [transcriptionId, fileBuffer], @@ -3183,13 +3299,14 @@ function handleNewAudio(audioData) { storagePath, audioPathForSplitting, tempPath, - finalPathIfLocal + finalPathIfLocal, + storageConfig.mode ); } }; // Transcribe based on mode (use the same mode as the main call) - const segmentTranscriptionMode = effectiveTranscriptionMode || 'local'; + const segmentTranscriptionMode = transcriptionConfig.mode; if (segmentTranscriptionMode === 'openai') { transcribeWithOpenAIAPI(segment.audioPath, segmentCallback); } else if (segmentTranscriptionMode === 'remote') { @@ -3229,6 +3346,12 @@ function handleNewAudio(audioData) { logger.warn(warningMsg); updateTranscription(transcriptionId, "", async () => { logger.info(`Updated DB with empty transcription for ID ${transcriptionId}`); + if (transcriptionJobId) { + safelyUpdateProcessingJob('mark transcription job completed', () => markJobCompleted(db, transcriptionJobId, { + empty: true, + reason: 'no_transcription' + })); + } // *** IMPORTANT: Check for two-tone even with empty transcription *** // Tone files might contain only tones without voice content @@ -3236,9 +3359,9 @@ function handleNewAudio(audioData) { logger.info(`Checking for two-tone in talk group ${talkGroupID} (ID: ${transcriptionId}) - empty transcription`); // Use the audio file path for tone detection - const audioPathForTones = STORAGE_MODE === 's3' ? - `https://${S3_ENDPOINT.replace('https://', '').replace('http://', '')}/${S3_BUCKET_NAME}/${filename}` : - (finalPathIfLocal || path.join(__dirname, 'audio', filename)); + const audioPathForTones = storageConfig.mode === 's3' + ? getToneAudioPath(storageConfig, filename) + : (finalPathIfLocal || path.join(__dirname, 'audio', filename)); // Wait for tone detection to complete before continuing await new Promise((resolve) => { @@ -3252,7 +3375,7 @@ function handleNewAudio(audioData) { } // Clean up temp file only if storage was S3 - if (STORAGE_MODE === 's3') { + if (storageConfig.mode === 's3') { // Use setImmediate to avoid file handle race conditions setImmediate(() => { fs.unlink(tempPath, (errUnlink) => { @@ -3283,11 +3406,12 @@ function handleNewAudio(audioData) { emergency, priority, encrypted, call_length, // <-- Pass call metadata freq_error, signalQuality, // <-- Pass signal quality frequency, start_time, stop_time, // <-- Pass timing/frequency - tdma_slot, phase2_tdma, color_code // <-- Pass TDMA/color code + tdma_slot, phase2_tdma, color_code, // <-- Pass TDMA/color code + storageConfig ); // Clean up temp file only if storage was S3 - if (STORAGE_MODE === 's3') { + if (storageConfig.mode === 's3') { // Use setImmediate to avoid file handle race conditions setImmediate(() => { fs.unlink(tempPath, (errUnlink) => { @@ -3299,31 +3423,40 @@ function handleNewAudio(audioData) { }); }); } + if (transcriptionJobId) { + safelyUpdateProcessingJob('mark transcription job completed', () => markJobCompleted(db, transcriptionJobId, { + empty: false, + transcriptionLength: transcriptionText.length + })); + } logger.info(`Successfully processed: ${filename}`); }); }; // --- End common callback definition --- // --- Choose transcription method based on mode --- - logger.info(`Initiating transcription for ID ${transcriptionId} using mode: ${effectiveTranscriptionMode}`); + logger.info(`Initiating transcription for ID ${transcriptionId} using mode: ${transcriptionConfig.mode}`); + if (transcriptionJobId) { + safelyUpdateProcessingJob('mark transcription job processing', () => markJobProcessing(db, transcriptionJobId)); + } - if (effectiveTranscriptionMode === 'openai') { + if (transcriptionConfig.mode === 'openai') { // OpenAI API transcription mode - const pathToUse = (STORAGE_MODE === 'local') ? finalPathIfLocal : tempPath; + const pathToUse = (storageConfig.mode === 'local') ? finalPathIfLocal : tempPath; transcribeWithOpenAIAPI(pathToUse, processingCallback); - } else if (effectiveTranscriptionMode === 'remote') { + } else if (transcriptionConfig.mode === 'remote') { // Use the remote function for faster-whisper server - const pathToUseForRemote = (STORAGE_MODE === 'local') ? finalPathIfLocal : tempPath; + const pathToUseForRemote = (storageConfig.mode === 'local') ? finalPathIfLocal : tempPath; transcribeAudioRemotely(pathToUseForRemote, processingCallback); - } else if (effectiveTranscriptionMode === 'icad') { + } else if (transcriptionConfig.mode === 'icad') { // ICAD API transcription mode (OpenAI-compatible interface) - const pathToUse = (STORAGE_MODE === 'local') ? finalPathIfLocal : tempPath; + const pathToUse = (storageConfig.mode === 'local') ? finalPathIfLocal : tempPath; transcribeWithICADAPI(pathToUse, processingCallback); } else { // 'local' transcription mode const localRequestId = uuidv4(); let payload; - if (STORAGE_MODE === 's3') { + if (storageConfig.mode === 's3') { // S3 Storage + Local Transcription: Send buffer logger.info(`Queueing local transcription (ID: ${localRequestId}) for DB ID ${transcriptionId} using BASE64 BUFFER`); @@ -3417,27 +3550,43 @@ function handleNewAudio(audioData) { // --- End afterStorageComplete function definition --- // --- Handle Audio Storage based on Mode --- - if (STORAGE_MODE === 's3') { + if (storageConfig.mode === 's3') { + if (!isS3Ready(storageConfig)) { + const error = new Error('S3 storage mode is selected, but S3 endpoint, bucket, or credentials are incomplete.'); + logger.error(error.message); + if (transcriptionJobId) { + safelyUpdateProcessingJob('mark transcription job failed', () => markJobFailed(db, transcriptionJobId, error)); + } + db.run('DELETE FROM transcriptions WHERE id = ?', [transcriptionId], () => {}); + fs.unlink(tempPath, (errUnlink) => { + if (errUnlink) logger.error(`Error deleting temp file after incomplete S3 config ${tempPath}:`, errUnlink); + }); + return; + } + const s3Client = createS3Client(storageConfig); // Upload the buffer to S3 const s3Params = { - Bucket: S3_BUCKET_NAME, + Bucket: storageConfig.s3BucketName, Key: storagePath, // Use the determined S3 key Body: fileBuffer, // ContentType: 'audio/mpeg', // Or determine dynamically }; - s3.upload(s3Params, (s3Err, data) => { + s3Client.upload(s3Params, (s3Err, data) => { if (s3Err) { // Check for specific MinIO storage threshold error const errorMessage = s3Err.message || s3Err.toString() || ''; if (errorMessage.includes('minimum free drive threshold') || errorMessage.includes('free drive threshold')) { logger.error(`[MINIO STORAGE FULL] MinIO server has reached its minimum free drive threshold.`); logger.error(`[MINIO STORAGE FULL] Transcription ID ${transcriptionId} could not be uploaded.`); - logger.error(`[MINIO STORAGE FULL] Action required: Free up disk space on MinIO server or delete old objects from bucket: ${S3_BUCKET_NAME}`); + logger.error(`[MINIO STORAGE FULL] Action required: Free up disk space on MinIO server or delete old objects from bucket: ${storageConfig.s3BucketName}`); logger.error(`[MINIO STORAGE FULL] Full error: ${errorMessage}`); } else { logger.error(`Error uploading audio to S3 for transcription ID ${transcriptionId} (key: ${storagePath}):`, s3Err); } // If S3 upload fails, should we delete the DB record? + if (transcriptionJobId) { + safelyUpdateProcessingJob('mark transcription job failed', () => markJobFailed(db, transcriptionJobId, s3Err)); + } db.run('DELETE FROM transcriptions WHERE id = ?', [transcriptionId], () => {}); fs.unlink(tempPath, (errUnlink) => { // Delete temp file on S3 error if (errUnlink) logger.error(`Error deleting temp file after S3 upload error ${tempPath}:`, errUnlink); @@ -3455,6 +3604,9 @@ function handleNewAudio(audioData) { if (renameErr) { logger.error(`Error moving temp file ${tempPath} to final location ${finalLocalPath}:`, renameErr); // If rename fails, delete DB record and original temp file + if (transcriptionJobId) { + safelyUpdateProcessingJob('mark transcription job failed', () => markJobFailed(db, transcriptionJobId, renameErr)); + } db.run('DELETE FROM transcriptions WHERE id = ?', [transcriptionId], () => {}); fs.unlink(tempPath, (errUnlink) => { if (errUnlink) logger.error(`Error deleting temp file after rename error ${tempPath}:`, errUnlink); @@ -3594,7 +3746,8 @@ function transcribeAudio(filePath, callback) { id: requestId, path: filePath, callback: processCallback, // Use the wrapper callback - // retry tracking removed (was unused) + retries: 0, // Track retry attempts + maxRetries: 2 // Maximum number of retries }); // Try to process @@ -3917,7 +4070,8 @@ async function processMergedCallSegments( storagePath, audioPathForSplitting, tempPath, - finalPathIfLocal + finalPathIfLocal, + storageMode = STORAGE_MODE ) { logger.info(`Processing ${segmentTranscriptions.length} segments for merged call ID ${transcriptionId}`); @@ -3933,7 +4087,7 @@ async function processMergedCallSegments( .trim(); // Build combined transcription lines for Discord (all segments in one message) - const audioUrl = `http://${PUBLIC_DOMAIN}/audio/${transcriptionId}`; + const audioUrl = await getPublicAudioUrl(transcriptionId); const transcriptionLines = []; for (const segment of sortedSegments) { @@ -3983,7 +4137,7 @@ async function processMergedCallSegments( ); // Clean up temp file only if storage was S3 - if (STORAGE_MODE === 's3') { + if (storageMode === 's3') { setImmediate(() => { fs.unlink(tempPath, (errUnlink) => { if (errUnlink && errUnlink.code !== 'ENOENT') { @@ -4022,11 +4176,13 @@ async function handleNewTranscription( stop_time, tdma_slot, phase2_tdma, - color_code + color_code, + storageConfig = null ) { logger.info(`Starting handleNewTranscription for ID ${id}`); logger.info(`Transcription text length: ${transcriptionText.length} characters`); logger.info(`Talk Group: ${talkGroupID} - ${talkGroupName}`); + const resolvedStorageConfig = storageConfig || await getBotStorageConfig(); // Auto-queue calls after two-tone detection (if in two-tone mode) if (IS_TWO_TONE_MODE_ENABLED && lastTwoToneTime > 0 && lastDetectedToneGroup) { @@ -4074,9 +4230,7 @@ async function handleNewTranscription( logger.info(`Checking for two-tone in talk group ${talkGroupID} (ID: ${id})`); // Construct the proper audio path for tone detection - const audioPathForTones = STORAGE_MODE === 's3' ? - `https://${S3_ENDPOINT.replace('https://', '').replace('http://', '')}/${S3_BUCKET_NAME}/${audioFilePath}` : - path.join(__dirname, 'audio', audioFilePath); // Construct full local path + const audioPathForTones = getToneAudioPath(resolvedStorageConfig, audioFilePath); // Wait for tone detection to complete before continuing await new Promise((resolve) => { @@ -4287,12 +4441,12 @@ function sendAlertMessage( callback ) { // Look up the audio_id from the database for this transcription - db.get('SELECT id FROM audio_files WHERE transcription_id = ?', [audioID], (err, row) => { + db.get('SELECT id FROM audio_files WHERE transcription_id = ?', [audioID], async (err, row) => { // Use transcription ID as fallback if audio ID not found const actualAudioID = (err || !row) ? audioID : row.id; // Create a URL for the audio file - const audioUrl = `http://${PUBLIC_DOMAIN}/audio/${actualAudioID}`; + const audioUrl = await getPublicAudioUrl(actualAudioID); // Log the IDs for debugging logger.info(`Alert - Transcription ID: ${audioID}, Audio ID: ${actualAudioID}, URL: ${audioUrl}`); @@ -4527,7 +4681,7 @@ function sendTranscriptionMessage( } // Get or create the channel within the category - getOrCreateChannel(channelName, category.id, (channel) => { + getOrCreateChannel(channelName, category.id, async (channel) => { if (!channel) { logger.error('Failed to get or create channel.'); if (callback) callback(); // Ensure callback is called even on error @@ -4538,7 +4692,7 @@ function sendTranscriptionMessage( // Note: We use transcription ID (`audioID` parameter) for the URL now // as audio_files might get cleaned up. // The audio server route /audio/:id expects the transcription ID. - const audioUrl = `http://${PUBLIC_DOMAIN}/audio/${audioID}`; + const audioUrl = await getPublicAudioUrl(audioID); // Log the ID and URL for debugging logger.info(`Creating link for Transcription ID: ${audioID}, Audio URL: ${audioUrl}`); @@ -5058,26 +5212,32 @@ Focus on providing insightful analysis of each transmission. The "description" f Include no other text besides this JSON.`; // Call the AI provider with a timeout + const runtime = await getBotRuntimeConfig(); + const aiProvider = (runtime.settings.aiProvider || AI_PROVIDER || 'ollama').toLowerCase(); + const openaiApiKey = runtime.secrets.openaiApiKey || OPENAI_API_KEY || ''; + const openaiModel = runtime.settings.openaiModel || OPENAI_MODEL || 'gpt-4o-mini'; + const ollamaUrl = runtime.settings.ollamaUrl || OLLAMA_URL || 'http://localhost:11434'; + const ollamaModel = runtime.settings.ollamaModel || OLLAMA_MODEL || 'llama3.1:8b'; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); // 30 second timeout let resultText = ''; try { - if (AI_PROVIDER.toLowerCase() === 'openai') { - if (!OPENAI_API_KEY) { + if (aiProvider === 'openai') { + if (!openaiApiKey) { logger.error("[Bot] FATAL: AI_PROVIDER is set to openai, but OPENAI_API_KEY is not configured!"); throw new Error("OpenAI API key is not configured."); } - logger.info(`[Bot] Generating summary with OpenAI model: ${OPENAI_MODEL}`); + logger.info(`[Bot] Generating summary with OpenAI model: ${openaiModel}`); const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${OPENAI_API_KEY}` + 'Authorization': `Bearer ${openaiApiKey}` }, body: JSON.stringify({ - model: OPENAI_MODEL, + model: openaiModel, messages: [{ role: 'user', content: commonPrompt }], temperature: 0.3, response_format: { type: "json_object" } // Request JSON output @@ -5095,13 +5255,13 @@ Include no other text besides this JSON.`; } } else { // Default to Ollama - logger.info(`[Bot] Generating summary with Ollama model: ${OLLAMA_MODEL}`); + logger.info(`[Bot] Generating summary with Ollama model: ${ollamaModel}`); - const response = await fetch(`${OLLAMA_URL}/api/generate`, { + const response = await fetch(`${ollamaUrl}/api/generate`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ - model: OLLAMA_MODEL, + model: ollamaModel, prompt: commonPrompt, stream: false }), @@ -5360,7 +5520,7 @@ async function updateSummaryEmbed() { if (summary.highlights && summary.highlights.length > 0) { for (const highlight of summary.highlights) { // Get audio URL - const audioUrl = `http://${PUBLIC_DOMAIN}/audio/${highlight.id}`; + const audioUrl = await getPublicAudioUrl(highlight.id); // Fix timestamp display - use timestamp directly from database let timestampDisplay; @@ -5566,6 +5726,8 @@ function getOrCreateChannel(channelName, categoryId, callback) { // Voice channel and audio playback management const activeVoiceChannels = new Map(); +const audioFilesInUse = new Set(); + function playAudioForTalkGroup(talkGroupID, transcriptionId) { talkGroupID = talkGroupID.toString(); const talkGroupData = activeVoiceChannels.get(talkGroupID); @@ -5587,7 +5749,7 @@ function playAudioForTalkGroup(talkGroupID, transcriptionId) { } } -function processAudioQueue(talkGroupID) { +async function processAudioQueue(talkGroupID) { talkGroupID = talkGroupID.toString(); const talkGroupData = activeVoiceChannels.get(talkGroupID); if (!talkGroupData || !talkGroupData.player || !talkGroupData.queue) { @@ -5633,14 +5795,21 @@ function processAudioQueue(talkGroupID) { }); }; - if (STORAGE_MODE === 's3') { + const storageConfig = await getBotStorageConfig(); + if (storageConfig.mode === 's3') { + if (!isS3Ready(storageConfig)) { + logger.error(`S3 Mode: storage configuration incomplete for Discord playback (ID ${transcriptionId})`); + processAudioQueue(talkGroupID); + return; + } + const s3Client = createS3Client(storageConfig); db.get('SELECT audio_file_path FROM transcriptions WHERE id = ?', [transcriptionId], (err, row) => { if (err || !row || !row.audio_file_path) { logger.error(`S3 Mode: Could not find audio_file_path for transcription ID ${transcriptionId}`, err); processAudioQueue(talkGroupID); return; } - const s3Stream = s3.getObject({ Bucket: S3_BUCKET_NAME, Key: row.audio_file_path }).createReadStream(); + const s3Stream = s3Client.getObject({ Bucket: storageConfig.s3BucketName, Key: row.audio_file_path }).createReadStream(); s3Stream.on('error', s3Err => { logger.error(`Error streaming from S3 for Discord playback (ID ${transcriptionId}):`, s3Err); processAudioQueue(talkGroupID); @@ -5675,6 +5844,13 @@ function deleteAudioFile(transcriptionId) { }); } +function markAudioFileAsNotNeeded(transcriptionId) { + audioFilesInUse.delete(transcriptionId); + if (!audioFilesInUse.has(transcriptionId)) { + deleteAudioFile(transcriptionId); + } +} + async function handleListenLive(interaction, talkGroupID) { talkGroupID = talkGroupID.toString(); await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); @@ -5866,6 +6042,12 @@ function cleanupVoiceChannel(talkGroupID) { } } + if (queue && queue.length > 0) { + queue.forEach((transcriptionId) => { + markAudioFileAsNotNeeded(transcriptionId); + }); + } + if (voiceChannel) { voiceChannel .delete() @@ -5975,13 +6157,15 @@ client.once('ready', async () => { startSummaryScheduler(); // Start transcription process if needed - if (effectiveTranscriptionMode === 'local') { + const transcriptionConfig = await getBotTranscriptionConfig(); + if (transcriptionConfig.mode === 'local') { logger.info('Initializing local transcription process...'); startTranscriptionProcess(); } else { - logger.info(`Transcription mode set to '${effectiveTranscriptionMode}'. Local Python process will not be started.`); + logger.info(`Transcription mode set to '${transcriptionConfig.mode}'. Local Python process will not be started.`); } + isBootComplete = true; }); client.on('interactionCreate', async (interaction) => { @@ -6223,8 +6407,13 @@ client.on('interactionCreate', async (interaction) => { const userQuestion = interaction.fields.getTextInputValue('ai_question'); try { - // --- Read lookback from .env, default to 8 hours --- - const askAiLookbackHours = parseFloat(ASK_AI_LOOKBACK_HOURS) || 8; + const runtime = await getBotRuntimeConfig(); + const askAiLookbackHours = parseFloat(runtime.settings.askAiLookbackHours || ASK_AI_LOOKBACK_HOURS) || 8; + const aiProvider = (runtime.settings.aiProvider || AI_PROVIDER || 'ollama').toLowerCase(); + const openaiApiKey = runtime.secrets.openaiApiKey || OPENAI_API_KEY || ''; + const openaiModel = runtime.settings.openaiModel || OPENAI_MODEL || 'gpt-4o-mini'; + const ollamaUrl = runtime.settings.ollamaUrl || OLLAMA_URL || 'http://localhost:11434'; + const ollamaModel = runtime.settings.ollamaModel || OLLAMA_MODEL || 'llama3.1:8b'; const now = new Date(); const queryStartDate = new Date(now.getTime() - askAiLookbackHours * 60 * 60 * 1000); // Convert start date to Unix seconds for the query @@ -6325,20 +6514,20 @@ User Question: ${userQuestion} let aiResponseText = 'Error: Could not get response from AI.'; try { - if (AI_PROVIDER.toLowerCase() === 'openai') { - if (!OPENAI_API_KEY) { + if (aiProvider === 'openai') { + if (!openaiApiKey) { throw new Error("OpenAI API key is not configured."); } - logger.info(`[Bot] Answering question with OpenAI model: ${OPENAI_MODEL}`); + logger.info(`[Bot] Answering question with OpenAI model: ${openaiModel}`); const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${OPENAI_API_KEY}` + 'Authorization': `Bearer ${openaiApiKey}` }, body: JSON.stringify({ - model: OPENAI_MODEL, + model: openaiModel, messages: [{ role: 'user', content: commonPrompt }], temperature: 0.5, max_tokens: 500 @@ -6355,13 +6544,13 @@ User Question: ${userQuestion} } } else { // Default to Ollama - logger.info(`[Bot] Answering question with Ollama model: ${OLLAMA_MODEL}`); + logger.info(`[Bot] Answering question with Ollama model: ${ollamaModel}`); - const response = await fetch(`${OLLAMA_URL}/api/generate`, { + const response = await fetch(`${ollamaUrl}/api/generate`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ - model: OLLAMA_MODEL, + model: ollamaModel, prompt: commonPrompt, stream: false, options: { num_ctx: 35000 } @@ -6497,4 +6686,4 @@ process.on('SIGINT', () => { process.exit(0); }); }); -}); \ No newline at end of file +}); diff --git a/demo/sample-calls.json b/demo/sample-calls.json new file mode 100644 index 0000000..3dcc716 --- /dev/null +++ b/demo/sample-calls.json @@ -0,0 +1,35 @@ +[ + { + "id": 1, + "talk_group_id": "1001", + "timestamp": 1779033180, + "transcription": "Engine 12 responding to a medical call near Main Street and Oak Avenue.", + "audio_file_path": "", + "address": "Main Street and Oak Avenue", + "lat": 39.083997, + "lon": -77.152758, + "category": "Medical Call" + }, + { + "id": 2, + "talk_group_id": "2001", + "timestamp": 1779033360, + "transcription": "Units checking a vehicle collision near the northbound ramp.", + "audio_file_path": "", + "address": "Northbound ramp", + "lat": 39.099721, + "lon": -77.184516, + "category": "Vehicle Collision" + }, + { + "id": 3, + "talk_group_id": "3001", + "timestamp": 1779033540, + "transcription": "Police responding for a disturbance at the shopping center.", + "audio_file_path": "", + "address": "Shopping center", + "lat": 39.045753, + "lon": -77.118741, + "category": "Disturbance" + } +] diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..0251760 --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,21 @@ +# Copy to docker/.env before: docker compose --profile core up -d + +PROFILE_TAG=core +WEB_PORT=3000 +BOT_PORT=3306 + +TRANSCRIPTION_MODE=remote +FASTER_WHISPER_SERVER_URL=http://localhost:8000 + +AI_PROVIDER=ollama +OLLAMA_URL=http://ollama:11434 +OLLAMA_MODEL=llama3.1:8b + +PUBLIC_DOMAIN=localhost +WEBSERVER_PORT=3000 +BOT_PORT=3306 +ENABLE_AUTH=false + +# Geocoding (at least one required for full operation) +# Maps_API_KEY= +# LOCATIONIQ_API_KEY= diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..3ab5ed4 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,30 @@ +# Scanner Map — core image (Node + base Python, no ML stack) +FROM node:20-bookworm-slim AS core + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg python3 python3-pip python3-venv ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev + +COPY requirements-base.txt ./ +RUN pip3 install --no-cache-dir --break-system-packages -r requirements-base.txt \ + || pip3 install --no-cache-dir -r requirements-base.txt + +COPY . . + +RUN chmod +x docker/entrypoint.sh + +ENV NODE_ENV=production \ + WEBSERVER_PORT=3000 \ + BOT_PORT=3306 + +EXPOSE 3000 3306 + +VOLUME ["/app/data", "/app/audio", "/app/models"] + +ENTRYPOINT ["/app/docker/entrypoint.sh"] +CMD ["node", "bot.js"] diff --git a/docker/Dockerfile.qwen b/docker/Dockerfile.qwen new file mode 100644 index 0000000..5db4375 --- /dev/null +++ b/docker/Dockerfile.qwen @@ -0,0 +1,27 @@ +FROM node:20-bookworm-slim AS qwen + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg python3 python3-pip python3-venv ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev + +COPY requirements-base.txt requirements-local-qwen.txt ./ +RUN pip3 install --no-cache-dir --break-system-packages -r requirements-local-qwen.txt \ + || pip3 install --no-cache-dir -r requirements-local-qwen.txt + +COPY . . +RUN chmod +x docker/entrypoint.sh + +ENV NODE_ENV=production \ + TRANSCRIPTION_MODE=local \ + LOCAL_TRANSCRIPTION_BACKEND=qwen3-asr \ + QWEN_ASR_MODEL=Qwen/Qwen3-ASR-0.6B + +EXPOSE 3000 3306 +VOLUME ["/app/data", "/app/audio", "/app/models"] +ENTRYPOINT ["/app/docker/entrypoint.sh"] +CMD ["node", "bot.js"] diff --git a/docker/Dockerfile.tone b/docker/Dockerfile.tone new file mode 100644 index 0000000..faad140 --- /dev/null +++ b/docker/Dockerfile.tone @@ -0,0 +1,26 @@ +# Scanner Map — tone detection image (extends core) +FROM node:20-bookworm-slim AS tone + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg python3 python3-pip python3-venv ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev + +COPY requirements-base.txt requirements-tone.txt ./ +RUN pip3 install --no-cache-dir --break-system-packages -r requirements-tone.txt \ + || pip3 install --no-cache-dir -r requirements-tone.txt + +COPY . . +RUN chmod +x docker/entrypoint.sh + +ENV NODE_ENV=production \ + ENABLE_TONE_DETECTION=true + +EXPOSE 3000 3306 +VOLUME ["/app/data", "/app/audio", "/app/models"] +ENTRYPOINT ["/app/docker/entrypoint.sh"] +CMD ["node", "bot.js"] diff --git a/docker/Dockerfile.whisper b/docker/Dockerfile.whisper new file mode 100644 index 0000000..1562481 --- /dev/null +++ b/docker/Dockerfile.whisper @@ -0,0 +1,26 @@ +FROM node:20-bookworm-slim AS whisper + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg python3 python3-pip python3-venv ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev + +COPY requirements-base.txt requirements-local-whisper.txt ./ +RUN pip3 install --no-cache-dir --break-system-packages -r requirements-local-whisper.txt \ + || pip3 install --no-cache-dir -r requirements-local-whisper.txt + +COPY . . +RUN chmod +x docker/entrypoint.sh + +ENV NODE_ENV=production \ + TRANSCRIPTION_MODE=local \ + LOCAL_TRANSCRIPTION_BACKEND=faster-whisper + +EXPOSE 3000 3306 +VOLUME ["/app/data", "/app/audio", "/app/models"] +ENTRYPOINT ["/app/docker/entrypoint.sh"] +CMD ["node", "bot.js"] diff --git a/docker/docker-compose.gpu.yml b/docker/docker-compose.gpu.yml new file mode 100644 index 0000000..9f0ad47 --- /dev/null +++ b/docker/docker-compose.gpu.yml @@ -0,0 +1,25 @@ +# GPU override: docker compose -f docker/docker-compose.yml -f docker/docker-compose.gpu.yml --profile local-whisper up -d + +services: + scanner-map-whisper: + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + environment: + TRANSCRIPTION_DEVICE: cuda + + scanner-map-qwen: + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + environment: + TRANSCRIPTION_DEVICE: cuda + QWEN_ASR_BACKEND: transformers diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..4d2ac96 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,42 @@ +name: scanner-map + +services: + scanner-map: + image: ${SCANNER_MAP_IMAGE:-ghcr.io/dadud/scanner-map:${PROFILE_TAG:-core}} + build: + context: .. + dockerfile: docker/Dockerfile + args: + PROFILE: ${PROFILE_TAG:-core} + profiles: [core, local-whisper, local-qwen, tone-detect, ollama, full] + env_file: + - path: .env + required: false + environment: + TRANSCRIPTION_MODE: ${TRANSCRIPTION_MODE:-remote} + LOCAL_TRANSCRIPTION_BACKEND: ${LOCAL_TRANSCRIPTION_BACKEND:-faster-whisper} + WEBSERVER_PORT: ${WEBSERVER_PORT:-3000} + BOT_PORT: ${BOT_PORT:-3306} + ports: + - "${WEB_PORT:-3000}:${WEBSERVER_PORT:-3000}" + - "${BOT_PORT:-3306}:${BOT_PORT:-3306}" + volumes: + - scanner-appdata:/app/data + - scanner-audio:/app/audio + - scanner-models:/app/models + restart: unless-stopped + + ollama: + image: ollama/ollama:latest + profiles: [ollama, full] + volumes: + - ollama-data:/root/.ollama + ports: + - "11434:11434" + restart: unless-stopped + +volumes: + scanner-appdata: + scanner-audio: + scanner-models: + ollama-data: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..e0e7ede --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +cd /app + +mkdir -p /app/data /app/audio /app/models /app/logs + +if [ ! -f /app/data/.env-linked ] && [ -f /app/.env ]; then + : # use mounted or baked .env +fi + +export WHISPER_MODEL="${WHISPER_MODEL:-large-v3}" +export TRANSCRIPTION_DEVICE="${TRANSCRIPTION_DEVICE:-cpu}" +export LOCAL_TRANSCRIPTION_BACKEND="${LOCAL_TRANSCRIPTION_BACKEND:-faster-whisper}" + +exec "$@" diff --git a/docs/github/BRANCH_PROTECTION.md b/docs/github/BRANCH_PROTECTION.md new file mode 100644 index 0000000..2bdda08 --- /dev/null +++ b/docs/github/BRANCH_PROTECTION.md @@ -0,0 +1,58 @@ +# Upstream branch protection (poisonednumber/Scanner-map) + +Configure these rules once in GitHub under **Settings → Branches → Branch protection rules** for [poisonednumber/Scanner-map](https://github.com/poisonednumber/Scanner-map). + +These settings cannot be applied from this repository alone; a repo admin must configure them in the GitHub UI or via the GitHub API with appropriate credentials. + +## `main` (stable) + +| Setting | Value | +|---------|-------| +| Require pull request before merging | **Yes** | +| Allow direct pushes | **No** (except bypass list empty or owner-only emergency) | +| Require status checks | **Yes** — require CI workflow | +| Restrict who can push | **Nobody** (PR merges only) | +| Restrict who can merge | Repository owner / release managers | +| Block force pushes | **Yes** | +| Allow deletions | **No** | + +**Intent:** `main` only moves via reviewed PR from `beta` at release time. + +## `beta` (public integration) + +| Setting | Value | +|---------|-------| +| Require pull request before merging | **No** (owner may push directly) | +| Allow force pushes | **Optional** — allow `--force-with-lease` if you accept reset promotions | +| Require status checks | **Recommended** — CI + Docker `:beta` publish | +| Restrict who can push | Repository owner | + +**Intent:** `beta` receives `git push upstream dev:beta` when integration is ready. + +## CI expectations + +| Event | Action | +|-------|--------| +| PR → `origin/dev` | `npm ci`, `npm test`, `npm run check:syntax` | +| Push to `upstream/beta` | Same CI + publish `ghcr.io/poisonednumber/scanner-map:beta` | +| Tag `v*` on `upstream/main` | Publish `:core`, `:whisper`, `:qwen`, `:latest` | + +## Applying via GitHub CLI (optional) + +Replace `OWNER` with `poisonednumber` if using a personal access token with admin scope: + +```bash +# Example: require PR on main (adjust IDs for your org) +gh api repos/poisonednumber/Scanner-map/branches/main/protection \ + --method PUT \ + -f required_status_checks='{"strict":true,"contexts":["smoke"]}' \ + -f enforce_admins=true \ + -f required_pull_request_reviews='{"required_approving_review_count":0}' \ + -F restrictions=null +``` + +Review the [GitHub branch protection API](https://docs.github.com/en/rest/branches/branch-protection) before applying. `beta` typically needs lighter or no protection so direct pushes work. + +## Fork default branch + +On **Dadud/Scanner-map**, set the default branch to **`dev`** under **Settings → General → Default branch** so new clones and PRs target integration correctly. diff --git a/docs/modernization-roadmap.md b/docs/modernization-roadmap.md new file mode 100644 index 0000000..b5d0dea --- /dev/null +++ b/docs/modernization-roadmap.md @@ -0,0 +1,58 @@ +# Scanner Map Modernization Roadmap + +This roadmap breaks the larger architecture work into reviewable PRs. Each phase should preserve current behavior while creating room for deeper changes. + +## Phase 1: Foundations + +- Add shared config parsing and validation. +- Add a migration module that can replace scattered table creation over time. +- Add ingestion normalization helpers for SDRTrunk, TrunkRecorder, and rdio-scanner compatible uploads. +- Add a local demo data generator. +- Add smoke tests and CI. +- Add role and permission primitives that can back future RBAC. + +## Phase 2: Runtime Integration + +- Replace scattered `process.env` reads in `bot.js`, `webserver.js`, and `geocoding.js` with the shared config module. +- Move database initialization to the migration runner. +- Route upload handling through the ingestion normalization helpers. +- Keep the old endpoint behavior intact while shrinking request-handler complexity. + +## Phase 3: Reliable Processing Queue + +- Persist call processing jobs in SQLite or a dedicated queue backend. +- Track job state: pending, processing, failed, complete, and retryable. +- Retry transcription, geocoding, categorization, Discord publishing, and storage steps independently. +- Add admin visibility for queue depth, failed jobs, and processing latency. + +## Phase 4: Local Demo And Developer Mode + +- Add a demo server mode that serves sample calls without SDRTrunk, TrunkRecorder, Discord, geocoding keys, or audio hardware. +- Add sample talkgroups, categories, and map markers. +- Make frontend work possible with one command. + +## Phase 5: Frontend Modules + +- Split `public/app.js` into modules for map setup, markers, audio playback, live feed, auth, talkgroup modal, purge modal, and geocoding search. +- Gate verbose browser logging behind a debug flag. +- Add targeted browser smoke tests once the local demo mode exists. + +## Phase 6: Data Model And Retention + +- Add schema versioning and repeatable migrations. +- Add indexes for common call history, talkgroup, timestamp, and category queries. +- Add configurable retention rules for calls and audio. +- Add database maintenance docs for long-running deployments. + +## Phase 7: Roles And Permissions + +- Add a `role` column for users and migrate existing admin users. +- Replace ad hoc admin checks with permission checks. +- Introduce viewer, editor, moderator, and admin roles. +- Add UI controls only when the current user has the matching permission. + +## Phase 8: Adapter Architecture + +- Formalize ingestion adapters for SDRTrunk, TrunkRecorder, and rdio-scanner compatible uploads. +- Add adapter tests with real-world fixture payloads. +- Make future upload sources additive instead of route-handler rewrites. diff --git a/geocoding.js b/geocoding.js index 4dbe88d..92f0434 100644 --- a/geocoding.js +++ b/geocoding.js @@ -156,10 +156,44 @@ logger.info(`Loaded ${Object.keys(TALK_GROUPS).length} talk groups from environm // Keep the TARGET_CITIES list for reference (remains the same) const TARGET_CITIES = process.env.TARGET_CITIES_LIST ? process.env.TARGET_CITIES_LIST.split(',').map(city => city.trim()) : []; -// Function to load talk groups from env vars (remains the same) +// Function to load talk groups from DB locations, env overrides, and county fallback function loadTalkGroups(db) { - logger.info(`Using talk groups from environment variables. Found ${Object.keys(TALK_GROUPS).length} talk groups`); - return Promise.resolve(TALK_GROUPS); + return new Promise((resolve, reject) => { + const merged = { ...TALK_GROUPS }; + + db.all('SELECT talk_group_id, location_hint FROM talk_group_locations', [], (locationErr, locationRows) => { + if (locationErr) { + reject(locationErr); + return; + } + + for (const row of locationRows || []) { + if (!merged[row.talk_group_id]) { + merged[row.talk_group_id] = row.location_hint; + } + } + + db.all( + 'SELECT id, county FROM talk_groups WHERE county IS NOT NULL AND county != ""', + [], + (talkgroupErr, talkgroupRows) => { + if (talkgroupErr) { + reject(talkgroupErr); + return; + } + + for (const row of talkgroupRows || []) { + if (!merged[row.id]) { + merged[row.id] = row.county; + } + } + + logger.info(`Loaded ${Object.keys(merged).length} talk groups for geocoding`); + resolve(merged); + } + ); + }); + }); } /** diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9422d52 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4467 @@ +{ + "name": "scanner-map", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scanner-map", + "version": "1.0.0", + "dependencies": { + "@discordjs/voice": "^0.18.0", + "@snazzah/davey": "^0.1.2", + "aws-sdk": "^2.1692.0", + "bcrypt": "^5.1.1", + "busboy": "^1.6.0", + "csv-parser": "^3.2.0", + "discord.js": "^14.20.0", + "dotenv": "^16.6.1", + "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "form-data": "^4.0.4", + "moment-timezone": "^0.6.0", + "node-cache": "^5.1.2", + "node-fetch": "^2.7.0", + "openai": "^4.104.0", + "opusscript": "^0.0.8", + "prism-media": "^1.3.5", + "public-ip": "^8.0.0", + "socket.io": "^4.8.1", + "sqlite3": "^5.1.7", + "uuid": "^11.1.0", + "winston": "^3.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", + "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/formatters/node_modules/discord-api-types": { + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/rest": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", + "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.2.0", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.5", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.40", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util/node_modules/discord-api-types": { + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/voice": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.18.0.tgz", + "integrity": "sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==", + "license": "Apache-2.0", + "dependencies": { + "@types/ws": "^8.5.12", + "discord-api-types": "^0.37.103", + "prism-media": "^1.3.5", + "tslib": "^2.6.3", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "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/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@snazzah/davey": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey/-/davey-0.1.11.tgz", + "integrity": "sha512-oBN+msHzPnm1M5DDx3wVD7iBwpNXFUtkh2MrAbUJu0OhKjliLChi28hq++mu1+qdMpAVQO5JKAvQQxYVbyneiw==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "funding": { + "url": "https://github.com/sponsors/Snazzah" + }, + "optionalDependencies": { + "@snazzah/davey-android-arm-eabi": "0.1.11", + "@snazzah/davey-android-arm64": "0.1.11", + "@snazzah/davey-darwin-arm64": "0.1.11", + "@snazzah/davey-darwin-x64": "0.1.11", + "@snazzah/davey-freebsd-x64": "0.1.11", + "@snazzah/davey-linux-arm-gnueabihf": "0.1.11", + "@snazzah/davey-linux-arm64-gnu": "0.1.11", + "@snazzah/davey-linux-arm64-musl": "0.1.11", + "@snazzah/davey-linux-x64-gnu": "0.1.11", + "@snazzah/davey-linux-x64-musl": "0.1.11", + "@snazzah/davey-wasm32-wasi": "0.1.11", + "@snazzah/davey-win32-arm64-msvc": "0.1.11", + "@snazzah/davey-win32-ia32-msvc": "0.1.11", + "@snazzah/davey-win32-x64-msvc": "0.1.11" + } + }, + "node_modules/@snazzah/davey-android-arm-eabi": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-android-arm-eabi/-/davey-android-arm-eabi-0.1.11.tgz", + "integrity": "sha512-T1RYbNYKN6tLOcGIDKJd8OI6FBSEemwL7DOYdTMmhqfhhMr3YVN8WOhfoxGg63OcnpTN2e2c5tdY2bAx25RmQQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-android-arm64": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-android-arm64/-/davey-android-arm64-0.1.11.tgz", + "integrity": "sha512-ksJn/x2VU8h6w9eku1HT96ugSRZ7lKVkKNKbFleaFN+U99DJaPM+gMu2YvnFU4V54HR06ZBnRihnVG6VLXQpDw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-darwin-arm64": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-darwin-arm64/-/davey-darwin-arm64-0.1.11.tgz", + "integrity": "sha512-E1d7PbaaVMO3Lj9EiAPqOVbuV0xg5+PsHzHH097DDXiD1+zUDXvJaTnUWsnm5z50pJniHpi4GtaYmk+ieB/guA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-darwin-x64": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-darwin-x64/-/davey-darwin-x64-0.1.11.tgz", + "integrity": "sha512-Tl4TI/LTmgJZepgbgVMYDi8RqlAkPtPg1OEBPl7a9Tn3AwR36Vs6lyIT1cs/lGy/ds/+B+mKI4rPObN1cyILTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-freebsd-x64": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-freebsd-x64/-/davey-freebsd-x64-0.1.11.tgz", + "integrity": "sha512-T8Iw9FXkuI1T+YBAFzh9v/TXf9IOTOSqnd/BFpTRTrlW72PR2lhIidzSmg027VxO7r5pX47iFwiOkb9I/NU/EA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-linux-arm-gnueabihf": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-linux-arm-gnueabihf/-/davey-linux-arm-gnueabihf-0.1.11.tgz", + "integrity": "sha512-1Txj+8pqA8uq/OGtaUaBFWAPnNMQzFgIywj0iA7EI4xZl+mab48/pv+YZ1pNb/suC6ynsW44oB9efiXSdcUAgA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-linux-arm64-gnu": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-linux-arm64-gnu/-/davey-linux-arm64-gnu-0.1.11.tgz", + "integrity": "sha512-ERzF5nM/IYW1BcN3wLXpEwBCGLFf0kGJUVhaV6yfiInz0tkU8UmvrrgpaMaACfMjIhfWdq5CcX+aTkXo/saNcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-linux-arm64-musl": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-linux-arm64-musl/-/davey-linux-arm64-musl-0.1.11.tgz", + "integrity": "sha512-e6pX6Hiabtz99q+H/YHNkm9JVlpqN8HGh0qPib8G2+UY4/SSH8WvqWipk3v581dMy2oyCHt7MOoY1aU1P1N/xA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-linux-x64-gnu": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-gnu/-/davey-linux-x64-gnu-0.1.11.tgz", + "integrity": "sha512-TW5bSoqChOJMbvsDb4wAATYrxmAXuNnse7wFNVSAJUaZKSeRfZbu3UAiPWSNn7GwLwSfU6hg322KZUn8IWCuvg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-linux-x64-musl": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-musl/-/davey-linux-x64-musl-0.1.11.tgz", + "integrity": "sha512-5j6Pmc+Wzv5lSxVP6quA7teYRJXibkZqQyYGfTDnTsUOO5dPpcojpqlXlkhyvsA1OAQTj4uxbOCciN3cVWwzug==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-wasm32-wasi": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-wasm32-wasi/-/davey-wasm32-wasi-0.1.11.tgz", + "integrity": "sha512-rKOwZ/0J8lp+4VEyOdMDBRP9KR+PksZpa9V1Qn0veMzy4FqTVKthkxwGqewheFe0SFg9fdvt798l/PBFrfDeZw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@snazzah/davey-win32-arm64-msvc": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-win32-arm64-msvc/-/davey-win32-arm64-msvc-0.1.11.tgz", + "integrity": "sha512-5fptJU4tX901m3mj0SHiBljMrPT4ZEsynbBhR7bK1yn9TY1jjyhN8EFi7QF5IWtUEni+0mia2BCMHZ5ZkmFZqQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-win32-ia32-msvc": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-win32-ia32-msvc/-/davey-win32-ia32-msvc-0.1.11.tgz", + "integrity": "sha512-ualexn8SeLsiMHhWfzVrzRcjHgcBapg++FPaVgJJxoh2S/jCRiklXOu3luqIZdJdNKvhe2V9SwO/cImPeIIBKw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@snazzah/davey-win32-x64-msvc": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@snazzah/davey-win32-x64-msvc/-/davey-win32-x64-msvc-0.1.11.tgz", + "integrity": "sha512-muNhc8UKXtknzsH/w4AIkbPR2I8BuvApn0pDXar0IEvY8PCjqU/M8MPbOOEYwQVvQRMwVTgExtxzrkBPSXB4nA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "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/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "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" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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" + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "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", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1693.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz", + "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", + "deprecated": "The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "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/bl/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/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "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/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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "license": "MIT", + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "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" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csv-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.1.tgz", + "integrity": "sha512-v8RPMSglouR9od735SnwSxLBbCJqEPSbgm1R5qfr8yIiMUCEFjox56kRZid0SvgHJEkxeIEu3+a9QS3YRh7CuA==", + "license": "MIT", + "bin": { + "csv-parser": "bin/csv-parser" + }, + "engines": { + "node": ">= 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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.120", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz", + "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.26.4", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", + "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.14.1", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.1", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/discord.js/node_modules/discord-api-types": { + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dns-socket": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/dns-socket/-/dns-socket-4.2.2.tgz", + "integrity": "sha512-BDeBd8najI4/lS00HSKpdFia+OvUMytaVjfzR9n5Lq8MlZRSvtbI+uLtx1+XmQFls5wFU9dssccTmQQ6nfpjdg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.4" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/engine.io": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.7.tgz", + "integrity": "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/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/engine.io/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "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.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "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" + }, + "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/function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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" + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "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.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "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.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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" + }, + "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", + "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.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "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", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ip": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz", + "integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==", + "license": "MIT", + "dependencies": { + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "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/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/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/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/magic-bytes.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", + "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "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.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "license": "BlueOak-1.0.0", + "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/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.2.tgz", + "integrity": "sha512-lDsQv8FoGdBUdf0+TjGsq2orxKuXdwFlQ6Zw6TX3xIcTwTfEpCLyKqvEauvCHJ8iu3KBV8+uPhlv70YsNGdUBQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "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.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "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/node-gyp/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/node-gyp/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/node-gyp/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/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.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/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/opusscript": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz", + "integrity": "sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==", + "license": "MIT" + }, + "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", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prism-media": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", + "integrity": "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==", + "license": "Apache-2.0", + "peerDependencies": { + "@discordjs/opus": ">=0.8.0 <1.0.0", + "ffmpeg-static": "^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0", + "node-opus": "^0.3.3", + "opusscript": "^0.0.8" + }, + "peerDependenciesMeta": { + "@discordjs/opus": { + "optional": true + }, + "ffmpeg-static": { + "optional": true + }, + "node-opus": { + "optional": true + }, + "opusscript": { + "optional": true + } + } + }, + "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/public-ip": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/public-ip/-/public-ip-8.0.0.tgz", + "integrity": "sha512-XzVyz98rNQiTRciAC+I4w45fWWxM9KKedDGNtH4unPwBcWo2Y9n7kgPXqlTiWqKN0EFlIIU1i8yrWOy9mxgZ8g==", + "license": "MIT", + "dependencies": { + "dns-socket": "^4.2.2", + "is-ip": "^5.0.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/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", + "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/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/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.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/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/socket.io-adapter/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/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/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/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/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.1.1", + "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.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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/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/sqlite3/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/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/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "license": "MIT", + "dependencies": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "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.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-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/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "license": "MIT", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "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/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "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/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "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": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/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/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "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/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "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..ad5465f --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "scanner-map", + "version": "1.0.0", + "private": true, + "description": "Real-time mapping system for radio calls with transcription, geocoding, and Discord integration.", + "main": "bot.js", + "scripts": { + "start": "node bot.js", + "web": "node webserver.js", + "setup": "node scripts/setup.js", + "doctor": "node scripts/doctor.js", + "import-talkgroups": "node import_csv.js", + "check:config": "node scripts/check-config.js", + "check:syntax": "node --check bot.js && node --check webserver.js && node --check geocoding.js && node --check import_csv.js && node --check public/app.js && node --check public/setup.js && node --check public/js/admin-settings.js && node --check src/radio/talkgroupImport.js && node --check src/radio/importState.js && node --check src/auth/adminAuth.js && node --check scripts/setup.js && node --check scripts/doctor.js", + "demo:data": "node scripts/generate-demo-data.js", + "test": "node --test test/*.test.js" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@discordjs/voice": "^0.18.0", + "@snazzah/davey": "^0.1.2", + "aws-sdk": "^2.1692.0", + "bcrypt": "^5.1.1", + "busboy": "^1.6.0", + "csv-parser": "^3.2.0", + "discord.js": "^14.20.0", + "dotenv": "^16.6.1", + "express": "^4.21.2", + "express-rate-limit": "^7.5.0", + "form-data": "^4.0.4", + "moment-timezone": "^0.6.0", + "node-cache": "^5.1.2", + "node-fetch": "^2.7.0", + "openai": "^4.104.0", + "opusscript": "^0.0.8", + "prism-media": "^1.3.5", + "public-ip": "^8.0.0", + "socket.io": "^4.8.1", + "sqlite3": "^5.1.7", + "uuid": "^11.1.0", + "winston": "^3.18.3" + } +} diff --git a/public/app.js b/public/app.js index 11021d5..ae21256 100644 --- a/public/app.js +++ b/public/app.js @@ -617,7 +617,7 @@ function updateCategoryCounts() { categoryItem.className = 'category-item'; categoryItem.dataset.category = catInfo.name; categoryItem.innerHTML = ` -
${catInfo.name}
+
${escapeHtml(catInfo.name)}
${catInfo.count}
`; @@ -1464,6 +1464,23 @@ function createLocationIQDropdown(inputElement, scope) { // Search LocationIQ API async function searchLocationIQ(query, dropdown, inputElement, scope) { try { + if (appConfig.geocoding.useProxy) { + const data = await safeFetchJson(`/api/geocode/autocomplete?q=${encodeURIComponent(query)}`); + const results = data.results || []; + dropdown.innerHTML = ''; + results.forEach((item) => { + const el = document.createElement('div'); + el.className = 'locationiq-item'; + el.textContent = item.label; + el.dataset.lat = item.lat || ''; + el.dataset.lon = item.lon || ''; + el.addEventListener('click', () => selectLocationIQItem(el, inputElement, dropdown, scope)); + dropdown.appendChild(el); + }); + dropdown.style.display = results.length ? 'block' : 'none'; + return; + } + // Get current map center for dynamic bias const mapCenter = map.getCenter(); const biasLat = mapCenter.lat; @@ -1719,8 +1736,8 @@ async function startAddressSearch(callId, originalMarker, modal) { let autocompleteDropdown = null; // Check which providers are available - const googleAvailable = appConfig.geocoding.googleApiKey && typeof google !== 'undefined' && google.maps && google.maps.places; - const locationiqAvailable = appConfig.geocoding.locationiqApiKey; + const googleAvailable = !appConfig.geocoding.useProxy && appConfig.geocoding.googleApiKey && typeof google !== 'undefined' && google.maps && google.maps.places; + const locationiqAvailable = appConfig.geocoding.locationiqAvailable || appConfig.geocoding.locationiqApiKey || appConfig.geocoding.useProxy; if (googleAvailable) { console.log('[Address Search] Using Google Places Autocomplete'); @@ -1798,6 +1815,15 @@ async function startAddressSearch(callId, originalMarker, modal) { } function getOriginalAddress(lat, lng) { + if (appConfig.geocoding.useProxy) { + return safeFetchJson(`/api/geocode/reverse?lat=${lat}&lon=${lng}`) + .then(data => { + if (data.results && data.results[0]) return data.results[0].formatted_address; + if (data.display_name) return data.display_name; + return `Unknown (${lat}, ${lng})`; + }) + .catch(() => `Unknown (${lat}, ${lng})`); + } // Try Google first if available, then LocationIQ as fallback if (appConfig.geocoding.googleApiKey) { return fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${appConfig.geocoding.googleApiKey}`) @@ -1823,6 +1849,11 @@ async function startAddressSearch(callId, originalMarker, modal) { } function getLocationIQReverseGeocode(lat, lng) { + if (appConfig.geocoding.useProxy) { + return safeFetchJson(`/api/geocode/reverse?lat=${lat}&lon=${lng}`) + .then(data => data.display_name || (data.results && data.results[0] && data.results[0].formatted_address) || `Unknown (${lat}, ${lng})`) + .catch(() => `Unknown (${lat}, ${lng})`); + } const params = new URLSearchParams({ key: appConfig.geocoding.locationiqApiKey, lat: lat, @@ -2528,6 +2559,13 @@ function clearMarkers() { console.log(`[DEBUG] clearMarkers started`); console.log(`[DEBUG] Current markers count: ${Object.keys(markers).length}`); console.log(`[DEBUG] Current allMarkers count: ${Object.keys(allMarkers).length}`); + + Object.keys(wavesurfers).forEach(callId => { + if (wavesurfers[callId]) { + try { wavesurfers[callId].destroy(); } catch (e) { /* ignore */ } + delete wavesurfers[callId]; + } + }); // First, remove all pulse markers Object.keys(markers).forEach(callId => { diff --git a/public/config.js b/public/config.js index 2cb57f7..c144bbb 100644 --- a/public/config.js +++ b/public/config.js @@ -168,13 +168,21 @@ async function fetchGeocodingConfig() { const data = await response.json(); if (data.google.available) { - config.geocoding.googleApiKey = data.google.apiKey; - console.log('[Config] Google Places API available'); + config.geocoding.googleAvailable = true; + config.geocoding.useProxy = !!data.google.useProxy; + if (!data.google.useProxy && data.google.apiKey) { + config.geocoding.googleApiKey = data.google.apiKey; + } + console.log('[Config] Google geocoding available' + (data.google.useProxy ? ' (server proxy)' : '')); } if (data.locationiq.available) { - config.geocoding.locationiqApiKey = data.locationiq.apiKey; - console.log('[Config] LocationIQ API available'); + config.geocoding.locationiqAvailable = true; + config.geocoding.useProxy = config.geocoding.useProxy || !!data.locationiq.useProxy; + if (!data.locationiq.useProxy && data.locationiq.apiKey) { + config.geocoding.locationiqApiKey = data.locationiq.apiKey; + } + console.log('[Config] LocationIQ geocoding available' + (data.locationiq.useProxy ? ' (server proxy)' : '')); } // Log available providers diff --git a/public/css/console.css b/public/css/console.css new file mode 100644 index 0000000..0e4fc28 --- /dev/null +++ b/public/css/console.css @@ -0,0 +1,401 @@ +@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap'); + +:root { + --primary-color: #00ff00; + --background-color: #000000; + --text-color: #00ff00; + --border-color: #00ff00; + --hover-color: #003300; + --panel-bg: rgba(0, 30, 0, 0.85); + --muted-text: rgba(0, 255, 0, 0.55); +} + +* { + box-sizing: border-box; +} + +body.console-page { + margin: 0; + min-height: 100vh; + font-family: 'Share Tech Mono', monospace; + color: var(--text-color); + background: var(--background-color); + overflow-x: hidden; +} + +.console-shell { + max-width: 1180px; + margin: 0 auto; + padding: 24px 16px 48px; +} + +.console-header { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; + margin-bottom: 20px; + padding: 16px; + background: rgba(0, 0, 0, 0.7); + border: 1px solid var(--border-color); + border-radius: 4px; + box-shadow: 0 0 10px rgba(0, 255, 0, 0.35); +} + +.console-header h1 { + margin: 0 0 8px; + font-size: 28px; + color: var(--primary-color); +} + +.console-header p { + margin: 0; + color: var(--muted-text); + max-width: 680px; +} + +.console-layout { + display: grid; + grid-template-columns: 220px 1fr; + gap: 16px; +} + +.console-nav, +.console-panel { + background: var(--panel-bg); + border: 1px solid rgba(0, 255, 0, 0.25); + border-radius: 6px; + box-shadow: 0 0 12px rgba(0, 255, 0, 0.15); +} + +.console-nav { + padding: 8px; + height: fit-content; +} + +.console-tab, +.step-button { + width: 100%; + border: 1px solid rgba(0, 255, 0, 0.25); + border-radius: 4px; + background: rgba(0, 50, 0, 0.35); + color: var(--text-color); + cursor: pointer; + font-family: inherit; + font-size: 13px; + padding: 10px 12px; + margin-bottom: 6px; + text-align: left; + transition: all 0.2s; +} + +.console-tab:hover, +.step-button:hover, +.console-btn:hover { + background: rgba(0, 100, 0, 0.45); + border-color: var(--primary-color); + box-shadow: 0 0 8px rgba(0, 255, 0, 0.25); +} + +.console-tab.active, +.step-button.active { + background: rgba(0, 255, 0, 0.15); + border-color: var(--primary-color); +} + +.console-panel { + padding: 20px; +} + +.section { + display: none; +} + +.section.active { + display: block; +} + +.section h2 { + margin: 0 0 16px; + color: var(--primary-color); + font-size: 18px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.settings-group { + background: rgba(0, 20, 0, 0.5); + border: 1px solid rgba(0, 255, 0, 0.15); + border-radius: 6px; + padding: 14px; + margin-bottom: 14px; +} + +.settings-group-title { + font-size: 13px; + font-weight: bold; + color: var(--primary-color); + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 12px; +} + +.field label { + display: block; + font-size: 12px; + margin-bottom: 6px; + color: var(--muted-text); +} + +.field input, +.field select, +.settings-input, +.settings-select { + width: 100%; + padding: 8px 10px; + background: var(--background-color); + color: var(--text-color); + border: 1px solid rgba(0, 255, 0, 0.35); + border-radius: 4px; + font-family: inherit; + font-size: 13px; +} + +.field-hint { + font-size: 11px; + color: var(--muted-text); + margin-top: 4px; +} + +.panel-hidden { + display: none !important; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + margin-top: 16px; +} + +.console-btn, +.primary, +.secondary { + padding: 8px 16px; + background: var(--background-color); + color: var(--text-color); + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: inherit; + cursor: pointer; + transition: all 0.2s; +} + +.console-btn.primary, +.primary { + background: rgba(0, 255, 0, 0.12); +} + +.console-link, +.status-pill { + display: inline-block; + padding: 8px 14px; + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-color); + text-decoration: none; + white-space: nowrap; +} + +.badge { + display: inline-block; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + border: 1px solid rgba(0, 255, 0, 0.35); +} + +.badge-warn { + color: #ffff99; + border-color: #ffff99; +} + +.badge-ok { + color: var(--primary-color); +} + +.check-list { + list-style: none; + padding: 0; + margin: 12px 0 0; +} + +.check-item { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid rgba(0, 255, 0, 0.1); + font-size: 13px; +} + +.check-item:last-child { + border-bottom: none; +} + +.check-status-pass { color: var(--primary-color); } +.check-status-warn { color: #ffff99; } +.check-status-fail { color: #ff6666; } + +.jobs-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; + margin-top: 12px; +} + +.jobs-table th, +.jobs-table td { + border: 1px solid rgba(0, 255, 0, 0.2); + padding: 8px; + text-align: left; +} + +.jobs-table th { + color: var(--primary-color); +} + +#save-result, +.save-result { + color: var(--muted-text); + font-size: 12px; +} + +@media (max-width: 768px) { + .console-layout { + grid-template-columns: 1fr; + } +} + +.field-source { + display: block; + margin-top: 4px; + font-size: 11px; + color: var(--muted-text); +} + +.field-source.source-env { + color: #66ccff; +} + +.field-source.source-sqlite { + color: #00ff88; +} + +.field-source.source-default { + color: var(--muted-text); +} + +.field-source.source-missing { + color: #ff6666; +} + +.location-row { + display: grid; + grid-template-columns: 1.4fr 1.2fr auto; + gap: 12px; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid rgba(0, 255, 0, 0.08); +} + +.location-input { + width: 100%; +} + +.settings-textarea { + width: 100%; + min-height: 180px; + padding: 10px 12px; + background: #000; + color: #00ff00; + border: 1px solid rgba(0, 255, 0, 0.4); + border-radius: 4px; + font-family: 'Share Tech Mono', monospace; + font-size: 12px; + line-height: 1.45; + resize: vertical; + box-sizing: border-box; +} + +.settings-textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 8px rgba(0, 255, 0, 0.3); +} + +.settings-group { + margin-bottom: 20px; + padding: 16px; + background: rgba(0, 30, 0, 0.35); + border: 1px solid rgba(0, 255, 0, 0.15); + border-radius: 6px; +} + +.settings-group-title { + font-size: 13px; + font-weight: bold; + color: var(--primary-color); + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +input[type="file"] { + color: var(--muted-text); + font-family: 'Share Tech Mono', monospace; + font-size: 12px; +} + +.settings-error-banner { + margin-bottom: 16px; + padding: 12px 16px; + background: rgba(255, 0, 0, 0.12); + border: 1px solid #ff6666; + color: #ff9999; + border-radius: 4px; +} + +.advanced-panel summary { + cursor: pointer; + list-style: none; +} + +.advanced-panel summary::-webkit-details-marker { + display: none; +} + +.advanced-panel[open] summary { + margin-bottom: 12px; +} + +.saved-import-panel { + border-color: rgba(0, 255, 0, 0.45); + box-shadow: 0 0 12px rgba(0, 255, 0, 0.12); +} + +.settings-group[data-active-import="true"] { + border-color: rgba(0, 255, 0, 0.55); +} + +.console-btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} diff --git a/public/css/settings.css b/public/css/settings.css new file mode 100644 index 0000000..1a0b87c --- /dev/null +++ b/public/css/settings.css @@ -0,0 +1,435 @@ +/* --- Settings Modal Styles --- */ +.settings-modal-content { + width: 90%; + max-width: 700px; + max-height: 85vh; + display: flex; + flex-direction: column; +} + +.settings-tabs { + display: flex; + gap: 5px; + margin-bottom: 15px; + border-bottom: 1px solid rgba(0, 255, 0, 0.2); + padding-bottom: 10px; + flex-wrap: wrap; +} + +.settings-tab { + padding: 8px 16px; + background: rgba(0, 50, 0, 0.3); + border: 1px solid rgba(0, 255, 0, 0.3); + border-radius: 4px; + color: var(--text-color); + cursor: pointer; + font-family: 'Share Tech Mono', monospace; + font-size: 13px; + transition: all 0.2s; +} + +.settings-tab:hover { + background: rgba(0, 100, 0, 0.4); + border-color: var(--primary-color); +} + +.settings-tab.active { + background: rgba(0, 255, 0, 0.15); + border-color: var(--primary-color); + box-shadow: 0 0 8px rgba(0, 255, 0, 0.3); +} + +.settings-section { + display: none; + flex-direction: column; + gap: 15px; + overflow-y: auto; + padding-right: 10px; + max-height: 55vh; +} + +.settings-section.active { + display: flex; +} + +.settings-group { + background: rgba(0, 30, 0, 0.4); + border: 1px solid rgba(0, 255, 0, 0.15); + border-radius: 6px; + padding: 15px; +} + +.settings-group-title { + font-size: 14px; + font-weight: bold; + color: var(--primary-color); + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.setting-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid rgba(0, 255, 0, 0.08); +} + +.setting-row:last-child { + border-bottom: none; +} + +.setting-label { + display: flex; + flex-direction: column; + gap: 4px; +} + +.setting-label span:first-child { + font-size: 13px; + color: var(--text-color); +} + +.setting-label span:last-child { + font-size: 11px; + color: rgba(0, 255, 0, 0.5); +} + +.setting-control { + display: flex; + align-items: center; + gap: 10px; +} + +.settings-toggle { + position: relative; + width: 48px; + height: 24px; +} + +.settings-toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.settings-toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 100, 0, 0.4); + border: 1px solid rgba(0, 255, 0, 0.3); + border-radius: 12px; + transition: 0.3s; +} + +.settings-toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 2px; + bottom: 2px; + background-color: #666; + border-radius: 50%; + transition: 0.3s; +} + +.settings-toggle input:checked + .settings-toggle-slider { + background-color: rgba(0, 255, 0, 0.3); + border-color: var(--primary-color); +} + +.settings-toggle input:checked + .settings-toggle-slider:before { + transform: translateX(24px); + background-color: var(--primary-color); +} + +.settings-select { + background: #000; + color: #00ff00; + border: 1px solid rgba(0, 255, 0, 0.4); + border-radius: 4px; + padding: 6px 10px; + font-family: 'Share Tech Mono', monospace; + font-size: 12px; + min-width: 120px; +} + +.settings-input { + background: #000; + color: #00ff00; + border: 1px solid rgba(0, 255, 0, 0.4); + border-radius: 4px; + padding: 6px 10px; + font-family: 'Share Tech Mono', monospace; + font-size: 12px; + width: 80px; + text-align: center; +} + +.settings-range { + width: 120px; + accent-color: var(--primary-color); +} + +.settings-actions { + display: flex; + justify-content: space-between; + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid rgba(0, 255, 0, 0.2); +} + +/* --- Onboarding Styles --- */ +.onboarding-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.95); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; +} + +.onboarding-container { + width: 90%; + max-width: 600px; + background: #0a0a0a; + border: 2px solid var(--primary-color); + border-radius: 8px; + box-shadow: 0 0 40px rgba(0, 255, 0, 0.3); + padding: 30px; + max-height: 90vh; + overflow-y: auto; +} + +.onboarding-header { + text-align: center; + margin-bottom: 25px; +} + +.onboarding-header h1 { + font-size: 24px; + color: var(--primary-color); + margin-bottom: 10px; + text-shadow: 0 0 10px rgba(0, 255, 0, 0.5); +} + +.onboarding-header p { + color: rgba(0, 255, 0, 0.7); + font-size: 14px; +} + +.onboarding-step { + display: none; +} + +.onboarding-step.active { + display: block; +} + +.onboarding-step h2 { + font-size: 18px; + color: #00ff00; + margin-bottom: 15px; +} + +.onboarding-step p { + font-size: 13px; + color: rgba(0, 255, 0, 0.8); + margin-bottom: 20px; + line-height: 1.5; +} + +.onboarding-options { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 20px; +} + +.onboarding-option { + display: flex; + align-items: center; + padding: 12px 15px; + background: rgba(0, 50, 0, 0.3); + border: 1px solid rgba(0, 255, 0, 0.2); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s; +} + +.onboarding-option:hover { + background: rgba(0, 100, 0, 0.4); + border-color: var(--primary-color); +} + +.onboarding-option.selected { + background: rgba(0, 255, 0, 0.15); + border-color: var(--primary-color); +} + +.onboarding-option input { + margin-right: 12px; + accent-color: var(--primary-color); + width: 18px; + height: 18px; +} + +.onboarding-option label { + cursor: pointer; + flex: 1; +} + +.onboarding-option .option-title { + font-size: 14px; + color: #00ff00; + display: block; +} + +.onboarding-option .option-desc { + font-size: 11px; + color: rgba(0, 255, 0, 0.6); + display: block; + margin-top: 4px; +} + +.onboarding-progress { + display: flex; + justify-content: center; + gap: 8px; + margin-bottom: 25px; +} + +.onboarding-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(0, 255, 0, 0.2); + border: 1px solid rgba(0, 255, 0, 0.4); + transition: all 0.3s; +} + +.onboarding-dot.active { + background: var(--primary-color); + box-shadow: 0 0 8px var(--primary-color); +} + +.onboarding-dot.completed { + background: #00aa00; +} + +.onboarding-nav { + display: flex; + justify-content: space-between; + margin-top: 20px; +} + +.onboarding-btn { + padding: 10px 25px; + border: 1px solid var(--primary-color); + border-radius: 4px; + font-family: 'Share Tech Mono', monospace; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.onboarding-btn.primary { + background: rgba(0, 255, 0, 0.2); + color: #00ff00; +} + +.onboarding-btn.primary:hover { + background: rgba(0, 255, 0, 0.3); + box-shadow: 0 0 15px rgba(0, 255, 0, 0.4); +} + +.onboarding-btn.secondary { + background: transparent; + color: rgba(0, 255, 0, 0.6); + border-color: rgba(0, 255, 0, 0.3); +} + +.onboarding-btn.secondary:hover { + background: rgba(0, 255, 0, 0.1); + color: #00ff00; +} + +.onboarding-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.onboarding-features { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 15px; + margin: 20px 0; +} + +.onboarding-feature { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 10px; + background: rgba(0, 30, 0, 0.3); + border-radius: 4px; +} + +.onboarding-feature-icon { + font-size: 20px; +} + +.onboarding-feature-text h4 { + font-size: 12px; + color: #00ff00; + margin-bottom: 4px; +} + +.onboarding-feature-text p { + font-size: 11px; + color: rgba(0, 255, 0, 0.6); + margin: 0; +} + +.onboarding-input-group { + margin-bottom: 15px; +} + +.onboarding-input-group label { + display: block; + font-size: 12px; + color: rgba(0, 255, 0, 0.8); + margin-bottom: 6px; +} + +.onboarding-input { + width: 100%; + padding: 10px 12px; + background: #000; + color: #00ff00; + border: 1px solid rgba(0, 255, 0, 0.4); + border-radius: 4px; + font-family: 'Share Tech Mono', monospace; + font-size: 14px; + box-sizing: border-box; +} + +.onboarding-input:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 8px rgba(0, 255, 0, 0.3); +} + +.onboarding-input::placeholder { + color: rgba(0, 255, 0, 0.3); +} diff --git a/public/index.html b/public/index.html index bc737ee..8577ce1 100644 --- a/public/index.html +++ b/public/index.html @@ -21,6 +21,7 @@ + @@ -415,441 +416,6 @@ filter: invert(1) hue-rotate(120deg) brightness(1.5) !important; } - /* --- Settings Modal Styles --- */ - .settings-modal-content { - width: 90%; - max-width: 700px; - max-height: 85vh; - display: flex; - flex-direction: column; - } - - .settings-tabs { - display: flex; - gap: 5px; - margin-bottom: 15px; - border-bottom: 1px solid rgba(0, 255, 0, 0.2); - padding-bottom: 10px; - flex-wrap: wrap; - } - - .settings-tab { - padding: 8px 16px; - background: rgba(0, 50, 0, 0.3); - border: 1px solid rgba(0, 255, 0, 0.3); - border-radius: 4px; - color: var(--text-color); - cursor: pointer; - font-family: 'Share Tech Mono', monospace; - font-size: 13px; - transition: all 0.2s; - } - - .settings-tab:hover { - background: rgba(0, 100, 0, 0.4); - border-color: var(--primary-color); - } - - .settings-tab.active { - background: rgba(0, 255, 0, 0.15); - border-color: var(--primary-color); - box-shadow: 0 0 8px rgba(0, 255, 0, 0.3); - } - - .settings-section { - display: none; - flex-direction: column; - gap: 15px; - overflow-y: auto; - padding-right: 10px; - max-height: 55vh; - } - - .settings-section.active { - display: flex; - } - - .settings-group { - background: rgba(0, 30, 0, 0.4); - border: 1px solid rgba(0, 255, 0, 0.15); - border-radius: 6px; - padding: 15px; - } - - .settings-group-title { - font-size: 14px; - font-weight: bold; - color: var(--primary-color); - margin-bottom: 12px; - text-transform: uppercase; - letter-spacing: 1px; - } - - .setting-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 0; - border-bottom: 1px solid rgba(0, 255, 0, 0.08); - } - - .setting-row:last-child { - border-bottom: none; - } - - .setting-label { - display: flex; - flex-direction: column; - gap: 4px; - } - - .setting-label span:first-child { - font-size: 13px; - color: var(--text-color); - } - - .setting-label span:last-child { - font-size: 11px; - color: rgba(0, 255, 0, 0.5); - } - - .setting-control { - display: flex; - align-items: center; - gap: 10px; - } - - .settings-toggle { - position: relative; - width: 48px; - height: 24px; - } - - .settings-toggle input { - opacity: 0; - width: 0; - height: 0; - } - - .settings-toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 100, 0, 0.4); - border: 1px solid rgba(0, 255, 0, 0.3); - border-radius: 12px; - transition: 0.3s; - } - - .settings-toggle-slider:before { - position: absolute; - content: ""; - height: 18px; - width: 18px; - left: 2px; - bottom: 2px; - background-color: #666; - border-radius: 50%; - transition: 0.3s; - } - - .settings-toggle input:checked + .settings-toggle-slider { - background-color: rgba(0, 255, 0, 0.3); - border-color: var(--primary-color); - } - - .settings-toggle input:checked + .settings-toggle-slider:before { - transform: translateX(24px); - background-color: var(--primary-color); - } - - .settings-select { - background: #000; - color: #00ff00; - border: 1px solid rgba(0, 255, 0, 0.4); - border-radius: 4px; - padding: 6px 10px; - font-family: 'Share Tech Mono', monospace; - font-size: 12px; - min-width: 120px; - } - - .settings-input { - background: #000; - color: #00ff00; - border: 1px solid rgba(0, 255, 0, 0.4); - border-radius: 4px; - padding: 6px 10px; - font-family: 'Share Tech Mono', monospace; - font-size: 12px; - width: 80px; - text-align: center; - } - - .settings-range { - width: 120px; - accent-color: var(--primary-color); - } - - .settings-actions { - display: flex; - justify-content: space-between; - margin-top: 15px; - padding-top: 15px; - border-top: 1px solid rgba(0, 255, 0, 0.2); - } - - /* --- Onboarding Styles --- */ - .onboarding-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.95); - z-index: 10000; - display: flex; - align-items: center; - justify-content: center; - } - - .onboarding-container { - width: 90%; - max-width: 600px; - background: #0a0a0a; - border: 2px solid var(--primary-color); - border-radius: 8px; - box-shadow: 0 0 40px rgba(0, 255, 0, 0.3); - padding: 30px; - max-height: 90vh; - overflow-y: auto; - } - - .onboarding-header { - text-align: center; - margin-bottom: 25px; - } - - .onboarding-header h1 { - font-size: 24px; - color: var(--primary-color); - margin-bottom: 10px; - text-shadow: 0 0 10px rgba(0, 255, 0, 0.5); - } - - .onboarding-header p { - color: rgba(0, 255, 0, 0.7); - font-size: 14px; - } - - .onboarding-step { - display: none; - } - - .onboarding-step.active { - display: block; - } - - .onboarding-step h2 { - font-size: 18px; - color: #00ff00; - margin-bottom: 15px; - } - - .onboarding-step p { - font-size: 13px; - color: rgba(0, 255, 0, 0.8); - margin-bottom: 20px; - line-height: 1.5; - } - - .onboarding-options { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 20px; - } - - .onboarding-option { - display: flex; - align-items: center; - padding: 12px 15px; - background: rgba(0, 50, 0, 0.3); - border: 1px solid rgba(0, 255, 0, 0.2); - border-radius: 6px; - cursor: pointer; - transition: all 0.2s; - } - - .onboarding-option:hover { - background: rgba(0, 100, 0, 0.4); - border-color: var(--primary-color); - } - - .onboarding-option.selected { - background: rgba(0, 255, 0, 0.15); - border-color: var(--primary-color); - } - - .onboarding-option input { - margin-right: 12px; - accent-color: var(--primary-color); - width: 18px; - height: 18px; - } - - .onboarding-option label { - cursor: pointer; - flex: 1; - } - - .onboarding-option .option-title { - font-size: 14px; - color: #00ff00; - display: block; - } - - .onboarding-option .option-desc { - font-size: 11px; - color: rgba(0, 255, 0, 0.6); - display: block; - margin-top: 4px; - } - - .onboarding-progress { - display: flex; - justify-content: center; - gap: 8px; - margin-bottom: 25px; - } - - .onboarding-dot { - width: 10px; - height: 10px; - border-radius: 50%; - background: rgba(0, 255, 0, 0.2); - border: 1px solid rgba(0, 255, 0, 0.4); - transition: all 0.3s; - } - - .onboarding-dot.active { - background: var(--primary-color); - box-shadow: 0 0 8px var(--primary-color); - } - - .onboarding-dot.completed { - background: #00aa00; - } - - .onboarding-nav { - display: flex; - justify-content: space-between; - margin-top: 20px; - } - - .onboarding-btn { - padding: 10px 25px; - border: 1px solid var(--primary-color); - border-radius: 4px; - font-family: 'Share Tech Mono', monospace; - font-size: 14px; - cursor: pointer; - transition: all 0.2s; - } - - .onboarding-btn.primary { - background: rgba(0, 255, 0, 0.2); - color: #00ff00; - } - - .onboarding-btn.primary:hover { - background: rgba(0, 255, 0, 0.3); - box-shadow: 0 0 15px rgba(0, 255, 0, 0.4); - } - - .onboarding-btn.secondary { - background: transparent; - color: rgba(0, 255, 0, 0.6); - border-color: rgba(0, 255, 0, 0.3); - } - - .onboarding-btn.secondary:hover { - background: rgba(0, 255, 0, 0.1); - color: #00ff00; - } - - .onboarding-btn:disabled { - opacity: 0.4; - cursor: not-allowed; - } - - .onboarding-features { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 15px; - margin: 20px 0; - } - - .onboarding-feature { - display: flex; - align-items: flex-start; - gap: 10px; - padding: 10px; - background: rgba(0, 30, 0, 0.3); - border-radius: 4px; - } - - .onboarding-feature-icon { - font-size: 20px; - } - - .onboarding-feature-text h4 { - font-size: 12px; - color: #00ff00; - margin-bottom: 4px; - } - - .onboarding-feature-text p { - font-size: 11px; - color: rgba(0, 255, 0, 0.6); - margin: 0; - } - - .onboarding-input-group { - margin-bottom: 15px; - } - - .onboarding-input-group label { - display: block; - font-size: 12px; - color: rgba(0, 255, 0, 0.8); - margin-bottom: 6px; - } - - .onboarding-input { - width: 100%; - padding: 10px 12px; - background: #000; - color: #00ff00; - border: 1px solid rgba(0, 255, 0, 0.4); - border-radius: 4px; - font-family: 'Share Tech Mono', monospace; - font-size: 14px; - box-sizing: border-box; - } - - .onboarding-input:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 8px rgba(0, 255, 0, 0.3); - } - - .onboarding-input::placeholder { - color: rgba(0, 255, 0, 0.3); - } @@ -913,15 +479,17 @@ + Server Settings @@ -1128,7 +696,7 @@

Live Feed Setup