diff --git a/README.md b/README.md index ba72211..f25fe59 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A cross-platform macro management application with desktop and web interfaces. C - **Hotkeys**: Key combinations (Ctrl+C, Alt+Tab, etc.) - **Wait**: Insert delays between commands (in milliseconds) - **App**: Launch applications or scripts +- **Media & System Keys**: Control playback and audio (volume up/down/mute, play/pause, next/previous track, stop) alongside regular keys - **Custom Images**: Assign images to macros for easy identification - **Category Management**: Organize macros into custom tabs @@ -119,15 +120,18 @@ A macro that types a username, waits, presses Tab, types a password, and presses ### Web Interface (PWA) 1. Start the application (web server starts automatically on port 40000) -2. Note the URL shown in the toolbar (e.g., `http://192.168.1.100:40000`) +2. Note the URL shown in the toolbar (e.g., `http://192.168.1.100:40000/?token=...`) 3. Open this URL on any device on your network 4. **Install as PWA**: - Mobile: Tap browser menu → "Add to Home Screen" - Desktop: Click install icon in address bar +> [!IMPORTANT] +> The local web server now requires an access token. A per-install token is auto-generated and stored in `settings.json`, and is embedded in the URL shown in the toolbar and in the QR code — so copying the URL or scanning the QR just works. Anyone with that URL can control your macros, so treat it like a password. By default the server also binds to your LAN; set `web.allow_lan` to `false` in settings to restrict access to localhost only. + The web interface provides full macro management: - View and execute macros -- Create and edit macros with command builder +- Create, edit, and delete macros directly from the web app — tap the **+** button to open the command-sequence builder (no desktop app required) - Organize into categories - Real-time sync across devices @@ -149,12 +153,18 @@ Access your macros from outside your local network using a relay server: Once connected, a relay URL will appear in the toolbar. Use this URL from any device with internet access. The relay provides: - Secure HTTPS connection -- Full macro execution and management +- Macro browsing and execution - PWA installation support - Wake lock and fullscreen mode > [!NOTE] -> You need access to a relay server. See [MP-Relay](https://repo.anhonesthost.net/MacroPad/MP-Relay) for self-hosting instructions. +> The relay web client is **execute-only**: remote devices can browse and trigger macros, but macro creation/editing is intentionally limited to the local network (the desktop app and the local web interface). This keeps macro authoring — which defines what runs on your computer — off the public internet. + +> [!NOTE] +> The relay password is stored in your OS keyring when one is available, falling back to the settings file (written with restricted `0600` permissions) otherwise. + +> [!NOTE] +> You need access to a relay server. See [MP-Relay](https://repo.anhonesthost.net/MacroPad/MP-Relay) for self-hosting instructions. The self-hosted relay now enforces login rate-limiting/lockout, keeps credentials out of URLs, and requires setting sane environment values — see the relay repo's `.env.example`. ## Command Types Reference @@ -164,10 +174,13 @@ Once connected, a relay URL will appear in the toolbar. Use this URL from any de | `key` | Presses a single key | `value`: Key name (enter, tab, escape, f1-f12, etc.) | | `hotkey` | Presses key combination | `keys`: List of keys (e.g., ["ctrl", "c"]) | | `wait` | Delays execution | `ms`: Milliseconds to wait | -| `app` | Launches application | `command`: Shell command to execute | +| `app` | Launches an application | `command`: Program and arguments to run (tokenized and executed directly — no shell) | ## Example Application Commands +> [!IMPORTANT] +> `app` commands are tokenized and the program is executed **directly**, without a shell. This closes a remote-code-execution risk, but it means shell features are **not** supported: command chaining (`;`, `&&`, `|`), output redirection (`>`), environment-variable expansion (`$VAR`, `%VAR%`), and shell built-ins will not work. Launching a program with arguments works exactly as shown below. + ### Windows ```bash # Steam game diff --git a/config.py b/config.py index b73ab9e..49364f1 100644 --- a/config.py +++ b/config.py @@ -13,7 +13,10 @@ THEME = { 'button_bg': "#505050", 'button_fg': "#ffffff", 'tab_bg': "#404040", - 'tab_selected': "#007acc" + 'tab_selected': "#007acc", + 'accent_hover': "#0096ff", + 'danger_color': "#dc3545", + 'danger_hover': "#c82333" } # File extensions for images diff --git a/gui/macro_editor.py b/gui/macro_editor.py index 39c0ba7..422a34b 100644 --- a/gui/macro_editor.py +++ b/gui/macro_editor.py @@ -1,19 +1,30 @@ # Macro editor dialog with command builder (PySide6) import os -from typing import Optional, List, Dict +from typing import Optional, List from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, - QComboBox, QSpinBox, QMessageBox, QFileDialog, QWidget, - QGroupBox, QScrollArea, QCompleter + QComboBox, QMessageBox, QFileDialog, QWidget, + QGroupBox, QScrollArea ) from PySide6.QtCore import Qt, Signal -from PySide6.QtGui import QPixmap, QIcon +from PySide6.QtGui import QPixmap from config import THEME, IMAGE_EXTENSIONS +# Selectable single keys offered in the key picker. Includes common media/system +# keys pyautogui supports, a near-zero-cost win for a media/stream deck. +KEY_CHOICES = [ + "Enter", "Tab", "Escape", "Space", "Backspace", "Delete", + "Up", "Down", "Left", "Right", "Home", "End", "PageUp", "PageDown", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + # Media / system keys + "VolumeUp", "VolumeDown", "VolumeMute", + "PlayPause", "NextTrack", "PrevTrack", "Stop", +] + class CommandItem(QWidget): """Widget representing a single command in the list.""" @@ -86,7 +97,7 @@ class CommandItem(QWidget): del_btn = QPushButton("X") del_btn.setStyleSheet(f""" QPushButton {{ - background-color: #dc3545; + background-color: {THEME['danger_color']}; color: white; border: none; padding: 6px 8px; @@ -94,7 +105,7 @@ class CommandItem(QWidget): min-height: 20px; }} QPushButton:hover {{ - background-color: #c82333; + background-color: {THEME['danger_hover']}; }} """) del_btn.setFixedSize(30, 28) @@ -212,9 +223,7 @@ class CommandBuilder(QWidget): elif cmd_type == "key": from PySide6.QtWidgets import QInputDialog - keys = ["Enter", "Tab", "Escape", "Space", "Backspace", "Delete", - "Up", "Down", "Left", "Right", "Home", "End", "PageUp", "PageDown", - "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + keys = list(KEY_CHOICES) key, ok = QInputDialog.getItem(self, "Key Command", "Select key:", keys, 0, True) if not ok or not key: return @@ -295,9 +304,7 @@ class CommandBuilder(QWidget): cmd["value"] = text elif cmd_type == "key": - keys = ["Enter", "Tab", "Escape", "Space", "Backspace", "Delete", - "Up", "Down", "Left", "Right", "Home", "End", "PageUp", "PageDown", - "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"] + keys = list(KEY_CHOICES) keys_lower = [k.lower() for k in keys] current = keys_lower.index(cmd.get("value", "enter")) if cmd.get("value") in keys_lower else 0 key, ok = QInputDialog.getItem(self, "Edit Key", "Select key:", keys, current, True) @@ -511,7 +518,7 @@ class MacroEditorDialog(QDialog): delete_btn = QPushButton("Delete") delete_btn.setStyleSheet(f""" QPushButton {{ - background-color: #dc3545; + background-color: {THEME['danger_color']}; color: white; border: none; padding: 10px 20px; @@ -519,7 +526,7 @@ class MacroEditorDialog(QDialog): font-weight: bold; }} QPushButton:hover {{ - background-color: #c82333; + background-color: {THEME['danger_hover']}; }} """) delete_btn.clicked.connect(self.delete_macro) @@ -554,7 +561,7 @@ class MacroEditorDialog(QDialog): font-weight: bold; }} QPushButton:hover {{ - background-color: #0096ff; + background-color: {THEME['accent_hover']}; }} """) save_btn.clicked.connect(self.save_macro) @@ -634,9 +641,15 @@ class MacroEditorDialog(QDialog): def delete_macro(self): """Delete the current macro.""" + macro_name = self.name_input.text().strip() + prompt = ( + f"Are you sure you want to delete \"{macro_name}\"?" + if macro_name else + "Are you sure you want to delete this macro?" + ) reply = QMessageBox.question( self, "Delete Macro", - "Are you sure you want to delete this macro?", + prompt, QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: diff --git a/gui/main_window.py b/gui/main_window.py index 4e18a5d..68eb75e 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -21,11 +21,11 @@ def get_resource_path(relative_path): from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTabWidget, QGridLayout, - QScrollArea, QFrame, QMenu, QMenuBar, QStatusBar, + QScrollArea, QMenu, QStatusBar, QMessageBox, QApplication, QSystemTrayIcon, QStyle ) -from PySide6.QtCore import Qt, Signal, QTimer, QSize, QEvent -from PySide6.QtGui import QIcon, QPixmap, QAction, QFont +from PySide6.QtCore import Qt, Signal, QTimer, QEvent +from PySide6.QtGui import QIcon, QPixmap, QAction from config import VERSION, THEME, DEFAULT_PORT, SETTINGS_FILE from macro_manager import MacroManager @@ -127,6 +127,8 @@ class MainWindow(QMainWindow): relay_session_received = Signal(str) relay_connected_signal = Signal() relay_disconnected_signal = Signal() + # Report macro execution result back to the GUI thread (success flag). + macro_execution_finished = Signal(bool) def __init__(self, app_dir: str): super().__init__() @@ -146,7 +148,12 @@ class MainWindow(QMainWindow): self.macro_manager = MacroManager(data_file, images_dir, app_dir) # Initialize web server - self.web_server = WebServer(self.macro_manager, app_dir, DEFAULT_PORT) + token = self.settings_manager.get_web_auth_token() + host = "0.0.0.0" if self.settings_manager.get_web_allow_lan() else "127.0.0.1" + self.web_server = WebServer( + self.macro_manager, app_dir, DEFAULT_PORT, + auth_token=token, host=host + ) self.server_thread = None # Relay client (initialized later if enabled) @@ -169,6 +176,14 @@ class MainWindow(QMainWindow): self.relay_session_received.connect(self._handle_relay_session) self.relay_connected_signal.connect(lambda: self._update_relay_status(True)) self.relay_disconnected_signal.connect(lambda: self._update_relay_status(False)) + self.macro_execution_finished.connect(self._on_macro_execution_finished) + + # Debounce timer for resize-driven grid rebuilds. + self._resize_timer = QTimer(self) + self._resize_timer.setSingleShot(True) + self._resize_timer.setInterval(150) + self._resize_timer.timeout.connect(self._apply_resize) + self._last_col_count = None # Load initial data self.refresh_tabs() @@ -204,7 +219,7 @@ class MainWindow(QMainWindow): font-weight: bold; }} QPushButton:hover {{ - background-color: #0096ff; + background-color: {THEME['accent_hover']}; }} """) add_btn.clicked.connect(self.add_macro) @@ -450,7 +465,10 @@ class MainWindow(QMainWindow): self.ip_label.setText(full_url) return - # Fall back to local IP + # Fall back to local IP. Append the auth token as a query param so the + # QR/copied URL lets a LAN device authenticate against the API. + token = self.settings_manager.get_web_auth_token() + token_qs = f"?token={token}" if token else "" try: import netifaces for iface in netifaces.interfaces(): @@ -459,11 +477,11 @@ class MainWindow(QMainWindow): for addr in addrs[netifaces.AF_INET]: ip = addr.get('addr', '') if ip and not ip.startswith('127.'): - self.ip_label.setText(f"http://{ip}:{DEFAULT_PORT}") + self.ip_label.setText(f"http://{ip}:{DEFAULT_PORT}{token_qs}") return except Exception: pass - self.ip_label.setText(f"http://localhost:{DEFAULT_PORT}") + self.ip_label.setText(f"http://localhost:{DEFAULT_PORT}{token_qs}") def copy_url_to_clipboard(self): """Copy the web interface URL to clipboard.""" @@ -547,7 +565,8 @@ class MainWindow(QMainWindow): filtered = self.macro_manager.filter_macros_by_tab(macro_list, tab_name) # Add macro buttons - cols = max(1, (self.width() - 40) // 130) + cols = self._compute_columns() + self._last_col_count = cols for i, (macro_id, macro) in enumerate(filtered): btn = MacroButton(macro_id, macro, self.app_dir) btn.clicked.connect(lambda checked, mid=macro_id: self.execute_macro(mid)) @@ -560,8 +579,25 @@ class MainWindow(QMainWindow): self.refresh_macros() def execute_macro(self, macro_id: str): - """Execute a macro.""" - success = self.macro_manager.execute_macro(macro_id) + """Execute a macro off the GUI thread so the UI doesn't freeze. + + pyautogui presses and wait steps are blocking; running them on the Qt + main thread would freeze the window. Run in a worker thread and report + the result back via a signal (thread-safe). + """ + self.status_bar.showMessage("Executing macro...", 2000) + + def run(): + try: + success = self.macro_manager.execute_macro(macro_id) + except Exception: + success = False + self.macro_execution_finished.emit(success) + + threading.Thread(target=run, daemon=True).start() + + def _on_macro_execution_finished(self, success: bool): + """Report macro execution result on the GUI thread.""" if success: self.status_bar.showMessage("Macro executed", 2000) else: @@ -585,9 +621,16 @@ class MainWindow(QMainWindow): def delete_macro(self, macro_id: str): """Delete a macro with confirmation.""" + macro = self.macro_manager.get_macro(macro_id) + macro_name = macro.get("name", "") if macro else "" + prompt = ( + f"Are you sure you want to delete \"{macro_name}\"?" + if macro_name else + "Are you sure you want to delete this macro?" + ) reply = QMessageBox.question( self, "Delete Macro", - "Are you sure you want to delete this macro?", + prompt, QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: @@ -703,7 +746,8 @@ class MainWindow(QMainWindow): local_port=DEFAULT_PORT, on_connected=self.on_relay_connected, on_disconnected=self.on_relay_disconnected, - on_session_id=self.on_relay_session_id + on_session_id=self.on_relay_session_id, + local_auth_token=self.settings_manager.get_web_auth_token() ) self.relay_client.start() self.status_bar.showMessage("Connecting to relay server...") @@ -729,7 +773,8 @@ class MainWindow(QMainWindow): def _handle_relay_session(self, session_id: str): """Handle relay session on main thread.""" - self.settings_manager.set_relay_session_id(session_id) + if not self.settings_manager.set_relay_session_id(session_id): + self.status_bar.showMessage("Warning: failed to save relay session", 3000) self.update_ip_label() def _update_relay_status(self, connected: bool): @@ -816,15 +861,27 @@ class MainWindow(QMainWindow): event.accept() + def _compute_columns(self) -> int: + """Compute how many macro columns fit at the current width.""" + return max(1, (self.width() - 40) // 130) + + def _apply_resize(self): + """Rebuild the grid only if the column count actually changed.""" + if self._compute_columns() != self._last_col_count: + self.refresh_macros() + def resizeEvent(self, event): - """Handle window resize.""" + """Handle window resize (debounced to coalesce rapid resize events).""" super().resizeEvent(event) - self.refresh_macros() + # Coalesce a burst of resize events; only rebuild after things settle + # and only when the computed column count changed. + self._resize_timer.start() def changeEvent(self, event): """Handle window state changes - minimize to tray.""" if event.type() == QEvent.Type.WindowStateChange: - if self.windowState() & Qt.WindowMinimized: + if (self.windowState() & Qt.WindowMinimized + and self.settings_manager.get_minimize_to_tray()): # Hide instead of minimize (goes to tray) event.ignore() self.hide() diff --git a/gui/settings_dialog.py b/gui/settings_dialog.py index 68d1de2..f29aa9e 100644 --- a/gui/settings_dialog.py +++ b/gui/settings_dialog.py @@ -123,7 +123,7 @@ class SettingsDialog(QDialog): color: white; }} QPushButton:hover {{ - background-color: #0096ff; + background-color: {THEME['accent_hover']}; }} """) save_btn.clicked.connect(self.save_settings) @@ -316,9 +316,15 @@ class SettingsDialog(QDialog): self.settings_manager.set('relay.enabled', new_enabled) self.settings_manager.set('relay.server_url', new_url) - self.settings_manager.set('relay.password', new_password) - self.settings_manager.save() + # Store the password via the keyring-aware setter (also persists the + # rest of the settings). Avoid writing the plaintext password to JSON. + ok = self.settings_manager.set_relay_password(new_password) + if not ok: + QMessageBox.warning( + self, "Save Warning", + "Settings could not be fully saved to disk." + ) if relay_changed: self.relay_settings_changed.emit() diff --git a/gui/settings_manager.py b/gui/settings_manager.py index 613bc13..2d24de7 100644 --- a/gui/settings_manager.py +++ b/gui/settings_manager.py @@ -2,8 +2,19 @@ import os import json +import secrets from typing import Any, Optional +# Optional OS keyring for storing the relay password at rest. If the library or +# a working backend is unavailable we fall back to the JSON file (now chmod 0600). +try: + import keyring as _keyring +except Exception: # ImportError or any backend import-time failure + _keyring = None + +KEYRING_SERVICE = "MacroPad" +KEYRING_USER = "relay" + DEFAULT_SETTINGS = { "relay": { @@ -12,6 +23,10 @@ DEFAULT_SETTINGS = { "session_id": None, "password": "" }, + "web": { + "auth_token": "", + "allow_lan": True + }, "minimize_to_tray": True } @@ -36,12 +51,23 @@ class SettingsManager: # Merge with defaults to ensure all keys exist self.settings = self._merge_defaults(DEFAULT_SETTINGS, self.settings) + # Ensure a web auth token exists (this is the security boundary for API access) + if not self.get('web.auth_token'): + token = secrets.token_urlsafe(32) + self.set('web.auth_token', token) + self.save() + def save(self): """Save settings to file.""" try: os.makedirs(os.path.dirname(self.settings_file), exist_ok=True) with open(self.settings_file, 'w', encoding='utf-8') as f: json.dump(self.settings, f, indent=2) + # Best-effort restrict permissions (protects stored relay password + token) + try: + os.chmod(self.settings_file, 0o600) + except OSError: + pass return True except IOError: return False @@ -93,13 +119,52 @@ class SettingsManager: return self.get('relay.session_id') def get_relay_password(self) -> str: - """Get the relay password.""" + """Get the relay password, preferring the OS keyring when available.""" + if _keyring is not None: + try: + value = _keyring.get_password(KEYRING_SERVICE, KEYRING_USER) + if value: + return value + except Exception: + # Keyring backend missing/broken: fall back to JSON. + pass return self.get('relay.password', '') - def set_relay_session_id(self, session_id: str): - """Store the relay session ID.""" + def set_relay_password(self, password: str) -> bool: + """Store the relay password, preferring the OS keyring when available. + + When keyring works, the plaintext password is kept out of the JSON file. + Never crashes if the keyring backend is missing; falls back to JSON. + Returns True on successful persistence. + """ + password = password or "" + stored_in_keyring = False + if _keyring is not None: + try: + _keyring.set_password(KEYRING_SERVICE, KEYRING_USER, password) + stored_in_keyring = True + except Exception: + stored_in_keyring = False + + if stored_in_keyring: + # Don't persist plaintext in the JSON when the keyring holds it. + self.set('relay.password', "") + else: + self.set('relay.password', password) + return self.save() + + def set_relay_session_id(self, session_id: str) -> bool: + """Store the relay session ID. Returns True on successful save.""" self.set('relay.session_id', session_id) - self.save() + return self.save() + + def get_web_auth_token(self) -> str: + """Get the web API auth token.""" + return self.get('web.auth_token', '') + + def get_web_allow_lan(self) -> bool: + """Check if LAN (0.0.0.0) binding is allowed.""" + return self.get('web.allow_lan', True) def get_minimize_to_tray(self) -> bool: """Check if minimize to tray is enabled.""" diff --git a/macro_manager.py b/macro_manager.py index c770f07..4c94b19 100644 --- a/macro_manager.py +++ b/macro_manager.py @@ -3,6 +3,9 @@ import copy import json import os +import shlex +import tempfile +import threading import uuid import pyautogui import subprocess @@ -11,37 +14,61 @@ from PIL import Image from typing import Optional +# --- Payload validation for execution --------------------------------------- +# Allowlist of key names pyautogui knows how to press. Anything not in this set +# is rejected before being handed to pyautogui so arbitrary strings can't be +# passed through. Populated from pyautogui when available. +VALID_KEYS = frozenset(getattr(pyautogui, "KEYBOARD_KEYS", []) or []) + +# Upper bound (milliseconds) for a single wait step so a macro can't hang the +# executor forever. +MAX_WAIT_MS = 60000 + +# Only persist last_used to disk at most this often (seconds) to avoid rewriting +# the whole JSON file on every single execution. +LAST_USED_SAVE_INTERVAL = 5.0 + + class MacroManager: - """Manages macro storage, migration, and execution with command sequences.""" + """Manages macro storage, migration, and execution with command sequences. + + Thread-safe: all reads/writes of ``self.macros`` and all disk writes are + guarded by ``self._lock``. Public methods acquire the lock and delegate to + ``*_locked`` internal helpers so we never re-acquire a non-reentrant lock. + """ def __init__(self, data_file: str, images_dir: str, app_dir: str): self.data_file = data_file self.images_dir = images_dir self.app_dir = app_dir self.macros = {} + self._lock = threading.Lock() + # Timestamp of the last last_used-triggered save (for debouncing). + self._last_used_save_time = 0.0 self.load_macros() def load_macros(self): """Load macros from JSON file and migrate if needed.""" - try: - if os.path.exists(self.data_file): - with open(self.data_file, "r") as file: - self.macros = json.load(file) + with self._lock: + try: + if os.path.exists(self.data_file): + with open(self.data_file, "r") as file: + self.macros = json.load(file) - # Migrate old format macros - migrated = False - for macro_id, macro in list(self.macros.items()): - if macro.get("type") != "sequence": - self.macros[macro_id] = self._migrate_macro(macro) - migrated = True + # Migrate old format macros + migrated = False + for macro_id, macro in list(self.macros.items()): + if macro.get("type") != "sequence": + self.macros[macro_id] = self._migrate_macro(macro) + migrated = True - if migrated: - self.save_macros() - print("Migrated macros to new command sequence format") + if migrated: + self._save_macros_locked() + print("Migrated macros to new command sequence format") - except Exception as e: - print(f"Error loading macros: {e}") - self.macros = {} + except Exception as e: + print(f"Error loading macros: {e}") + self.macros = {} def _migrate_macro(self, old_macro: dict) -> dict: """Convert old macro format to new command sequence format.""" @@ -91,28 +118,56 @@ class MacroManager: } def save_macros(self): - """Save macros to JSON file.""" + """Save macros to JSON file (thread-safe, atomic).""" + with self._lock: + self._save_macros_locked() + + def _save_macros_locked(self): + """Atomically write macros to disk. Caller must hold ``self._lock``. + + Writes to a temp file in the same directory then os.replace() over the + target so a crash mid-write can't leave a truncated/corrupt JSON file. + """ try: - with open(self.data_file, "w") as file: - json.dump(self.macros, file, indent=4) + target_dir = os.path.dirname(self.data_file) or "." + os.makedirs(target_dir, exist_ok=True) + fd, tmp_path = tempfile.mkstemp( + prefix=".macros-", suffix=".tmp", dir=target_dir + ) + try: + with os.fdopen(fd, "w") as file: + json.dump(self.macros, file, indent=4) + file.flush() + os.fsync(file.fileno()) + os.replace(tmp_path, self.data_file) + except Exception: + # Clean up the temp file on any failure + try: + os.remove(tmp_path) + except OSError: + pass + raise except Exception as e: print(f"Error saving macros: {e}") def get_sorted_macros(self, sort_by: str = "name"): """Get macros sorted by specified criteria.""" - macro_list = list(self.macros.items()) + with self._lock: + macro_list = list(self.macros.items()) if sort_by == "name": - macro_list.sort(key=lambda x: x[1]["name"].lower()) + macro_list.sort(key=lambda x: x[1].get("name", "").lower()) elif sort_by == "type": # Sort by first command type in sequence def get_first_type(macro): cmds = macro.get("commands", []) return cmds[0].get("type", "") if cmds else "" - macro_list.sort(key=lambda x: (get_first_type(x[1]), x[1]["name"].lower())) + macro_list.sort( + key=lambda x: (get_first_type(x[1]), x[1].get("name", "").lower()) + ) elif sort_by == "recent": macro_list.sort( - key=lambda x: (x[1].get("last_used", 0), x[1]["name"].lower()), + key=lambda x: (x[1].get("last_used", 0), x[1].get("name", "").lower()), reverse=True ) @@ -141,9 +196,10 @@ class MacroManager: tabs = ["All"] categories = set() - for macro in self.macros.values(): - if macro.get("category"): - categories.add(macro["category"]) + with self._lock: + for macro in self.macros.values(): + if macro.get("category"): + categories.add(macro["category"]) for category in sorted(categories): if category and category not in tabs: @@ -161,7 +217,7 @@ class MacroManager: """Add a new macro with command sequence.""" macro_id = str(uuid.uuid4()) - # Process image if provided + # Process image if provided (file IO; does not touch self.macros) image_path_reference = self._process_image(image_path) if image_path else "" # Create macro data (deep copy commands to avoid reference issues) @@ -174,8 +230,9 @@ class MacroManager: "last_used": 0 } - self.macros[macro_id] = macro_data - self.save_macros() + with self._lock: + self.macros[macro_id] = macro_data + self._save_macros_locked() return macro_id def update_macro( @@ -187,28 +244,32 @@ class MacroManager: image_path: Optional[str] = None ) -> bool: """Update an existing macro.""" - if macro_id not in self.macros: - return False + with self._lock: + if macro_id not in self.macros: + return False + existing_image = self.macros[macro_id].get("image_path", "") + existing_last_used = self.macros[macro_id].get("last_used", 0) - macro = self.macros[macro_id] - - # Keep old image or update with new one + # Keep old image or update with new one (file IO outside the lock) if image_path is not None: image_path_reference = self._process_image(image_path) if image_path else "" else: - image_path_reference = macro.get("image_path", "") + image_path_reference = existing_image - # Update macro data (deep copy commands to avoid reference issues) - self.macros[macro_id] = { + new_data = { "name": name, "type": "sequence", "commands": copy.deepcopy(commands), "category": category, "image_path": image_path_reference, - "last_used": macro.get("last_used", 0) + "last_used": existing_last_used } - self.save_macros() + with self._lock: + if macro_id not in self.macros: + return False + self.macros[macro_id] = new_data + self._save_macros_locked() return True def _process_image(self, image_path: str) -> str: @@ -233,37 +294,50 @@ class MacroManager: def delete_macro(self, macro_id: str) -> bool: """Delete a macro.""" - if macro_id not in self.macros: - return False + with self._lock: + if macro_id not in self.macros: + return False - macro = self.macros[macro_id] + macro = self.macros[macro_id] + image_path = macro.get("image_path") + del self.macros[macro_id] + self._save_macros_locked() - # Delete associated image file - if macro.get("image_path"): + # Delete associated image file (outside the lock; pure file IO) + if image_path: try: - img_path = os.path.join(self.app_dir, macro["image_path"]) + img_path = os.path.join(self.app_dir, image_path) if os.path.exists(img_path): os.remove(img_path) except Exception as e: print(f"Error removing image file: {e}") - del self.macros[macro_id] - self.save_macros() return True def execute_macro(self, macro_id: str) -> bool: - """Execute a macro by ID.""" - if macro_id not in self.macros: - return False + """Execute a macro by ID. - macro = self.macros[macro_id] + The macro's commands are copied under the lock, then executed *outside* + the lock so long-running wait/keyboard steps don't block other threads + (web server, GUI). last_used is updated in memory and only persisted at + most every LAST_USED_SAVE_INTERVAL seconds to avoid rewriting the whole + file on every press. + """ + with self._lock: + if macro_id not in self.macros: + return False - # Update last_used timestamp - self.macros[macro_id]["last_used"] = time.time() - self.save_macros() + macro = self.macros[macro_id] + commands = copy.deepcopy(macro.get("commands", [])) + + # Update last_used in memory; debounce the disk write. + now = time.time() + macro["last_used"] = now + if now - self._last_used_save_time >= LAST_USED_SAVE_INTERVAL: + self._last_used_save_time = now + self._save_macros_locked() try: - commands = macro.get("commands", []) for cmd in commands: self._execute_command(cmd) return True @@ -271,6 +345,22 @@ class MacroManager: print(f"Error executing macro: {e}") return False + @staticmethod + def _valid_keys(keys) -> list: + """Filter a list of key names to those pyautogui recognises.""" + result = [] + for k in keys: + if not isinstance(k, str): + continue + name = k.strip().lower() + if not name: + continue + if VALID_KEYS and name not in VALID_KEYS: + print(f"Skipping invalid key: {name!r}") + continue + result.append(name) + return result + def _execute_command(self, cmd: dict): """Execute a single command from a sequence.""" cmd_type = cmd.get("type", "") @@ -293,10 +383,11 @@ class MacroManager: pyautogui.typewrite(value, interval=0.02) elif cmd_type == "key": - # Press a single key + # Press a single key (validated against pyautogui's known keys) key = cmd.get("value", "") - if key: - pyautogui.press(key) + valid = self._valid_keys([key]) if key else [] + if valid: + pyautogui.press(valid[0]) elif cmd_type == "hotkey": # Press key combination @@ -305,21 +396,37 @@ class MacroManager: # Ensure keys is a list, not a string if isinstance(keys, str): keys = [k.strip().lower() for k in keys.split(",")] - # Small delay before hotkey for reliability on Windows - time.sleep(0.05) - pyautogui.hotkey(*keys, interval=0.05) + valid = self._valid_keys(keys) + # Only fire if every requested key was valid; a partial hotkey + # is worse than nothing. + if valid and len(valid) == len([k for k in keys if str(k).strip()]): + # Small delay before hotkey for reliability on Windows + time.sleep(0.05) + pyautogui.hotkey(*valid, interval=0.05) elif cmd_type == "wait": - # Delay in milliseconds - ms = cmd.get("ms", 0) + # Delay in milliseconds (bounded, non-negative) + try: + ms = int(cmd.get("ms", 0)) + except (TypeError, ValueError): + ms = 0 if ms > 0: + ms = min(ms, MAX_WAIT_MS) time.sleep(ms / 1000.0) elif cmd_type == "app": - # Launch application + # Launch application. + # SECURITY: shell=True was removed to kill shell-metacharacter + # injection (no ; && | $() chaining). We tokenize the command + # and exec the program directly without a shell. command = cmd.get("command", "") if command: - subprocess.Popen(command, shell=True) + try: + args = shlex.split(command, posix=(os.name != "nt")) + if args: + subprocess.Popen(args) + except Exception as e: + print(f"Error launching app command: {e}") # Legacy API compatibility methods def add_macro_legacy( @@ -357,8 +464,11 @@ class MacroManager: def get_macro(self, macro_id: str) -> Optional[dict]: """Get a macro by ID.""" - return self.macros.get(macro_id) + with self._lock: + macro = self.macros.get(macro_id) + return copy.deepcopy(macro) if macro is not None else None def get_all_macros(self) -> dict: """Get all macros.""" - return self.macros.copy() + with self._lock: + return copy.deepcopy(self.macros) diff --git a/macropad-relay/.env.example b/macropad-relay/.env.example index 45adbaf..b3e2474 100644 --- a/macropad-relay/.env.example +++ b/macropad-relay/.env.example @@ -9,12 +9,37 @@ NODE_ENV=production # Security BCRYPT_ROUNDS=10 -# Session ID length (default: 6 characters) -SESSION_ID_LENGTH=6 +# Session ID length (default: 12 characters) +SESSION_ID_LENGTH=12 -# Rate limiting -RATE_LIMIT_WINDOW_MS=90 -RATE_LIMIT_MAX=1024 +# Minimum password length enforced on session creation +MIN_PASSWORD_LENGTH=6 + +# Maximum number of concurrent sessions +MAX_SESSIONS=1000 + +# Prune sessions unused for this many days +SESSION_TTL_DAYS=90 + +# HTTP rate limiting (15 minute window, 300 requests per window) +RATE_LIMIT_WINDOW_MS=900000 +RATE_LIMIT_MAX=300 + +# WebSocket upgrade rate limiting (per IP) +WS_UPGRADE_WINDOW_MS=60000 +WS_UPGRADE_MAX=60 + +# Auth brute-force protection +AUTH_MAX_SOCKET_FAILURES=5 +AUTH_LOCKOUT_MAX_ATTEMPTS=10 +AUTH_LOCKOUT_WINDOW_MS=900000 +AUTH_LOCKOUT_DURATION_MS=900000 + +# Allowed origins for CORS + WebSocket (comma-separated). Empty = same-host only. +# ALLOWED_ORIGINS=https://macropad.example.com + +# Trust reverse proxy (number of hops, or true/false). Default: 1 +TRUST_PROXY=1 # WebSocket timeouts (in milliseconds) PING_INTERVAL=30000 diff --git a/macropad-relay/DEPLOY.md b/macropad-relay/DEPLOY.md index 35a997d..c2b6011 100644 --- a/macropad-relay/DEPLOY.md +++ b/macropad-relay/DEPLOY.md @@ -9,9 +9,12 @@ For AnHonestHost cloud-node-container deployment: ```bash cd /home/jknapp/code/macropad/macropad-relay npm install -npm run build +npm run build # required: the build is no longer triggered automatically on install ``` +> Note: the `postinstall` build hook was removed. Always run `npm run build` +> explicitly (locally or in CI) after `npm install` to produce `dist/`. + ### 2. Prepare Deployment Package The build outputs to `dist/` with public files copied. Upload: @@ -77,6 +80,19 @@ Set these in your container configuration: - `DATA_DIR` - Data storage path (default: ./data) - `NODE_ENV` - production or development - `LOG_LEVEL` - info, debug, error +- `ALLOWED_ORIGINS` - Comma-separated list of allowed browser origins for the + WebSocket Origin check (e.g. `https://macropad.example.com`). **Set this to + your public origin(s) when running behind a reverse proxy** (see note below). + +> **Reverse proxy note (`ALLOWED_ORIGINS`):** When `ALLOWED_ORIGINS` is unset, +> the WebSocket upgrade Origin check falls back to comparing the browser's +> `Origin` host against the raw `Host` header seen by the app. Behind a reverse +> proxy the internal `Host` (e.g. `localhost:3000`) often differs from the +> public origin the browser sends (e.g. `https://macropad.example.com`), so +> legitimate browser WebSocket upgrades get **rejected**. Set `ALLOWED_ORIGINS` +> to your public origin(s) to fix this. (Forwarding the public host via +> `proxy_set_header Host $host;`, as in the Nginx example below, also helps, but +> setting `ALLOWED_ORIGINS` explicitly is the reliable fix.) ## Test It Works @@ -84,8 +100,8 @@ Set these in your container configuration: # Test health endpoint curl http://localhost:3000/health -# Should return: -# {"status":"ok","desktopConnections":0,"webClients":0,"sessions":[]} +# Should return (counts only - no session ids are exposed): +# {"status":"ok","desktops":0,"webClients":0,"uptime":1.23} ``` ## Nginx/Reverse Proxy (for HTTPS) diff --git a/macropad-relay/package-lock.json b/macropad-relay/package-lock.json new file mode 100644 index 0000000..fd3b1d6 --- /dev/null +++ b/macropad-relay/package-lock.json @@ -0,0 +1,2484 @@ +{ + "name": "macropad-relay", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "macropad-relay", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "helmet": "^7.1.0", + "typescript": "^5.3.2", + "winston": "^3.11.0", + "ws": "^8.14.2" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.16", + "@types/express": "^4.17.21", + "@types/ws": "^8.5.9", + "nodemon": "^3.0.2", + "ts-node": "^10.9.2" + } + }, + "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/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "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/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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/@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/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/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/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/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/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/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.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/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/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/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/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/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "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/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/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-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/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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.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/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-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-minipass/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/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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "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/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/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/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/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/glob/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/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/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/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/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/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-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/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "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/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-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "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/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/minizlib/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/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "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-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-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/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "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/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/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/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-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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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/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/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "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/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/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/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-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/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.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/macropad-relay/package.json b/macropad-relay/package.json index 4247ca2..163c9b0 100644 --- a/macropad-relay/package.json +++ b/macropad-relay/package.json @@ -4,7 +4,6 @@ "description": "Relay server for MacroPad remote access", "main": "dist/index.js", "scripts": { - "postinstall": "npm run build", "build": "tsc", "start": "node dist/index.js", "dev": "nodemon --exec ts-node src/index.ts", @@ -21,7 +20,6 @@ "express-rate-limit": "^7.1.5", "helmet": "^7.1.0", "typescript": "^5.3.2", - "uuid": "^9.0.0", "winston": "^3.11.0", "ws": "^8.14.2" }, @@ -29,7 +27,6 @@ "@types/bcrypt": "^5.0.2", "@types/cors": "^2.8.16", "@types/express": "^4.17.21", - "@types/uuid": "^9.0.6", "@types/ws": "^8.5.9", "nodemon": "^3.0.2", "ts-node": "^10.9.2" diff --git a/macropad-relay/public/app.html b/macropad-relay/public/app.html index 526b617..2eacf99 100644 --- a/macropad-relay/public/app.html +++ b/macropad-relay/public/app.html @@ -264,8 +264,8 @@
- - + + @@ -277,345 +277,6 @@
- + diff --git a/macropad-relay/public/app.js b/macropad-relay/public/app.js new file mode 100644 index 0000000..8cb6f02 --- /dev/null +++ b/macropad-relay/public/app.js @@ -0,0 +1,420 @@ +// MacroPad App for Relay Mode (externalized so the CSP can drop +// 'unsafe-inline' for scripts). All macro/tab/id/name values are rendered +// with safe DOM APIs (createElement/textContent) and never via innerHTML. +class MacroPadApp { + constructor() { + this.macros = {}; + this.tabs = []; + this.currentTab = 'All'; + this.ws = null; + this.desktopConnected = false; + this.wsAuthenticated = false; + + // Reconnect backoff. Reset only after a successful AUTH (not on + // socket open): a server that accepts the socket and then closes it + // would otherwise reset the backoff on every open and defeat it. + this.baseReconnectDelay = 3000; + this.maxReconnectDelay = 30000; + this.reconnectDelay = this.baseReconnectDelay; + + // Get session ID from URL + const pathMatch = window.location.pathname.match(/^\/([a-zA-Z0-9]+)/); + this.sessionId = pathMatch ? pathMatch[1] : null; + + // Get password from storage (never from the URL). + this.password = sessionStorage.getItem(`macropad_${this.sessionId}`) + || localStorage.getItem(`macropad_${this.sessionId}`); + + if (this.password) { + sessionStorage.setItem(`macropad_${this.sessionId}`, this.password); + } + + this.init(); + } + + async init() { + this.wakeLock = null; + this.wakeLockEnabled = false; + + this.setupPWA(); + this.setupWebSocket(); + this.setupEventListeners(); + this.setupWakeLock(); + } + + getApiHeaders() { + return { + 'Content-Type': 'application/json', + 'X-MacroPad-Password': this.password || '' + }; + } + + async loadTabs() { + try { + const response = await fetch(`/${this.sessionId}/api/tabs`, { + headers: this.getApiHeaders() + }); + if (response.status === 401) return this.handleAuthError(); + if (response.status === 503) return this.handleDesktopOffline(); + const data = await response.json(); + this.tabs = data.tabs || []; + this.renderTabs(); + } catch (error) { + console.error('Error loading tabs:', error); + } + } + + async loadMacros() { + try { + const path = this.currentTab === 'All' ? '/api/macros' : `/api/macros/${encodeURIComponent(this.currentTab)}`; + const response = await fetch(`/${this.sessionId}${path}`, { + headers: this.getApiHeaders() + }); + if (response.status === 401) return this.handleAuthError(); + if (response.status === 503) return this.handleDesktopOffline(); + const data = await response.json(); + this.macros = data.macros || {}; + this.renderMacros(); + } catch (error) { + console.error('Error loading macros:', error); + } + } + + async executeMacro(macroId) { + const card = document.querySelector(`[data-macro-id="${macroId}"]`); + if (card) card.classList.add('executing'); + + try { + const response = await fetch(`/${this.sessionId}/api/execute`, { + method: 'POST', + headers: this.getApiHeaders(), + body: JSON.stringify({ macro_id: macroId }) + }); + if (!response.ok) throw new Error('Failed'); + } catch (error) { + this.showToast('Execution failed', 'error'); + } + + setTimeout(() => card?.classList.remove('executing'), 300); + } + + handleAuthError() { + sessionStorage.removeItem(`macropad_${this.sessionId}`); + localStorage.removeItem(`macropad_${this.sessionId}`); + window.location.href = `/${this.sessionId}`; + } + + handleDesktopOffline() { + this.desktopConnected = false; + this.updateConnectionStatus(false); + document.getElementById('offline-banner').classList.add('visible'); + } + + setupWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/${this.sessionId}/ws`; + + this.ws = new WebSocket(wsUrl); + + this.ws.onmessage = (event) => { + const data = JSON.parse(event.data); + this.handleMessage(data); + }; + + this.ws.onclose = () => { + this.wsAuthenticated = false; + this.updateConnectionStatus(false); + // Reconnect using the current backoff, then grow it (capped) for + // the next attempt. The backoff is only reset on a successful + // auth response, so accept-then-close cycles keep backing off. + const delay = this.reconnectDelay; + this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay); + setTimeout(() => this.setupWebSocket(), delay); + }; + + this.ws.onerror = () => this.updateConnectionStatus(false); + } + + handleMessage(data) { + switch (data.type) { + case 'auth_required': + if (this.password) { + this.ws.send(JSON.stringify({ type: 'auth', password: this.password })); + } + break; + case 'auth_response': + if (data.success) { + this.wsAuthenticated = true; + // Reset the reconnect backoff only after a confirmed, + // successful auth (not merely on socket open). + this.reconnectDelay = this.baseReconnectDelay; + this.updateConnectionStatus(this.desktopConnected); + } else { + this.handleAuthError(); + } + break; + case 'desktop_status': + this.desktopConnected = data.status === 'connected'; + this.updateConnectionStatus(this.desktopConnected); + document.getElementById('offline-banner').classList.toggle('visible', !this.desktopConnected); + if (this.desktopConnected) { + this.loadTabs(); + this.loadMacros(); + } + break; + case 'macro_created': + case 'macro_updated': + case 'macro_deleted': + this.loadTabs(); + this.loadMacros(); + break; + case 'executed': { + const card = document.querySelector(`[data-macro-id="${data.macro_id}"]`); + if (card) { + card.classList.add('executing'); + setTimeout(() => card.classList.remove('executing'), 300); + } + break; + } + } + } + + updateConnectionStatus(connected) { + const dot = document.querySelector('.status-dot'); + const text = document.querySelector('.connection-status span'); + if (dot) dot.classList.toggle('connected', connected); + if (text) text.textContent = connected ? 'Connected' : 'Disconnected'; + } + + renderTabs() { + const container = document.getElementById('tabs-container'); + container.replaceChildren(); + for (const tab of this.tabs) { + const name = typeof tab === 'string' ? tab : String(tab); + const btn = document.createElement('button'); + btn.className = name === this.currentTab ? 'tab active' : 'tab'; + btn.setAttribute('data-tab', name); + btn.textContent = name; + container.appendChild(btn); + } + } + + renderMacros() { + const container = document.getElementById('macro-grid'); + const entries = Object.entries(this.macros); + + container.replaceChildren(); + + if (entries.length === 0) { + const empty = document.createElement('div'); + empty.className = 'empty-state'; + const p = document.createElement('p'); + p.textContent = 'No macros found'; + empty.appendChild(p); + container.appendChild(empty); + return; + } + + for (const [id, macro] of entries) { + const name = macro && typeof macro.name === 'string' ? macro.name : ''; + const hasImage = !!(macro && macro.image_path); + const firstChar = name.charAt(0).toUpperCase(); + + const card = document.createElement('div'); + card.className = 'macro-card'; + card.setAttribute('data-macro-id', id); + card.addEventListener('click', () => this.executeMacro(id)); + + if (hasImage) { + const img = document.createElement('img'); + img.className = 'macro-image'; + img.style.display = 'none'; + card.appendChild(img); + } + + const placeholder = document.createElement('div'); + placeholder.className = 'macro-image-placeholder'; + placeholder.textContent = firstChar; + card.appendChild(placeholder); + + const nameSpan = document.createElement('span'); + nameSpan.className = 'macro-name'; + nameSpan.textContent = name; + card.appendChild(nameSpan); + + container.appendChild(card); + } + + // Load macro images via authenticated fetch (header, not URL). + this.loadMacroImages(); + } + + // Fetch each macro image with the password header and display it + // as a blob object URL so the credential never appears in a URL. + async loadMacroImages() { + for (const [id, macro] of Object.entries(this.macros)) { + if (!macro.image_path) continue; + const card = document.querySelector(`[data-macro-id="${id}"]`); + if (!card) continue; + const img = card.querySelector('.macro-image'); + if (!img) continue; + + try { + const response = await fetch( + `/${this.sessionId}/api/image/${macro.image_path}`, + { headers: this.getApiHeaders() } + ); + if (!response.ok) continue; // keep placeholder + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + img.onload = () => URL.revokeObjectURL(objectUrl); + img.src = objectUrl; + img.style.display = ''; + const placeholder = img.nextElementSibling; + if (placeholder) placeholder.style.display = 'none'; + } catch (error) { + // Leave the placeholder visible on failure. + } + } + } + + setupEventListeners() { + document.getElementById('tabs-container').addEventListener('click', (e) => { + if (e.target.classList.contains('tab')) { + this.currentTab = e.target.dataset.tab; + this.renderTabs(); + this.loadMacros(); + } + }); + + const fullscreenBtn = document.getElementById('fullscreen-btn'); + if (fullscreenBtn) fullscreenBtn.addEventListener('click', () => this.toggleFullscreen()); + + const refreshBtn = document.getElementById('refresh-btn'); + if (refreshBtn) refreshBtn.addEventListener('click', () => this.refresh()); + } + + showToast(message, type = 'info') { + const container = document.getElementById('toast-container'); + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.textContent = message; + container.appendChild(toast); + setTimeout(() => toast.remove(), 3000); + } + + refresh() { + this.loadTabs(); + this.loadMacros(); + } + + // Fullscreen + toggleFullscreen() { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen().catch(err => { + console.log('Fullscreen error:', err); + }); + } else { + document.exitFullscreen(); + } + } + + // Wake Lock + async setupWakeLock() { + const status = document.getElementById('wake-lock-status'); + + if (!('wakeLock' in navigator)) { + console.log('Wake Lock API not supported'); + if (status) { + status.classList.add('unsupported'); + status.title = 'Wake lock not available (requires HTTPS)'; + } + return; + } + + if (status) { + status.style.cursor = 'pointer'; + status.addEventListener('click', () => this.toggleWakeLock()); + } + + await this.requestWakeLock(); + + document.addEventListener('visibilitychange', async () => { + if (document.visibilityState === 'visible' && this.wakeLockEnabled) { + await this.requestWakeLock(); + } + }); + } + + async toggleWakeLock() { + if (this.wakeLock) { + await this.wakeLock.release(); + this.wakeLock = null; + this.wakeLockEnabled = false; + this.updateWakeLockStatus(false); + this.showToast('Screen can now sleep', 'info'); + } else { + this.wakeLockEnabled = true; + await this.requestWakeLock(); + if (this.wakeLock) { + this.showToast('Screen will stay awake', 'success'); + } + } + } + + async requestWakeLock() { + try { + this.wakeLock = await navigator.wakeLock.request('screen'); + this.wakeLockEnabled = true; + this.updateWakeLockStatus(true); + + this.wakeLock.addEventListener('release', () => { + this.updateWakeLockStatus(false); + }); + } catch (err) { + console.log('Wake Lock error:', err); + this.updateWakeLockStatus(false); + const status = document.getElementById('wake-lock-status'); + if (status && !status.classList.contains('unsupported')) { + status.title = 'Wake lock failed: ' + err.message; + } + } + } + + updateWakeLockStatus(active) { + const status = document.getElementById('wake-lock-status'); + if (status) { + status.classList.toggle('active', active); + if (!status.classList.contains('unsupported')) { + status.title = active ? 'Screen will stay on (click to toggle)' : 'Screen may sleep (click to enable)'; + } + } + } + + // PWA manifest setup + setupPWA() { + // Create dynamic manifest for this session + const manifest = { + name: 'MacroPad', + short_name: 'MacroPad', + description: 'Remote macro control', + start_url: `/${this.sessionId}/app`, + display: 'standalone', + background_color: '#2e2e2e', + theme_color: '#007acc', + icons: [ + { src: '/static/icons/icon-192.png', sizes: '192x192', type: 'image/png' }, + { src: '/static/icons/icon-512.png', sizes: '512x512', type: 'image/png' } + ] + }; + + const blob = new Blob([JSON.stringify(manifest)], { type: 'application/json' }); + const manifestUrl = URL.createObjectURL(blob); + document.getElementById('manifest-link').setAttribute('href', manifestUrl); + } +} + +let app; +document.addEventListener('DOMContentLoaded', () => { + app = new MacroPadApp(); +}); diff --git a/macropad-relay/public/login.html b/macropad-relay/public/login.html index 97b3a27..ab6397e 100644 --- a/macropad-relay/public/login.html +++ b/macropad-relay/public/login.html @@ -158,92 +158,6 @@

Checking connection...

- + diff --git a/macropad-relay/public/login.js b/macropad-relay/public/login.js new file mode 100644 index 0000000..dd224f5 --- /dev/null +++ b/macropad-relay/public/login.js @@ -0,0 +1,91 @@ +// MacroPad login page logic (externalized so the CSP can drop 'unsafe-inline' +// for scripts). +const sessionId = window.location.pathname.split('/')[1]; +const form = document.getElementById('loginForm'); +const passwordInput = document.getElementById('password'); +const rememberCheckbox = document.getElementById('remember'); +const submitBtn = document.getElementById('submitBtn'); +const errorDiv = document.getElementById('error'); +const statusDiv = document.getElementById('status'); + +let desktopConnected = false; + +// Check for saved password (session first, then "remembered" store) +const savedPassword = sessionStorage.getItem(`macropad_${sessionId}`) + || localStorage.getItem(`macropad_${sessionId}`); +if (savedPassword) { + passwordInput.value = savedPassword; +} + +// Connect to WebSocket to check desktop status +function checkStatus() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const ws = new WebSocket(`${protocol}//${window.location.host}/${sessionId}/ws`); + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.type === 'desktop_status') { + desktopConnected = data.status === 'connected'; + updateStatus(); + } + }; + + ws.onerror = () => { + statusDiv.textContent = 'Connection error'; + statusDiv.className = 'status disconnected'; + }; + + ws.onclose = () => { + setTimeout(checkStatus, 5000); + }; +} + +function updateStatus() { + if (desktopConnected) { + statusDiv.textContent = 'Desktop connected'; + statusDiv.className = 'status connected'; + submitBtn.disabled = false; + } else { + statusDiv.textContent = 'Desktop not connected'; + statusDiv.className = 'status disconnected'; + submitBtn.disabled = true; + } +} + +form.addEventListener('submit', async (e) => { + e.preventDefault(); + errorDiv.style.display = 'none'; + + const password = passwordInput.value; + + try { + // Test password with a simple API call + const response = await fetch(`/${sessionId}/api/tabs`, { + headers: { + 'X-MacroPad-Password': password + } + }); + + if (response.ok) { + // Hand the credential to the app via storage (never the URL). + sessionStorage.setItem(`macropad_${sessionId}`, password); + if (rememberCheckbox.checked) { + localStorage.setItem(`macropad_${sessionId}`, password); + } else { + localStorage.removeItem(`macropad_${sessionId}`); + } + + // Redirect to the PWA (credential is read from storage). + window.location.href = `/${sessionId}/app`; + } else { + const data = await response.json(); + errorDiv.textContent = data.error || 'Invalid password'; + errorDiv.style.display = 'block'; + } + } catch (error) { + errorDiv.textContent = 'Connection failed'; + errorDiv.style.display = 'block'; + } +}); + +checkStatus(); diff --git a/macropad-relay/src/config.ts b/macropad-relay/src/config.ts index 15fbe60..5669d5a 100644 --- a/macropad-relay/src/config.ts +++ b/macropad-relay/src/config.ts @@ -5,6 +5,14 @@ import path from 'path'; dotenv.config(); +function parseTrustProxy(): boolean | number | string { + const v = process.env.TRUST_PROXY ?? '1'; + if (v === 'false') return false; + if (v === 'true') return true; + const n = Number(v); + return Number.isNaN(n) ? v : n; +} + export const config = { port: parseInt(process.env.PORT || '3000', 10), host: process.env.HOST || '0.0.0.0', @@ -13,19 +21,47 @@ export const config = { dataDir: process.env.DATA_DIR || path.join(__dirname, '..', 'data'), // Session settings - sessionIdLength: parseInt(process.env.SESSION_ID_LENGTH || '6', 10), + sessionIdLength: parseInt(process.env.SESSION_ID_LENGTH || '12', 10), + + // Maximum number of sessions that may exist at once + maxSessions: parseInt(process.env.MAX_SESSIONS || '1000', 10), + + // Minimum password length enforced when creating a new session + minPasswordLength: parseInt(process.env.MIN_PASSWORD_LENGTH || '6', 10), + + // Sessions unused for this many days are pruned + sessionTtlDays: parseInt(process.env.SESSION_TTL_DAYS || '90', 10), // Security bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS || '10', 10), - // Rate limiting + // Rate limiting (HTTP) rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), // 15 minutes - rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '100', 10), + rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '300', 10), + + // WebSocket upgrade rate limiting (per IP) + wsUpgradeWindowMs: parseInt(process.env.WS_UPGRADE_WINDOW_MS || '60000', 10), // 1 minute + wsUpgradeMax: parseInt(process.env.WS_UPGRADE_MAX || '60', 10), // max upgrades per IP per window + + // Auth brute-force protection + authMaxSocketFailures: parseInt(process.env.AUTH_MAX_SOCKET_FAILURES || '5', 10), + authLockoutMaxAttempts: parseInt(process.env.AUTH_LOCKOUT_MAX_ATTEMPTS || '10', 10), + authLockoutWindowMs: parseInt(process.env.AUTH_LOCKOUT_WINDOW_MS || '900000', 10), // 15 minutes + authLockoutDurationMs: parseInt(process.env.AUTH_LOCKOUT_DURATION_MS || '900000', 10), // 15 minutes // WebSocket pingInterval: parseInt(process.env.PING_INTERVAL || '30000', 10), // 30 seconds requestTimeout: parseInt(process.env.REQUEST_TIMEOUT || '30000', 10), // 30 seconds + // Origin / CORS allowlist (comma-separated). Empty = same-host only. + allowedOrigins: (process.env.ALLOWED_ORIGINS || '') + .split(',') + .map((o) => o.trim()) + .filter((o) => o.length > 0), + + // Trust the X-Forwarded-* headers from a reverse proxy + trustProxy: parseTrustProxy(), + // Logging logLevel: process.env.LOG_LEVEL || 'info', diff --git a/macropad-relay/src/handlers/apiProxy.ts b/macropad-relay/src/handlers/apiProxy.ts index 5acb9d1..db8a070 100644 --- a/macropad-relay/src/handlers/apiProxy.ts +++ b/macropad-relay/src/handlers/apiProxy.ts @@ -12,26 +12,22 @@ export function createApiProxy( return async (req: Request, res: Response, next: NextFunction) => { const sessionId = req.params.sessionId; - // Check session exists - const session = sessionManager.getSession(sessionId); - if (!session) { - return res.status(404).json({ error: 'Session not found' }); + // Credentials must be supplied via the header (never the URL/query, + // which leaks into logs, history and referrers). + const password = req.headers['x-macropad-password'] as string; + + // Uniform auth failure. A missing session, a missing password, and a + // wrong password all return the SAME status + generic message so a + // caller cannot infer whether a given session id exists (no + // enumeration oracle). validatePassword() safely returns false for an + // unknown session id. + if (!password || !(await sessionManager.validatePassword(sessionId, password))) { + return res.status(401).json({ error: 'Authentication failed' }); } - // Check password in header or query - const password = req.headers['x-macropad-password'] as string || - req.query.password as string; - - if (!password) { - return res.status(401).json({ error: 'Password required' }); - } - - const valid = await sessionManager.validatePassword(sessionId, password); - if (!valid) { - return res.status(401).json({ error: 'Invalid password' }); - } - - // Check desktop is connected + // Only AFTER successful auth do we reveal desktop connectivity, so the + // 503 "not connected" signal cannot itself be used to probe which + // session ids exist. const desktop = connectionManager.getDesktopBySessionId(sessionId); if (!desktop) { return res.status(503).json({ error: 'Desktop not connected' }); diff --git a/macropad-relay/src/handlers/desktopHandler.ts b/macropad-relay/src/handlers/desktopHandler.ts index 39f5880..f7802fa 100644 --- a/macropad-relay/src/handlers/desktopHandler.ts +++ b/macropad-relay/src/handlers/desktopHandler.ts @@ -4,6 +4,11 @@ import WebSocket from 'ws'; import { ConnectionManager } from '../services/ConnectionManager'; import { SessionManager } from '../services/SessionManager'; import { logger } from '../utils/logger'; +import { config } from '../config'; +import { getLockoutRemaining, recordFailure, recordSuccess } from '../utils/authThrottle'; + +// Close code 1008 = policy violation (used for auth abuse). +const WS_POLICY_VIOLATION = 1008; interface AuthMessage { type: 'auth'; @@ -32,20 +37,33 @@ type DesktopMessage = AuthMessage | ApiResponseMessage | WsBroadcastMessage | Po export function handleDesktopConnection( socket: WebSocket, connectionManager: ConnectionManager, - sessionManager: SessionManager + sessionManager: SessionManager, + clientIp: string ): void { let authenticatedSessionId: string | null = null; + // Track failed auth attempts on this individual socket. + let socketAuthFailures = 0; socket.on('message', async (data) => { try { const message: DesktopMessage = JSON.parse(data.toString()); switch (message.type) { - case 'auth': - await handleAuth(socket, message, sessionManager, connectionManager, (sessionId) => { + case 'auth': { + const failed = await handleAuth(socket, message, sessionManager, connectionManager, clientIp, (sessionId) => { authenticatedSessionId = sessionId; }); + if (failed) { + socketAuthFailures++; + if (socketAuthFailures >= config.authMaxSocketFailures) { + logger.warn('Desktop exceeded auth attempts, closing socket'); + socket.close(WS_POLICY_VIOLATION, 'Too many failed attempts'); + } + } else { + socketAuthFailures = 0; + } break; + } case 'api_response': if (authenticatedSessionId) { @@ -87,32 +105,53 @@ export function handleDesktopConnection( }); } +// Returns true if the auth attempt failed (so the caller can count it). async function handleAuth( socket: WebSocket, message: AuthMessage, sessionManager: SessionManager, connectionManager: ConnectionManager, + clientIp: string, setSessionId: (id: string) => void -): Promise { +): Promise { try { let sessionId = message.sessionId; let session; if (sessionId) { + // Namespace the brute-force lockout key to the desktop keyspace and + // include the client IP. This keeps the desktop's lockout state entirely + // separate from the web-client keyspace, so a web-auth flood can never + // lock the desktop out of (re)authenticating. + const throttleKey = `desktop:${sessionId}:${clientIp}`; + + // Reject early if this key is currently locked out. + const lockRemaining = getLockoutRemaining(throttleKey); + if (lockRemaining > 0) { + socket.send(JSON.stringify({ + type: 'auth_response', + success: false, + error: 'Too many failed attempts, try again later', + retryAfterMs: lockRemaining + })); + return true; + } + // Validate existing session const valid = await sessionManager.validatePassword(sessionId, message.password); if (!valid) { + recordFailure(throttleKey); socket.send(JSON.stringify({ type: 'auth_response', success: false, error: 'Invalid session ID or password' })); - socket.close(); - return; + return true; } + recordSuccess(throttleKey); session = sessionManager.getSession(sessionId); } else { - // Create new session + // Create new session (may throw on limit / short password) session = await sessionManager.createSession(message.password); sessionId = session.id; } @@ -123,8 +162,7 @@ async function handleAuth( success: false, error: 'Failed to create session' })); - socket.close(); - return; + return true; } // Add to connection manager @@ -139,14 +177,15 @@ async function handleAuth( })); logger.info(`Desktop authenticated: ${sessionId}`); - } catch (error) { + return false; + } catch (error: any) { logger.error('Desktop auth error:', error); socket.send(JSON.stringify({ type: 'auth_response', success: false, - error: 'Authentication failed' + error: error?.message || 'Authentication failed' })); - socket.close(); + return true; } } diff --git a/macropad-relay/src/handlers/webClientHandler.ts b/macropad-relay/src/handlers/webClientHandler.ts index e078d32..bfedd71 100644 --- a/macropad-relay/src/handlers/webClientHandler.ts +++ b/macropad-relay/src/handlers/webClientHandler.ts @@ -4,6 +4,11 @@ import WebSocket from 'ws'; import { ConnectionManager, WebClientConnection } from '../services/ConnectionManager'; import { SessionManager } from '../services/SessionManager'; import { logger } from '../utils/logger'; +import { config } from '../config'; +import { getLockoutRemaining, recordFailure, recordSuccess } from '../utils/authThrottle'; + +// Close code 1008 = policy violation (used for auth abuse). +const WS_POLICY_VIOLATION = 1008; interface AuthMessage { type: 'auth'; @@ -20,30 +25,26 @@ export function handleWebClientConnection( socket: WebSocket, sessionId: string, connectionManager: ConnectionManager, - sessionManager: SessionManager + sessionManager: SessionManager, + clientIp: string ): void { - // Check if session exists - const session = sessionManager.getSession(sessionId); - if (!session) { - socket.send(JSON.stringify({ - type: 'error', - error: 'Session not found' - })); - socket.close(); - return; - } + // Namespace the brute-force lockout key to the web keyspace and include the + // client IP. This prevents one abusive IP (which knows the shareable session + // id) from locking out other web clients or the desktop for this session. + const throttleKey = `web:${sessionId}:${clientIp}`; - // Add client (not authenticated yet) + // Do NOT reveal whether the session exists. A non-existent session id and + // an existing one must be indistinguishable to an unauthenticated client so + // session ids cannot be enumerated: we always run the same handshake and let + // the (uniform) auth response be the only signal. validatePassword() safely + // returns false for an unknown session id, and desktop connectivity is only + // disclosed AFTER a successful auth (see the 'auth' case below). const client = connectionManager.addWebClient(sessionId, socket, false); - // Check if desktop is connected - const desktop = connectionManager.getDesktopBySessionId(sessionId); - socket.send(JSON.stringify({ - type: 'desktop_status', - status: desktop ? 'connected' : 'disconnected' - })); + // Track failed auth attempts on this individual socket. + let socketAuthFailures = 0; - // Request authentication + // Request authentication. socket.send(JSON.stringify({ type: 'auth_required' })); @@ -53,9 +54,25 @@ export function handleWebClientConnection( const message: WebClientMessage = JSON.parse(data.toString()); switch (message.type) { - case 'auth': - await handleAuth(socket, client, message, sessionId, sessionManager); + case 'auth': { + const failed = await handleAuth(socket, client, message, sessionId, sessionManager, throttleKey); + if (failed) { + socketAuthFailures++; + if (socketAuthFailures >= config.authMaxSocketFailures) { + logger.warn(`Web client exceeded auth attempts for session ${sessionId}, closing socket`); + socket.close(WS_POLICY_VIOLATION, 'Too many failed attempts'); + } + } else { + socketAuthFailures = 0; + // Authenticated: only now do we disclose desktop connectivity. + const desktop = connectionManager.getDesktopBySessionId(sessionId); + socket.send(JSON.stringify({ + type: 'desktop_status', + status: desktop ? 'connected' : 'disconnected' + })); + } break; + } case 'ping': socket.send(JSON.stringify({ type: 'pong' })); @@ -82,28 +99,46 @@ export function handleWebClientConnection( }); } +// Returns true if the auth attempt failed (so the caller can count it). async function handleAuth( socket: WebSocket, client: WebClientConnection, message: AuthMessage, sessionId: string, - sessionManager: SessionManager -): Promise { + sessionManager: SessionManager, + throttleKey: string +): Promise { + // Reject early if this key is currently locked out. + const lockRemaining = getLockoutRemaining(throttleKey); + if (lockRemaining > 0) { + socket.send(JSON.stringify({ + type: 'auth_response', + success: false, + error: 'Too many failed attempts, try again later', + retryAfterMs: lockRemaining + })); + return true; + } + const valid = await sessionManager.validatePassword(sessionId, message.password); if (valid) { + recordSuccess(throttleKey); client.authenticated = true; socket.send(JSON.stringify({ type: 'auth_response', success: true })); logger.debug(`Web client authenticated for session: ${sessionId}`); + return false; } else { + recordFailure(throttleKey); socket.send(JSON.stringify({ type: 'auth_response', success: false, - error: 'Invalid password' + error: 'Authentication failed' })); + return true; } } diff --git a/macropad-relay/src/server.ts b/macropad-relay/src/server.ts index b0f94d1..a576bfb 100644 --- a/macropad-relay/src/server.ts +++ b/macropad-relay/src/server.ts @@ -3,7 +3,7 @@ import express from 'express'; import fs from 'fs'; import http from 'http'; -import WebSocket, { WebSocketServer } from 'ws'; +import { WebSocketServer } from 'ws'; import cors from 'cors'; import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; @@ -17,19 +17,104 @@ import { handleDesktopConnection } from './handlers/desktopHandler'; import { handleWebClientConnection } from './handlers/webClientHandler'; import { createApiProxy } from './handlers/apiProxy'; +// Resolve the client IP for an upgrade request, honouring the proxy's +// X-Forwarded-For first hop when we trust the proxy. +function getClientIp(request: http.IncomingMessage): string { + if (config.trustProxy) { + const xff = request.headers['x-forwarded-for']; + if (typeof xff === 'string' && xff.length > 0) { + return xff.split(',')[0].trim(); + } + if (Array.isArray(xff) && xff.length > 0) { + return xff[0].split(',')[0].trim(); + } + } + return request.socket.remoteAddress || 'unknown'; +} + +// Simple in-memory sliding-window rate limiter for WS upgrades, keyed by IP. +function createUpgradeRateLimiter() { + const hits = new Map(); + + const cleanup = setInterval(() => { + const cutoff = Date.now() - config.wsUpgradeWindowMs; + for (const [ip, times] of hits) { + const recent = times.filter((t) => t > cutoff); + if (recent.length === 0) { + hits.delete(ip); + } else { + hits.set(ip, recent); + } + } + }, config.wsUpgradeWindowMs); + if (typeof cleanup.unref === 'function') cleanup.unref(); + + return function allow(ip: string): boolean { + const now = Date.now(); + const cutoff = now - config.wsUpgradeWindowMs; + const times = (hits.get(ip) || []).filter((t) => t > cutoff); + times.push(now); + hits.set(ip, times); + return times.length <= config.wsUpgradeMax; + }; +} + +// Validate the Origin header for a WS upgrade against the allowlist. +// Native (desktop) clients send no Origin and are permitted. +function isOriginAllowed(origin: string | undefined, host: string | undefined): boolean { + if (!origin) return true; // non-browser clients (e.g. desktop app) + + if (config.allowedOrigins.length > 0) { + return config.allowedOrigins.includes(origin); + } + + // Default: allow only same-host origins. + try { + return new URL(origin).host === host; + } catch { + return false; + } +} + export function createServer() { const app = express(); const server = http.createServer(app); + // Trust the reverse proxy so client IPs (rate limiting) are accurate. + app.set('trust proxy', config.trustProxy); + // Initialize managers const sessionManager = new SessionManager(); const connectionManager = new ConnectionManager(); // Middleware app.use(helmet({ - contentSecurityPolicy: false // Allow inline scripts for login page + // Page scripts are served as external files (/static/app.js, + // /static/login.js) with no inline handlers, so scripts are restricted to + // 'self' (no 'unsafe-inline'). The pages still use inline