From e3cb1f82af12778519d8248b20085907d2e071ac Mon Sep 17 00:00:00 2001 From: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:55:23 -0500 Subject: [PATCH] v2.1.0 (#711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: enhance terminal theme preview and persistence (#637) - Refactor Terminal.tsx to optimize theme update logic and eliminate flashes - Implement localStorage persistence for terminal themes per host - Fix hover preview to redraw buffer content instantly - Ensure theme preferences survive cookie clears Co-authored-by: Gemini CLI * fix: update darwin platform identifier to osx (#626) * feat: implement SSH algorithms mapping and refactor cipher usage across SSH modules (#627) * feat: enhance WebSocket connection handling for embedded mode (#628) * fix(auth): pass JWT token via URL param for Electron/mobile OIDC callback (#630) The OIDC callback redirect did not include the JWT token as a URL parameter for desktop/mobile device types, causing Electron and React Native webviews to have jwt = undefined after login. Closes Termix-SSH/Support#562 * fix: remove hardcoded version number from dashboard (#632) The version text was initialized to "v1.8.0" which displayed incorrect version on the dashboard before the API response. Changed to empty string so it shows nothing until the real version is fetched. Closes Termix-SSH/Support#550 * fix: admin role toggle showing incorrect state after update (#633) After successfully toggling admin status, onSuccess() closes the dialog and clears the user reference, but onOpenChange(true) then reopens the dialog with null user, causing isAdmin state to not sync properly. Removed the redundant dialog reopen after success - let onSuccess handle the cleanup normally. Closes Termix-SSH/Support#549 * fix: allow disabling password login when OIDC is configured via env vars (#634) The admin OIDC config endpoint only checked the database for OIDC configuration, ignoring environment variables. This caused the frontend to incorrectly block disabling password login when OIDC was configured via OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, etc. Now falls back to getOIDCConfigFromEnv() when no database config exists, matching the behavior of the public /oidc-config endpoint. Closes Termix-SSH/Support#561 * fix: sync snippet selected terminals count when tabs are closed (#635) selectedSnippetTabIds was not cleaned up when terminal tabs were closed, causing the snippet dialog to show stale terminal count. Added useEffect to filter out IDs of closed tabs. Closes Termix-SSH/Support#534 * feature: toggle history globally (#636) * Fix RDP audio output and dynamic session resize (#625) Co-authored-by: AllX * fix: check connection state before fallback exec in file manager (#644) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: check connection state before fallback exec in file manager When SFTP operations fail and tryFallbackMethod is called, the SSH client may already be disconnected. Calling client.exec() on a disconnected client throws an unhandled exception that crashes the backend process. Added connection state check at the start of all three tryFallbackMethod closures (listFiles, writeFile, uploadFile). Closes Termix-SSH/Support#451 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: restrict postMessage targetOrigin to prevent JWT leakage (#645) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: restrict postMessage targetOrigin to prevent JWT leakage Multiple postMessage calls used wildcard "*" as targetOrigin, allowing any parent window to intercept JWT tokens if Termix is embedded in an iframe. Changes: - main-axios.ts: Only send postMessage in Electron iframe context (added isElectron() check), use window.location.origin as target - Auth.tsx: Replace "*" with window.location.origin for all three AUTH_SUCCESS postMessage calls (already gated by isInElectronWebView) - ElectronLoginForm.tsx: Use server URL origin for iframe postMessage, fall back to "*" only if origin parsing fails --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: stats monitoring resolves SSH key from credential privateKey field (#643) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: stats monitoring resolves SSH key from credential privateKey field When loading credentials for status monitoring, only the `key` field was checked but not `privateKey`. The ssh_credentials table has both fields and some credentials store the key in `privateKey`. This caused stats polling to fail with auth errors for key-based credentials. Closes Termix-SSH/Support#429 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * feat: add collapse/expand all button for host manager folders (#642) * Update sha256 value for v2.0.0 universal dmg (#629) * feat: add collapse/expand all button for host manager folders All folders were always auto-expanded with no way to collapse them all at once. Added a toggle button in the toolbar that collapses or expands all folder accordions. Icon switches between ChevronsDownUp (collapse) and ChevronsUpDown (expand) to indicate current action. Closes Termix-SSH/Support#488 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: prevent invalid SSH key from crashing stats polling loop (#640) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: prevent invalid SSH key from crashing stats polling loop Two fixes: 1. Add .catch() to pollHostMetrics() call inside setInterval to prevent unhandled promise rejections from crashing the process 2. Add "Invalid SSH key format" to the auth failure error patterns in collectMetrics so it's properly tracked instead of re-thrown Closes Termix-SSH/Support#478 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: revoke all sessions when password is changed or reset (#647) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: revoke all sessions when password is changed or reset logoutUser() without sessionId only cleared in-memory crypto state but did not delete session records from the database. This meant old JWT tokens remained valid after password change/reset. Now deletes all session records for the user when no specific sessionId is provided, which is the code path used by password reset and password change handlers. --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: isolate RDP keyboard input to active tab (#663) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: disable RDP keyboard input when tab is not visible --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * Fix clipboard paste browser popup (#667) * feat: switch to adjacent tab when closing current tab (#661) * Update sha256 value for v2.0.0 universal dmg (#629) * feat: switch to adjacent tab when closing current tab Previously closing the current tab always switched to the first remaining tab (often Dashboard). Now switches to the adjacent tab — the next one in order, or the previous if the closed tab was last. This matches the tab-close behavior of browsers and IDEs. Closes Termix-SSH/Support#606 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: add auth token to database export/import in Electron app (#664) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: add Bearer token to database export/import requests in Electron --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * Fix WebSocket reconnection and add connection lost overlay (#668) * fix: skip metrics collection for hosts with authType none or opkssh (#639) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: skip metrics collection for hosts with authType none or opkssh supportsMetrics() only checked connectionType but ignored authType. Hosts configured with Authentication: None (e.g. Tailscale SSH) or opkssh would trigger SSH metrics polling, causing repeated auth failures since no credentials are available. Closes Termix-SSH/Support#515 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: align cookie maxAge with JWT expiration to prevent early logout (#658) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: align cookie maxAge with JWT expiration to prevent early logout The JWT cookie maxAge for regular (non-rememberMe) logins was set to 2 hours, while the JWT token itself was valid for 24 hours. After 2 hours the cookie expired and the user was logged out, even during active SSH sessions. Changed cookie maxAge from 2h to 24h for regular logins to match the JWT expiration. Affects both password login and OIDC login paths. Closes Termix-SSH/Support#595 Closes Termix-SSH/Support#583 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: remove sensitive data from log output (#649) - Password reset: stop logging the 6-digit reset code in plaintext. The code is still stored in the settings table for retrieval. - Password reset: return identical response for non-existent users and OIDC users to prevent username enumeration. - OPKSSH callback: remove URL, query params, and forwarded headers from log output to prevent token/code leakage. * feat: display file owner and group in file manager list view (#654) * Update sha256 value for v2.0.0 universal dmg (#629) * feat: display file owner and group in file manager list view Added an Owner column to the file manager list view showing owner:group for each file. The data was already returned by the backend (SFTP returns uid/gid, ls fallback returns usernames) but not displayed. Column is hidden on small screens (md:block) to avoid crowding. Closes Termix-SSH/Support#603 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: prevent file manager from showing stale directory contents (#655) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: prevent file manager from showing stale directory contents Two issues caused the file browser to get stuck showing outdated directory contents after folder operations: 1. handleRefreshDirectory could be blocked by a lingering isLoading state from a previous request. Now force-resets loading state before initiating the refresh. 2. debouncedLoadDirectory skipped requests when the path hadn't changed (path === lastPathChangeRef), but after create/move/delete operations the path stays the same while contents change. Added force parameter to bypass the path equality check. Closes Termix-SSH/Support#599 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: fallback to default layout when dashboard preferences lack cards (#652) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: fallback to default layout when dashboard preferences lack cards getDashboardPreferences may return null, empty object, or an object without a cards array (e.g. when behind a reverse proxy that alters the response, or on first load for a new user). This caused layout.cards.filter() to throw, leaving the dashboard as a black screen after login. Now validates that the response has a cards array before using it, falling back to DEFAULT_LAYOUT otherwise. --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: bind SSH sessions to userId and verify ownership on access (#650) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: bind SSH sessions to userId and verify ownership on access SSHSession objects in file-manager and docker did not store userId, allowing any authenticated user to operate on another user's session if they knew the sessionId. Changes: - Added userId field to SSHSession interface in both modules - Store userId when creating sessions (connect, TOTP, Warpgate paths) - Added verifySessionOwnership() helper in file-manager - Applied ownership checks to sudo-password, status, keepalive, listFiles endpoints in file-manager - Applied ownership check to keepalive endpoint in docker - Session creation in docker now stores userId in all 3 paths --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: use cookie-based auth for WebSocket instead of URL token (#646) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: use cookie-based auth for WebSocket instead of URL token JWT tokens in WebSocket URL query strings are exposed in nginx access logs, browser history, and proxy logs. Backend: terminal and docker-console WebSocket servers now read JWT from the cookie header as fallback when no URL token is provided. Frontend: desktop terminal and docker console no longer append token to WebSocket URL, relying on cookies sent automatically by the browser. Mobile and Guacamole WebSocket connections are unchanged as they may not have cookie access. --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: prevent long Docker container names from overflowing card bounds (#653) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: prevent long Docker container names from overflowing card bounds Container names were not constrained to the card width, causing long names to overlay adjacent container cards. Added overflow-hidden and min-w-0 to the Card root element so the existing truncate class on CardTitle takes effect within the grid layout. Closes Termix-SSH/Support#601 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: preserve original timestamps in SSH login statistics (#657) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: preserve original timestamps in SSH login statistics Failed login attempts showed the current time instead of the actual attempt time. Two issues: 1. auth.log dates (e.g. "Mar 15 10:23:45") were parsed with a format that could fail on some platforms, falling back to new Date() which gives the current time. Changed to a more reliable format ("Mar 15, 2026 10:23:45") and fall back to the raw string instead of the current time. 2. Dates from previous years (e.g. December logs viewed in January) were assigned the current year, producing future dates. Now checks if the parsed date is in the future and subtracts a year. Also fixed the same fallback issue for successful login timestamps. Closes Termix-SSH/Support#570 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: remove plaintext credentials from internal host API responses (#651) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: remove plaintext credentials from internal host API responses /db/host/internal and /db/host/internal/all returned password, key, keyPassword, and autostart credentials in plaintext, protected only by a static INTERNAL_AUTH_TOKEN. If the token leaked, all SSH credentials would be exposed. Changes: - Stripped password, key, keyPassword, autostartPassword, autostartKey, autostartKeyPassword from both internal API responses - Only return hostId, userId, and non-sensitive connection metadata - Updated tunnel.ts endpoint resolution to use resolveHostById() for credentials instead of reading from HTTP response - Autostart tunnel initialization no longer receives credentials from internal API, relying on server-side DB resolution at connect time --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: default keyType to auto instead of blocking host update (#641) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: default keyType to auto instead of blocking host update When editing a host with key authentication, missing keyType value caused form validation to fail silently, preventing the Update Host button from saving changes. Now defaults keyType to "auto" instead of raising a validation error. Closes Termix-SSH/Support#510 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: downgrade credential migration errors to warnings (#659) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: downgrade credential migration errors to warnings Credential migration failures during login (e.g. corrupted encrypted data from older versions) were logged at ERROR level, causing alarm in Docker logs. The migration is non-blocking — login succeeds regardless — so these should be warnings. Changed individual credential decryption failures and overall migration failures from error to warn level. Also improved log messages to be more descriptive. Closes Termix-SSH/Support#541 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * feat: add Select All / Deselect All buttons for snippet terminal selection (#660) * Update sha256 value for v2.0.0 universal dmg (#629) * feat: add Select All / Deselect All buttons for snippet terminal selection When running snippets on many terminals, users had to click each terminal individually. Added Select All and Deselect All buttons above the terminal list for batch selection. Closes Termix-SSH/Support#535 --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: admin user list not reading OIDC and admin status correctly (#665) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: align admin user list field names with API response (camelCase) --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: enable clipboard paste from host to RDP session (#666) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: sync host clipboard to RDP session on tab focus and mouse enter --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * fix: validate containerId and timestamp params to prevent command injection (#648) * Update sha256 value for v2.0.0 universal dmg (#629) * fix: validate containerId and timestamp params to prevent command injection Docker API endpoints passed containerId, since, and until parameters directly into shell commands via SSH exec without validation. An authenticated user with Docker access could inject arbitrary shell commands on the remote host. Added Express param middleware to validate containerId against ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ for all 9 endpoints. Also validate since/until timestamps in the logs endpoint against a strict regex. --------- Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> * Merge commit from fork * Merge commit from fork Replace double-quoted shell string interpolation with single-quoted escaping in extractArchive and compressFiles endpoints. Double quotes allow $(command) substitution, enabling arbitrary command execution on the remote SSH host via crafted archive paths or file names. Now uses the same single-quote escaping pattern used by all other file manager operations in this file. * Merge commit from fork CORS: Replace permissive origin checks (any http/https) across all 6 microservices with a shared cors-config module that only allows: - Same-origin requests (derived from Host header) - Configured origins via CORS_ALLOWED_ORIGINS env var - Dev origins (localhost:5173) Docker console: Validate containerId against ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ and restrict shell to allowlist [bash, sh, ash, zsh] to prevent command injection via WebSocket messages. * Merge commit from fork * refactor: add shared host-resolver for server-side credential resolution Creates resolveHostById() utility that loads a host from DB and resolves its credentials entirely server-side. This will be used by connection modules to avoid receiving credentials from the frontend. Also adds checkHostAccess() for permission validation. * fix: strip sensitive credentials from host API responses Remove password, key, keyPassword, sudoPassword, and other credential fields from GET /db/host and GET /db/host/:id responses. Add boolean indicators (hasPassword, hasKey, hasSudoPassword) so the frontend knows capabilities without seeing actual values. Add GET /db/host/:id/password endpoint for the copy-password feature to fetch a specific password on demand. * refactor: docker-console resolves credentials server-side by hostId Instead of receiving the full hostConfig with credentials from the frontend WebSocket message, docker-console now extracts hostId and uses resolveHostById() to load credentials from the database. Also validates containerId format and restricts shell to allowlist. * feat: add getHostPassword API and update copy-password to use it Add getHostPassword() frontend function that calls the new server-side password endpoint instead of reading from the host object. Update Tab component to use boolean indicators (hasPassword, hasKey, hasSudoPassword) from the sanitized API response, with backward compatibility for the old response format. Add boolean indicator fields to Host type definition. * refactor: file-manager resolves credentials server-side via host-resolver When frontend doesn't provide password/sshKey (due to API stripping), file-manager now uses resolveHostById() to load credentials from DB. Falls back to provided credentials for backward compatibility. * refactor: terminal resolves credentials server-side via host-resolver When frontend doesn't provide password/key (due to API stripping), terminal now uses resolveHostById() to load credentials from DB. Preserves backward compatibility with reconnect_with_credentials where user provides credentials interactively. * refactor: tunnel resolves source credentials server-side via host-resolver When frontend doesn't provide sourcePassword/sourceSSHKey (due to API stripping), tunnel now uses resolveHostById() to load credentials from DB for both the connect and cleanup paths. * fix: terminal sudo auto-fill fetches password from server on demand After credentials are stripped from API responses, hostConfig.password is no longer available. Sudo auto-fill now checks boolean indicators to show the prompt, then fetches the actual password via getHostPassword API only when the user confirms the auto-fill action. * fix: host editor fetches full credentials via export API for editing After credentials are stripped from the host list API, the editor would show empty password/key fields. Now uses exportSSHHostWithCredentials() to fetch the full host data with credentials when opening the editor. Applies to all paths: direct edit, sidebar click, and external navigation. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: update jsdoc comments for /host instead of /ssh * fix: align OIDC login cookie maxAge with JWT expiration (2h → 24h) (#671) * fix: persist OIDC JWT token to localStorage in Electron app (#672) * fix: add error toast for empty file download and remove stray prop in tab bar (#674) * fix: prevent server status failure from blocking host list loading (#673) * fix: show server config dialog on first launch instead of auto-selecting embedded (#675) * fix: remove unnecessary registration disabled toast on login page (#670) * fix: add clipboard fallback and toast feedback for Copy Password button (#669) Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: allow file origin for packaged Electron desktop app (#676) * Add AWS logo to README * fix: allow file origin for packaged Electron desktop app --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: backend compliation errors * feat: remove theme selector from nav bar * fix: validate and fallback credentialId during JSON host bulk import (#677) * Add AWS logo to README * fix: validate and fallback credentialId during JSON host bulk import --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: await tunnel cleanup to prevent new connection from being killed (#678) * Add AWS logo to README * fix: await tunnel cleanup before creating new connection to prevent race condition --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: disable keyboard-interactive when host auth is set to None (#682) * Add AWS logo to README * fix: disable keyboard-interactive auth when host authType is none --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: use carriage return for mobile startup snippet execution (#680) * Add AWS logo to README * fix: use carriage return for mobile startup snippet execution --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: prevent file upload from crashing backend on permission denied (#681) * Add AWS logo to README * fix: add stderr error handlers and connection check to prevent upload crash --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: add missing stream error handlers in Docker console (#684) * Add AWS logo to README * fix: add missing stream error handlers in Docker console to prevent crashes --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: use carriage return for snippet execution to support PowerShell (#679) * Add AWS logo to README * fix: use carriage return instead of line feed for snippet and command execution --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: restrict remaining postMessage targetOrigin from wildcard to origin (#685) * Add AWS logo to README * fix: restrict postMessage targetOrigin to prevent JWT token leakage --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add SESSION_TIMEOUT_HOURS environment variable for customizable session duration (#662) * feat: add SESSION_TIMEOUT_HOURS environment variable for session duration Session timeout was hardcoded to 24h (JWT) and 2h (cookie). Now both are configurable via SESSION_TIMEOUT_HOURS env var (default: 24). Set in docker-compose.yml: environment: SESSION_TIMEOUT_HOURS: "72" Also fixes the cookie maxAge mismatch (was 2h, now matches JWT). Remember Me sessions remain at 30 days regardless of this setting. Closes Termix-SSH/Support#609 Closes Termix-SSH/Support#595 * refactor: move session timeout from env var to Admin Settings Replace SESSION_TIMEOUT_HOURS environment variable with a database-backed setting configurable from Admin Settings UI. Default remains 24 hours. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add port knocking support for SSH connections (#694) * Add AWS logo to README * feat: add port knocking support for SSH connections Send TCP/UDP knock packets to a configurable port sequence before establishing SSH connections. Configured per-host in the host editor under a new Port Knocking accordion section. Supports custom protocol (TCP/UDP) and delay between knocks. Knocking failures don't block the connection attempt. Closes Termix-SSH/Support#524 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Wake-on-LAN support for hosts (#696) * Add AWS logo to README * feat: add Wake-on-LAN support for hosts --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add export all hosts as JSON (#688) * Add AWS logo to README * feat: add export all hosts as JSON Add GET /ssh/db/hosts/export endpoint and Export All button in the host manager toolbar. Exported format is compatible with existing bulk import. Includes sensitive data warning confirmation before download. Closes Termix-SSH/Support#582 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add snippet sharing with users and roles (#691) * Add AWS logo to README * feat: add snippet sharing with users and roles Add snippetAccess table and RBAC routes for sharing snippets, following the same pattern as host sharing. Users can share snippets with other users or roles via a share dialog. Shared snippets appear in a dedicated section in the snippets sidebar as read-only with copy support. Closes Termix-SSH/Support#474 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: auth errors and ws connection errors in dev env * feat: add opt-in tmux integration for persistent terminal sessions (#683) * Add backend tmux integration with native scrollback Detect tmux on remote hosts via SSH exec channel, auto-attach or create sessions with mouse mode, history-limit 50000, set-clipboard on, and allow-passthrough on for native scrollback, OSC 52 clipboard sync, and safe paste handling. Use && exit so the shell only closes if tmux started successfully. Query session name after auto-creation. * Add frontend tmux session handling and picker dialog Desktop: handle tmux WebSocket messages, show session picker with window count, attached clients, and last activity when multiple sessions exist. Toast warning when Auto-tmux is enabled but tmux is missing on remote. Mobile: auto-attach to first available session. All user-facing strings are localized via i18n. * Add Auto-tmux toggle in host settings and i18n strings Per-host opt-in toggle following the existing autoMosh pattern. English i18n strings for all tmux-related UI elements. * Show a toast hint on first drag inside a tmux session When the user drags the mouse inside a tmux-wrapped terminal, show a localized toast ("Adjust selection and press Enter to copy") once per tab session. Purely frontend so the hint is i18n-ready and doesn't pollute the tmux status bar. * chore: increment ver * feat: add right-click context menu in terminal to open file manager (#695) * Add AWS logo to README * feat: add right-click context menu in terminal to open file manager --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus * Fix/desktop guac connect flow (#687) * fix: use direct guacamole websocket port in embedded electron mode * Fix desktop remote token flow for redacted hosts --------- Co-authored-by: LukeGus Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: allow file:// origin in shared cors middleware (#686) Co-authored-by: LukeGus Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add command history toggle and sensitive command filtering (#693) * Add AWS logo to README * feat: add command history toggle and sensitive command filtering Add on/off toggle for command history recording in User Profile settings. Commands matching sensitive patterns (passwords, secrets, tokens, API keys) are automatically filtered on both frontend and backend, never stored. Closes Termix-SSH/Support#461 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus * OPKSSH proxy, certificate auth, and inline provider selection (#692) * fix: OPKSSH proxy integration for remote deployments Migrate proxy routes from /ssh/ to /host/ prefix. Use session's remote redirect URI for callback path instead of hardcoded /login-callback. Add OAuth callback fallback for external browser redirects with state parameter binding to prevent cross-session mixup. Reject cookie-less callbacks that can't be identified. * fix: implement OPKSSH certificate authentication for ssh2 Extract OPKSSH certificate auth into shared module that works around ssh2's lack of native certificate support: grafts cert blob onto parsed key, wraps ECDSA sign() for DER-to-SSH conversion, and patches Protocol.authPK for correct algorithm. Applied across terminal, file-manager, docker, and server-stats. Removes legacy temp file approach in favor of in-memory keys. * feat: inline OPKSSH provider selection in dialog Parse OIDC provider aliases and issuers from config.yml using js-yaml and send them to the frontend via the WebSocket message. The dialog renders a "Sign in with {Provider}" button per provider, opening the browser directly to the OAuth flow and skipping the external chooser page. Falls back to the existing "Open in Browser" behavior when providers aren't available. --------- Co-authored-by: LukeGus Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add configurable log level via Admin Settings (#690) * Add AWS logo to README * feat: add configurable log level via Admin Settings Add log verbosity control (debug/info/warn/error) through Admin Settings UI and LOG_LEVEL environment variable. Database setting takes precedence. Changes take effect immediately without restart. Closes Termix-SSH/Support#499 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus * feat: add reconnect button for disconnected SSH sessions (#689) * Add AWS logo to README * feat: add reconnect button for disconnected SSH sessions When an SSH connection drops, show a reconnect overlay instead of closing the tab. Users can click Reconnect to re-establish the connection or Close to dismiss. Also triggers after auto-reconnect attempts are exhausted. Closes Termix-SSH/Support#596 Closes Termix-SSH/Support#542 Closes Termix-SSH/Support#604 --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus * feat: fix port knocking and mac address not saving to backend * fix: fix snippets table not being created * fix: command history logic error, snippet sharing failing and improved UI for it * Reset stale trust state when TOTP is enabled (#697) * Add AWS logo to README * Reset stale trust state when TOTP is enabled Enablement now updates the user record, revokes existing sessions, clears trusted devices, and persists the result using the existing route flow. The change stays narrow and avoids introducing a one-off auth-manager wrapper or changing the save helper contract. Constraint: Keep the change close to the existing auth route and avoid extra abstractions Rejected: Keep the dedicated auth-manager helper | it was single-use and widened the surface area Confidence: high Scope-risk: narrow Directive: If this behavior changes again, keep the reset logic at the route boundary unless another caller appears Tested: tsc -p tsconfig.node.json --pretty false, git diff --check Not-tested: full frontend build --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix: version disabling in user profile not properly disabling and mac address not saving in host manager * feat: improved reconnect ui for terminals * feat: improve right click copy/paste * fix: some themes not including all the needed colors * chore: remove donate button * fix: schema errors, password logic errors, sswapped line order in host.ts * fix: cors being too strict * fix: passphrase erorr and tmux error * fix: guacd improvements, ui bugs, connection problems, etc * fix: shrink image, opkssh fixes, desktop ui changes * feat: dont require password to export and fixed export failures * feat: opkssh fixes, guacamole ui fixes, update readme for release * fix: tabs closing fast causiung no tab to be active and electron header persistance issue * fix: guacd params getting malformed * fix: desktop app header persistance * fix: desktop app header persistance * feat: desktop app not logging in * feat: improve okpkssh implementation and fix redirect uri bug * fix: opkssh redirect * fix: backend hang (ongoing) * fix: tunnels not being able to be saved * fix: c2s networking stability (activity/log, metrics, status) (#701) - /activity/log: the trim-over-100 path called SimpleDBOps.delete with a userId instead of a where clause and 500'd every call. Use inArray on the actual ids, best-effort (trim failures don't fail the log). - /metrics/register-viewer: now a graceful 200 no-op when the host can't be found, metrics are disabled, or the connection type doesn't support metrics. Any internal error is reported as skipped instead of a 500, and the fire-and-forget startMetricsForHost can no longer leak an unhandled rejection. - /metrics/:id: treat 404 as "no metrics yet / disabled" rather than an error. Dashboard skips hosts known to be offline before asking for metrics. - /status: retry with 2s/5s/8s timeouts and 3s/5s pauses (23s worst case, fits in the 30s poll cycle) before surfacing a network error; intermediate attempts stay silent. - Replace the blocking "connection lost" overlay with a persistent, non-dismissible toast ("Unstable server connection, recovering…") carrying a Reload action. Users keep full access to the UI; if they try to connect to a host and it fails, that's on them. The toast clears to the usual "Server connection restored" success toast on the next healthy API response. The toast triggers on any of ERR_NETWORK / ECONNREFUSED / ECONNABORTED / ECONNRESET / ETIMEDOUT / ERR_CANCELED, "Request aborted"/timeout messages, or database/drizzle/sqlite errors. * fix(guacamole): honor host RDP DPI in client and tab params (#703) * fix(file-manager): preserve remote file mode after SFTP write (#704) * fix(admin): target admin toggle APIs by user id (#705) * fix(terminal): resolve Electron SSH websocket URL from server config (#706) * fix(snippets): accept snippets or legacy updates in reorder API (#707) * fix(admin): fetch users list when users tab is opened (#708) * fix(docker): improve list layout and overflow for container cards (#709) * fix(guacamole): gate keyboard capture on focus and visibility (#710) * fix: remove snippets test file * chore: run linter * fix: increase macos memory for building * Potential fix for pull request finding 'Unused variable, import, function or class' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: Will Moore Co-authored-by: Gemini CLI Co-authored-by: Chakyiu <49145984+Chakyiu@users.noreply.github.com> Co-authored-by: Jozef Rebjak Co-authored-by: ZacharyZcR Co-authored-by: Daniel Quinan <68088383+DanielQuinan@users.noreply.github.com> Co-authored-by: allxm4 <77125344+allxm4@users.noreply.github.com> Co-authored-by: AllX Co-authored-by: Razvan Aurariu <38325118+rzv-me@users.noreply.github.com> Co-authored-by: Dylan Ysmal Co-authored-by: vvbbnn00 Co-authored-by: Lbubeer Co-authored-by: Dominik Co-authored-by: LukeGus Co-authored-by: TerrifiedBug <35064668+TerrifiedBug@users.noreply.github.com> Co-authored-by: JIHUN Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .github/workflows/electron.yml | 3 + README.md | 4 +- docker/compose.dev.yml | 33 + docker/docker-compose.yml | 2 +- electron/main.cjs | 43 + package-lock.json | 3914 ++++++++--------- package.json | 113 +- public/sw.js | 4 +- readme/README-AR.md | 66 +- readme/README-CN.md | 84 +- readme/README-DE.md | 58 +- readme/README-ES.md | 60 +- readme/README-FR.md | 66 +- readme/README-HI.md | 66 +- readme/README-IT.md | 60 +- readme/README-JA.md | 68 +- readme/README-KO.md | 66 +- readme/README-PT.md | 54 +- readme/README-RU.md | 60 +- readme/README-TR.md | 54 +- readme/README-VI.md | 54 +- src/backend/dashboard.ts | 89 +- src/backend/database/database.ts | 170 +- src/backend/database/db/index.ts | 32 +- src/backend/database/db/schema.ts | 27 + src/backend/database/routes/host.ts | 1482 +++++-- src/backend/database/routes/rbac.ts | 407 +- .../database/routes/snippets-reorder.ts | 33 + src/backend/database/routes/snippets.ts | 22 +- src/backend/database/routes/terminal.ts | 28 +- src/backend/database/routes/users.ts | 283 +- src/backend/guacamole/guacamole-server.ts | 3 +- src/backend/ssh/docker-console.ts | 197 +- src/backend/ssh/docker.ts | 107 +- src/backend/ssh/file-manager.ts | 655 +-- src/backend/ssh/host-resolver.ts | 173 + src/backend/ssh/opkssh-auth.ts | 272 +- src/backend/ssh/opkssh-cert-auth.ts | 236 + src/backend/ssh/server-stats.ts | 237 +- src/backend/ssh/terminal-session-manager.ts | 38 +- src/backend/ssh/terminal.ts | 428 +- src/backend/ssh/tmux-helper.ts | 156 + src/backend/ssh/tunnel.ts | 252 +- .../ssh/widgets/login-stats-collector.ts | 29 +- src/backend/starter.ts | 85 +- src/backend/utils/auth-manager.ts | 45 +- src/backend/utils/cors-config.ts | 71 + .../credential-system-encryption-migration.ts | 26 +- src/backend/utils/logger.ts | 29 +- src/backend/utils/opkssh-binary-manager.ts | 4 +- src/backend/utils/request-origin.ts | 4 +- .../utils/shared-credential-manager.ts | 2 +- src/backend/utils/ssh-algorithms.ts | 71 + src/backend/utils/wake-on-lan.ts | 46 + src/components/theme-provider.tsx | 42 +- src/components/ui/sonner.tsx | 12 +- src/constants/terminal-themes.ts | 57 + src/index.css | 260 ++ src/lib/db-health-monitor.ts | 87 +- src/locales/en.json | 82 +- src/types/guacamole-common-js.d.ts | 10 + src/types/index.ts | 19 + src/ui/desktop/DesktopApp.tsx | 88 +- src/ui/desktop/apps/admin/AdminSettings.tsx | 23 +- .../apps/admin/dialogs/UserEditDialog.tsx | 19 +- .../apps/admin/tabs/DatabaseSecurityTab.tsx | 131 +- .../apps/admin/tabs/GeneralSettingsTab.tsx | 116 + .../apps/admin/tabs/UserManagementTab.tsx | 18 +- .../apps/command-palette/CommandPalette.tsx | 16 +- src/ui/desktop/apps/dashboard/Dashboard.tsx | 228 +- .../dashboard/cards/ServerOverviewCard.tsx | 2 +- .../hooks/useDashboardPreferences.ts | 6 +- .../apps/features/docker/DockerManager.tsx | 2 +- .../docker/components/ConsoleTerminal.tsx | 3 +- .../docker/components/ContainerCard.tsx | 18 +- .../docker/components/ContainerList.tsx | 30 +- .../features/file-manager/FileManager.tsx | 9 +- .../features/file-manager/FileManagerGrid.tsx | 11 + .../features/guacamole/GuacamoleDisplay.tsx | 229 +- .../features/server-stats/ServerStats.tsx | 6 +- .../apps/features/terminal/Terminal.tsx | 672 ++- .../apps/host-manager/hosts/HostManager.tsx | 26 +- .../host-manager/hosts/HostManagerEditor.tsx | 42 +- .../host-manager/hosts/HostManagerViewer.tsx | 88 +- .../hosts/tabs/HostGeneralTab.tsx | 220 +- .../hosts/tabs/HostTerminalTab.tsx | 31 +- src/ui/desktop/apps/tools/SSHToolsSidebar.tsx | 622 ++- src/ui/desktop/authentication/Auth.tsx | 59 +- .../authentication/ElectronLoginForm.tsx | 288 +- .../authentication/ElectronServerConfig.tsx | 4 + src/ui/desktop/navigation/AppView.tsx | 52 +- src/ui/desktop/navigation/TopNavbar.tsx | 103 +- .../navigation/dialogs/OPKSSHDialog.tsx | 97 +- .../navigation/dialogs/TmuxSessionPicker.tsx | 122 + src/ui/desktop/navigation/hosts/Host.tsx | 50 +- src/ui/desktop/navigation/tabs/Tab.tsx | 61 +- src/ui/desktop/navigation/tabs/TabContext.tsx | 60 +- src/ui/desktop/user/ElectronVersionCheck.tsx | 4 + src/ui/desktop/user/UserProfile.tsx | 88 +- src/ui/hooks/useCommandTracker.ts | 25 +- src/ui/main-axios.ts | 452 +- src/ui/mobile/apps/terminal/Terminal.tsx | 99 +- src/ui/mobile/authentication/Auth.tsx | 11 +- 103 files changed, 10356 insertions(+), 5115 deletions(-) create mode 100644 docker/compose.dev.yml create mode 100644 src/backend/database/routes/snippets-reorder.ts create mode 100644 src/backend/ssh/host-resolver.ts create mode 100644 src/backend/ssh/opkssh-cert-auth.ts create mode 100644 src/backend/ssh/tmux-helper.ts create mode 100644 src/backend/utils/cors-config.ts create mode 100644 src/backend/utils/ssh-algorithms.ts create mode 100644 src/backend/utils/wake-on-lan.ts create mode 100644 src/ui/desktop/navigation/dialogs/TmuxSessionPicker.tsx diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index 58d63847..b7ab26d2 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -431,6 +431,7 @@ jobs: env: ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max-old-space-size=4096 run: | CURRENT_VERSION=$(node -p "require('./package.json').version") BUILD_VERSION="${{ github.run_number }}" @@ -488,6 +489,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + NODE_OPTIONS: --max-old-space-size=4096 run: | if [ "${{ steps.check_certs.outputs.has_certs }}" != "true" ]; then npm run build @@ -956,6 +958,7 @@ jobs: env: ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max-old-space-size=4096 run: | CURRENT_VERSION=$(node -p "require('./package.json').version") BUILD_VERSION="${{ github.run_number }}" diff --git a/README.md b/README.md index 88f36619..94fdc16b 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,14 @@ free and self-hosted alternative to Termius available for all platforms. - **Database Encryption** - Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more. - **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data - **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects -- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between dark or light mode based UI. Use URL routes to open any connection in full-screen. +- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between many different UI themes including light, dark, Dracula, etc. Use URL routes to open any connection in full-screen. - **Languages** - Built-in support ~30 languages (managed by [Crowdin](https://docs.termix.site/translations)) - **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS, can be run standalone without Termix backend), PWA, and dedicated mobile/tablet app for iOS and Android. - **SSH Tools** - Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals. - **Command History** - Auto-complete and view previously ran SSH commands - **Quick Connect** - Connect to a server without having to save the connection data - **Command Palette** - Double tap left shift to quickly access SSH connections with your keyboard -- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), etc. +- **SSH Feature Rich** - Supports jump hosts, Warpgate, TOTP based connections, SOCKS5, host key verification, password autofill, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc. - **Network Graph** - Customize your Dashboard to visualize your homelab based off your SSH connections with status support - **Persistent Tabs** - SSH sessions and tabs stay open across devices/refreshes if enabled in user profile diff --git a/docker/compose.dev.yml b/docker/compose.dev.yml new file mode 100644 index 00000000..20a632b5 --- /dev/null +++ b/docker/compose.dev.yml @@ -0,0 +1,33 @@ +services: + termix-dev: + build: + context: .. + dockerfile: docker/Dockerfile + container_name: termix-dev + restart: unless-stopped + ports: + - "8081:8080" + volumes: + - termix-dev-data:/app/data + environment: + PORT: "8080" + NODE_ENV: development + depends_on: + - guacd-dev + networks: + - termix-dev-net + + guacd-dev: + image: guacamole/guacd:1.6.0 + container_name: guacd-dev + restart: unless-stopped + networks: + - termix-dev-net + +volumes: + termix-dev-data: + driver: local + +networks: + termix-dev-net: + driver: bridge diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index cba466c6..7672b8e4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -15,7 +15,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/electron/main.cjs b/electron/main.cjs index 99a12f34..8fb19427 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -663,6 +663,49 @@ ipcMain.handle("set-setting", (event, key, value) => { } }); +ipcMain.handle("get-iframe-jwt", async () => { + try { + if (!mainWindow) return null; + const frames = mainWindow.webContents.mainFrame.framesInSubtree; + logToFile(`[get-iframe-jwt] scanning ${frames.length} frames`); + for (const frame of frames) { + if (frame === mainWindow.webContents.mainFrame) continue; + try { + const token = await frame.executeJavaScript( + `(function() { + try { + const t = localStorage.getItem('jwt') || sessionStorage.getItem('jwt'); + return t || null; + } catch(e) { return null; } + })()`, + ); + logToFile( + `[get-iframe-jwt] frame url=${frame.url} token found=${!!token} length=${token?.length}`, + ); + if (token && token.length > 20) return token; + } catch (err) { + logToFile(`[get-iframe-jwt] frame exec error:`, err.message); + } + } + return null; + } catch (error) { + logToFile("[get-iframe-jwt] error:", error.message); + return null; + } +}); + +ipcMain.handle("get-session-cookie", async (_event, name) => { + try { + const ses = mainWindow?.webContents?.session; + if (!ses) return null; + const cookies = await ses.cookies.get({ name }); + return cookies.length > 0 ? cookies[0].value : null; + } catch (error) { + console.error("Failed to get session cookie:", error); + return null; + } +}); + ipcMain.handle("clear-session-cookies", async () => { try { const ses = mainWindow?.webContents?.session; diff --git a/package-lock.json b/package-lock.json index b057c248..aefa65e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,49 @@ { "name": "termix", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "termix", - "version": "2.0.0", + "version": "2.1.0", "dependencies": { + "axios": "^1.10.0", + "bcryptjs": "^3.0.2", + "better-sqlite3": "^12.2.0", + "body-parser": "^1.20.2", + "chalk": "^4.1.2", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^17.2.0", + "drizzle-orm": "^0.44.3", + "express": "^5.1.0", + "guacamole-lite": "^1.2.0", + "https-proxy-agent": "^7.0.6", + "jose": "^5.2.3", + "js-yaml": "^4.1.1", + "jsonwebtoken": "^9.0.2", + "jszip": "^3.10.1", + "multer": "^2.0.2", + "nanoid": "^5.1.5", + "node-fetch": "^3.3.2", + "qrcode": "^1.5.4", + "socks": "^2.8.7", + "speakeasy": "^2.0.0", + "ssh2": "^1.16.0", + "ws": "^8.18.3" + }, + "devDependencies": { "@codemirror/autocomplete": "^6.18.7", "@codemirror/commands": "^6.3.3", "@codemirror/search": "^6.5.11", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.23.1", + "@commitlint/cli": "^20.1.0", + "@commitlint/config-conventional": "^20.0.0", + "@electron/notarize": "^2.5.0", + "@electron/rebuild": "^3.7.2", + "@eslint/js": "^9.34.0", "@hookform/resolvers": "^5.1.1", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.11", @@ -33,51 +64,52 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.14", "@types/bcryptjs": "^2.4.6", + "@types/better-sqlite3": "^7.6.13", "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", "@types/cytoscape": "^3.21.9", + "@types/express": "^5.0.3", "@types/guacamole-common-js": "^1.5.5", + "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.10", "@types/jszip": "^3.4.0", "@types/multer": "^2.0.0", + "@types/node": "^24.3.0", "@types/qrcode": "^1.5.5", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", "@types/react-grid-layout": "^1.3.6", "@types/speakeasy": "^2.0.10", + "@types/ssh2": "^1.15.5", + "@types/ws": "^8.18.1", "@uiw/codemirror-extensions-langs": "^4.24.1", "@uiw/codemirror-theme-github": "^4.25.4", "@uiw/react-codemirror": "^4.24.1", + "@vitejs/plugin-react": "^4.3.4", "@xterm/addon-clipboard": "^0.2.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", - "axios": "^1.10.0", - "bcryptjs": "^3.0.2", - "better-sqlite3": "^12.2.0", - "body-parser": "^1.20.2", - "chalk": "^4.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", + "concurrently": "^9.2.1", "cytoscape": "^3.33.1", - "dotenv": "^17.2.0", - "drizzle-orm": "^0.44.3", - "express": "^5.1.0", + "electron": "^38.0.0", + "electron-builder": "^26.0.12", + "eslint": "^9.34.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", "guacamole-common-js": "^1.5.0", - "guacamole-lite": "^1.2.0", - "https-proxy-agent": "^7.0.6", - "i18n-auto-translation": "^2.2.3", + "husky": "^9.1.7", "i18next": "^25.4.2", "i18next-browser-languagedetector": "^8.2.0", - "jose": "^5.2.3", - "jsonwebtoken": "^9.0.2", - "jszip": "^3.10.1", + "lint-staged": "^16.2.3", "lucide-react": "^0.525.0", - "multer": "^2.0.2", - "nanoid": "^5.1.5", "next-themes": "^0.4.6", - "node-fetch": "^3.3.2", - "qrcode": "^1.5.4", + "prettier": "3.6.2", "react": "^19.1.0", "react-cytoscapejs": "^2.0.0", "react-dom": "^19.1.0", @@ -89,53 +121,20 @@ "react-markdown": "^10.1.0", "react-pdf": "^10.1.0", "react-photo-view": "^1.2.7", - "react-player": "^3.3.3", "react-resizable-panels": "^3.0.3", "react-simple-keyboard": "^3.8.120", "react-syntax-highlighter": "^15.6.6", "react-xtermjs": "^1.0.10", "recharts": "^3.2.1", "remark-gfm": "^4.0.1", - "socks": "^2.8.7", "sonner": "^2.0.7", - "speakeasy": "^2.0.0", - "ssh2": "^1.16.0", + "swagger-jsdoc": "^6.2.8", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.14", - "wait-on": "^9.0.1", - "ws": "^8.18.3", - "zod": "^4.0.5" - }, - "devDependencies": { - "@commitlint/cli": "^20.1.0", - "@commitlint/config-conventional": "^20.0.0", - "@electron/notarize": "^2.5.0", - "@electron/rebuild": "^3.7.2", - "@eslint/js": "^9.34.0", - "@types/better-sqlite3": "^7.6.13", - "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24.3.0", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@types/ssh2": "^1.15.5", - "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^4.3.4", - "concurrently": "^9.2.1", - "electron": "^38.0.0", - "electron-builder": "^26.0.12", - "eslint": "^9.34.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", - "husky": "^9.1.7", - "lint-staged": "^16.2.3", - "prettier": "3.6.2", - "swagger-jsdoc": "^6.2.8", "typescript": "~5.9.2", "typescript-eslint": "^8.40.0", - "vite": "^7.1.5" + "vite": "^7.1.5", + "zod": "^4.0.5" } }, "node_modules/@apidevtools/json-schema-ref-parser": { @@ -444,6 +443,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -501,6 +501,7 @@ "version": "6.19.1", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz", "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -513,6 +514,7 @@ "version": "6.10.0", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -525,6 +527,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/lang-html": "^6.0.0", @@ -539,6 +542,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -549,6 +553,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -562,6 +567,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -575,6 +581,7 @@ "version": "6.4.11", "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -592,6 +599,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -602,6 +610,7 @@ "version": "6.2.4", "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -617,6 +626,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.0.tgz", "integrity": "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/lang-html": "^6.0.0", @@ -630,6 +640,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -640,6 +651,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/lang-css": "^6.2.0", @@ -653,6 +665,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz", "integrity": "sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -669,6 +682,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.7.1", @@ -684,6 +698,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/lang-html": "^6.0.0", @@ -697,6 +712,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.3.2", @@ -710,6 +726,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -720,6 +737,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/lang-css": "^6.2.0", @@ -733,6 +751,7 @@ "version": "6.10.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -747,6 +766,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/lang-html": "^6.0.0", @@ -761,6 +781,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -773,6 +794,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -787,6 +809,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -802,6 +825,7 @@ "version": "6.11.3", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -816,6 +840,7 @@ "version": "6.5.2", "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz", "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/lang-angular": "^0.1.0", @@ -847,6 +872,7 @@ "version": "6.5.2", "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0" @@ -856,6 +882,7 @@ "version": "6.9.1", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.1.tgz", "integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -867,6 +894,7 @@ "version": "6.5.11", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -878,6 +906,7 @@ "version": "6.5.2", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dev": true, "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" @@ -887,6 +916,7 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -899,6 +929,7 @@ "version": "6.38.6", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -1222,9 +1253,9 @@ } }, "node_modules/@develar/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1256,9 +1287,9 @@ "license": "MIT" }, "node_modules/@electron/asar": { - "version": "3.2.18", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz", - "integrity": "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, "license": "MIT", "dependencies": { @@ -1273,19 +1304,6 @@ "node": ">=10.12.0" } }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@electron/fuses": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", @@ -1394,9 +1412,9 @@ } }, "node_modules/@electron/node-gyp/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -1425,9 +1443,9 @@ } }, "node_modules/@electron/node-gyp/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -1453,9 +1471,9 @@ } }, "node_modules/@electron/osx-sign": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", - "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1547,13 +1565,13 @@ } }, "node_modules/@electron/universal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", - "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", "dev": true, "license": "MIT", "dependencies": { - "@electron/asar": "^3.2.7", + "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", @@ -1566,9 +1584,9 @@ } }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -1576,9 +1594,9 @@ } }, "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -1591,13 +1609,13 @@ } }, "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1629,9 +1647,9 @@ } }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "optional": true, @@ -1646,12 +1664,13 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1662,12 +1681,13 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1678,12 +1698,13 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1694,12 +1715,13 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1710,12 +1732,13 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1726,12 +1749,13 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1742,12 +1766,13 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1758,12 +1783,13 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1774,12 +1800,13 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1790,12 +1817,13 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1806,12 +1834,13 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1822,12 +1851,13 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1838,12 +1868,13 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1854,12 +1885,13 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1870,12 +1902,13 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1886,12 +1919,13 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1902,12 +1936,13 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1918,12 +1953,13 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1934,12 +1970,13 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1950,12 +1987,13 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1966,12 +2004,13 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1982,12 +2021,13 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1998,12 +2038,13 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2014,12 +2055,13 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2030,12 +2072,13 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2046,12 +2089,13 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2118,19 +2162,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", @@ -2182,9 +2213,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2218,19 +2249,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { "version": "9.39.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", @@ -2272,6 +2290,7 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" @@ -2281,6 +2300,7 @@ "version": "1.7.4", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.3", @@ -2291,6 +2311,7 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dev": true, "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.4" @@ -2304,6 +2325,7 @@ "version": "0.2.10", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, "license": "MIT" }, "node_modules/@gar/promisify": { @@ -2313,146 +2335,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@google-cloud/common": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-6.0.0.tgz", - "integrity": "sha512-IXh04DlkLMxWgYLIUYuHHKXKOUwPDzDgke1ykkkJPe48cGIS9kkL2U/o0pm4ankHLlvzLF/ma1eO86n/bkumIA==", - "dependencies": { - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", - "arrify": "^2.0.0", - "duplexify": "^4.1.3", - "extend": "^3.0.2", - "google-auth-library": "^10.0.0-rc.1", - "html-entities": "^2.5.2", - "retry-request": "^8.0.0", - "teeny-request": "^10.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@google-cloud/common/node_modules/@google-cloud/promisify": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.1.0.tgz", - "integrity": "sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", - "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-5.0.0.tgz", - "integrity": "sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@google-cloud/translate": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@google-cloud/translate/-/translate-9.2.0.tgz", - "integrity": "sha512-LBKoXMXsM6jyqD9RDO74E3Q8uUn9TWy7YwIrF+WS4I9erdI+VZHxmdffi4sFfQ196FeprfwMMAFa8Oy6u7G8xw==", - "dependencies": { - "@google-cloud/common": "^6.0.0", - "@google-cloud/promisify": "^5.0.0", - "arrify": "^2.0.0", - "extend": "^3.0.2", - "google-gax": "^5.0.0", - "is-html": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", - "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@hapi/address": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", - "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@hapi/formula": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", - "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/hoek": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", - "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/pinpoint": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", - "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/tlds": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", - "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@hapi/topo": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", - "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^11.0.2" - } - }, "node_modules/@hookform/resolvers": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "dev": true, "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" @@ -2517,6 +2404,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/@iconify/react/-/react-5.2.1.tgz", "integrity": "sha512-37GDR3fYDZmnmUn9RagyaX+zca24jfVOMY8E1IXTqJuE8pxNtN51KWPQe3VODOWvuUurq7q9uUu3CFrpqj5Iqg==", + "dev": true, "license": "MIT", "dependencies": { "@iconify/types": "^2.0.0" @@ -2532,33 +2420,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, "license": "MIT" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -2576,6 +2445,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2588,12 +2458,14 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -2611,6 +2483,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2624,10 +2497,34 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2638,6 +2535,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2648,6 +2546,7 @@ "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" @@ -2657,27 +2556,20 @@ "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.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", @@ -2689,12 +2581,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", + "dev": true, "license": "MIT" }, "node_modules/@lezer/cpp": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.3.tgz", "integrity": "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2706,6 +2600,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2717,6 +2612,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2728,6 +2624,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.3.0" @@ -2737,6 +2634,7 @@ "version": "1.3.12", "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz", "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2748,6 +2646,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2759,6 +2658,7 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2770,6 +2670,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2781,6 +2682,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -2790,6 +2692,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.5.1.tgz", "integrity": "sha512-F3ZFnIfNAOy/jPSk6Q0e3bs7e9grfK/n5zerkKoc5COH6Guy3Zb0vrJwXzdck79K16goBhYBRAvhf+ksqe0cMg==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0", @@ -2800,6 +2703,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2811,6 +2715,7 @@ "version": "1.1.18", "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2822,6 +2727,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2833,6 +2739,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2844,6 +2751,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2855,6 +2763,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2905,12 +2814,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, "license": "MIT" }, "node_modules/@monaco-editor/loader": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz", "integrity": "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==", + "dev": true, "license": "MIT", "dependencies": { "state-local": "^1.0.6" @@ -2920,6 +2831,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "dev": true, "license": "MIT", "dependencies": { "@monaco-editor/loader": "^1.5.0" @@ -2930,78 +2842,11 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@mux/mux-data-google-ima": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@mux/mux-data-google-ima/-/mux-data-google-ima-0.2.8.tgz", - "integrity": "sha512-0ZEkHdcZ6bS8QtcjFcoJeZxJTpX7qRIledf4q1trMWPznugvtajCjCM2kieK/pzkZj1JM6liDRFs1PJSfVUs2A==", - "license": "MIT", - "dependencies": { - "mux-embed": "5.9.0" - } - }, - "node_modules/@mux/mux-player": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.8.0.tgz", - "integrity": "sha512-2KcJdW4BBX8JDcXpclFKaNBsqpebtaEfTzwm5lPP1Lf6y5OMILvf2tqVCOczurREVFyaEoVD71vL0I5Vvqb1dA==", - "license": "MIT", - "dependencies": { - "@mux/mux-video": "0.27.2", - "@mux/playback-core": "0.31.2", - "media-chrome": "~4.15.1", - "player.style": "^0.3.0" - } - }, - "node_modules/@mux/mux-player-react": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.8.0.tgz", - "integrity": "sha512-c9TKtK9nsSpXOuC1LVLmmHA+Zlpcx4mzgGaA7ZlukrGMfoXWvA90ROSVAAjXRA+UKSHdLIbvNofgG3P6rEE/4Q==", - "license": "MIT", - "dependencies": { - "@mux/mux-player": "3.8.0", - "@mux/playback-core": "0.31.2", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0", - "react": "^17.0.2 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0", - "react-dom": "^17.0.2 || ^17.0.2-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@mux/mux-video": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.27.2.tgz", - "integrity": "sha512-VAqSw/3kS/qBzjyFSX3wClIX5Kdk6eXXlhxIJRWlClYvUKGm9ruhd7HzkwZVOJguvUh5QbGoiGWBEW2xkNIXzw==", - "license": "MIT", - "dependencies": { - "@mux/mux-data-google-ima": "0.2.8", - "@mux/playback-core": "0.31.2", - "castable-video": "~1.1.11", - "custom-media-element": "~1.4.5", - "media-tracks": "~0.3.3" - } - }, - "node_modules/@mux/playback-core": { - "version": "0.31.2", - "resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.2.tgz", - "integrity": "sha512-bhOVTGAuKCQuDzNOc3XvDq7vsgqy2DAacLP0WdJciUKjfZhs3oA11NbKG7qAN6akPnZVfgn0Jn/sJN8TRjE30A==", - "license": "MIT", - "dependencies": { - "hls.js": "~1.6.13", - "mux-embed": "^5.8.3" - } - }, "node_modules/@napi-rs/canvas": { "version": "0.1.81", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz", "integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==", + "dev": true, "license": "MIT", "optional": true, "workspaces": [ @@ -3030,6 +2875,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3046,6 +2892,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3062,6 +2909,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3078,6 +2926,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3094,6 +2943,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3110,6 +2960,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3126,6 +2977,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3142,6 +2994,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3158,6 +3011,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3174,6 +3028,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3221,6 +3076,45 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@npmcli/fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", @@ -3254,82 +3148,32 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "dev": true, "license": "MIT" }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "dev": true, "license": "MIT" }, "node_modules/@radix-ui/react-accordion": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3361,6 +3205,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3389,6 +3234,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3407,6 +3253,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -3430,6 +3277,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3460,6 +3308,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3490,6 +3339,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -3516,6 +3366,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3534,6 +3385,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3549,6 +3401,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3564,6 +3417,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3600,6 +3454,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3618,6 +3473,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3633,6 +3489,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3660,6 +3517,7 @@ "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3689,6 +3547,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -3704,6 +3563,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -3729,6 +3589,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -3747,6 +3608,7 @@ "version": "2.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -3770,6 +3632,7 @@ "version": "2.1.16", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3810,6 +3673,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3828,6 +3692,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -3865,6 +3730,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -3883,6 +3749,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "dev": true, "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -3915,6 +3782,7 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3", @@ -3939,6 +3807,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -3963,6 +3832,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.2.3" @@ -3986,6 +3856,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4004,6 +3875,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-context": "1.1.2", @@ -4028,6 +3900,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -4059,6 +3932,7 @@ "version": "1.2.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", @@ -4090,6 +3964,7 @@ "version": "2.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", @@ -4133,6 +4008,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4151,6 +4027,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -4174,6 +4051,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", @@ -4207,6 +4085,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4225,6 +4104,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -4254,6 +4134,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -4284,6 +4165,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.3", @@ -4318,6 +4200,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" @@ -4336,6 +4219,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4351,6 +4235,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", @@ -4370,6 +4255,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4388,6 +4274,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" @@ -4406,6 +4293,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4421,6 +4309,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4436,6 +4325,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.1" @@ -4454,6 +4344,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" @@ -4472,6 +4363,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.1.3" @@ -4495,12 +4387,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "dev": true, "license": "MIT" }, "node_modules/@reduxjs/toolkit": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz", "integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==", + "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -4527,6 +4421,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz", "integrity": "sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==", + "dev": true, "license": "MIT", "peerDependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -4542,6 +4437,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-solidity/-/codemirror-lang-solidity-6.0.2.tgz", "integrity": "sha512-/dpTVH338KFV6SaDYYSadkB4bI/0B0QRF/bkt1XS3t3QtyR49mn6+2k0OUQhvt2ZSO7kt10J+OPilRAtgbmX0w==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/highlight": "^1.2.0" @@ -4554,6 +4450,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz", "integrity": "sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==", + "dev": true, "license": "MIT", "peerDependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -4577,12 +4474,13 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4590,12 +4488,13 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4603,12 +4502,13 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4616,12 +4516,13 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4629,12 +4530,13 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4642,12 +4544,13 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4655,12 +4558,13 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4668,12 +4572,13 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4681,12 +4586,13 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4694,12 +4600,13 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4707,12 +4614,27 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", "cpu": [ "loong64" ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4720,12 +4642,27 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", "cpu": [ "ppc64" ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4733,12 +4670,13 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4746,12 +4684,13 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4759,12 +4698,13 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4772,12 +4712,13 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4785,25 +4726,41 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4811,12 +4768,13 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4824,12 +4782,13 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4837,12 +4796,13 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4850,12 +4810,13 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4879,20 +4840,16 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, "license": "MIT" }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "dev": true, "license": "MIT" }, - "node_modules/@svta/common-media-library": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz", - "integrity": "sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==", - "license": "Apache-2.0" - }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -4910,6 +4867,7 @@ "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -4925,6 +4883,7 @@ "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -4951,6 +4910,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4967,6 +4927,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4983,6 +4944,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4999,6 +4961,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5015,6 +4978,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5031,6 +4995,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5047,6 +5012,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5063,6 +5029,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5079,6 +5046,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5103,6 +5071,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5117,6 +5086,66 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", @@ -5124,6 +5153,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5140,6 +5170,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5153,6 +5184,7 @@ "version": "4.1.16", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz", "integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==", + "dev": true, "license": "MIT", "dependencies": { "@tailwindcss/node": "4.1.16", @@ -5167,6 +5199,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -5221,6 +5254,7 @@ "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/better-sqlite3": { @@ -5237,6 +5271,7 @@ "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": "*", @@ -5260,6 +5295,7 @@ "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": "*" @@ -5279,6 +5315,7 @@ "version": "1.4.10", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/express": "*" @@ -5298,30 +5335,35 @@ "version": "3.21.9", "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz", "integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -5331,12 +5373,14 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -5346,6 +5390,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -5355,18 +5400,21 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, "license": "MIT" }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/ms": "*" @@ -5376,12 +5424,14 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree-jsx": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*" @@ -5391,6 +5441,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", @@ -5402,6 +5453,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -5424,12 +5476,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.5.tgz", "integrity": "sha512-dqDYo/PhbOXFGSph23rFDRZRzXdKPXy/nsTkovFMb6P3iGrd0qGB5r5BXHmX5Cr/LK7L1TK9nYrTMbtPkhdXyg==", + "dev": true, "license": "MIT" }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" @@ -5446,6 +5500,14 @@ "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/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -5470,6 +5532,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz", "integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==", + "dev": true, "license": "MIT", "dependencies": { "jszip": "*" @@ -5489,6 +5552,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" @@ -5498,18 +5562,21 @@ "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/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, "license": "MIT" }, "node_modules/@types/multer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, "license": "MIT", "dependencies": { "@types/express": "*" @@ -5519,6 +5586,7 @@ "version": "24.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -5540,6 +5608,7 @@ "version": "1.5.6", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5549,18 +5618,21 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "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/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -5570,7 +5642,7 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -5580,6 +5652,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.6.tgz", "integrity": "sha512-Cw7+sb3yyjtmxwwJiXtEXcu5h4cgs+sCGkHwHXsFmPyV30bf14LeD/fa2LwQovuD2HWxCcjIdNhDlcYGj95qGA==", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -5599,6 +5672,7 @@ "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": "*" @@ -5608,6 +5682,7 @@ "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": "*", @@ -5619,6 +5694,7 @@ "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", @@ -5629,6 +5705,7 @@ "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/speakeasy/-/speakeasy-2.0.10.tgz", "integrity": "sha512-QVRlDW5r4yl7p7xkNIbAIC/JtyOcClDIIdKfuG7PWdDT1MmyhtXSANsildohy0K+Lmvf/9RUtLbNLMacvrVwxA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5661,16 +5738,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "dev": true, "license": "MIT" }, "node_modules/@types/verror": { @@ -5893,9 +5981,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -5903,13 +5991,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5964,6 +6052,7 @@ "version": "4.25.2", "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.2.tgz", "integrity": "sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -5991,6 +6080,7 @@ "version": "4.25.2", "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.2.tgz", "integrity": "sha512-fWS9fP52QJAFgXbsUl6vKMBqQ2PIT8z5TvX8BKBqPz/I+ayh6Fk+HzoeUtslUGPTu+UTPgB5m0qt3rTIDXWvng==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -6012,6 +6102,7 @@ "version": "4.25.4", "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.4.tgz", "integrity": "sha512-M5zRT2vIpNsuKN0Lz+DwLnmhHW8Eddp1M9zC0hm3V+bvffmaSn/pUDey1eqGIv5xNNmjhqvDAz0a90xLYCzvSw==", + "dev": true, "license": "MIT", "dependencies": { "@uiw/codemirror-themes": "4.25.4" @@ -6024,6 +6115,7 @@ "version": "4.25.4", "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.4.tgz", "integrity": "sha512-2SLktItgcZC4p0+PfFusEbAHwbuAWe3bOOntCevVgHtrWGtGZX3IPv2k8IKZMgOXtAHyGKpJvT9/nspPn/uCQg==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -6043,6 +6135,7 @@ "version": "4.25.2", "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.2.tgz", "integrity": "sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", @@ -6069,18 +6162,9 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, "license": "ISC" }, - "node_modules/@vimeo/player": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@vimeo/player/-/player-2.29.0.tgz", - "integrity": "sha512-9JjvjeqUndb9otCCFd0/+2ESsLk7VkDE6sxOBy9iy2ukezuQbplVRi+g9g59yAurKofbmTi/KcKxBGO/22zWRw==", - "license": "MIT", - "dependencies": { - "native-promise-only": "0.8.1", - "weakmap-polyfill": "2.0.4" - } - }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -6103,9 +6187,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", "dev": true, "license": "MIT", "engines": { @@ -6116,6 +6200,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0.tgz", "integrity": "sha512-Dl31BCtBhLaUEECUbEiVcCLvLBbaeGYdT7NofB8OJkGTD3MWgBsaLjXvfGAD4tQNHhm6mbKyYkR7XD8kiZsdNg==", + "dev": true, "license": "MIT", "dependencies": { "js-base64": "^3.7.5" @@ -6125,6 +6210,7 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "dev": true, "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" @@ -6134,6 +6220,7 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.8.0.tgz", "integrity": "sha512-LxinXu8SC4OmVa6FhgwsVCBZbr8WoSGzBl2+vqe8WcQ6hb1r6Gj9P99qTNdPiFPh4Ceiu2pC8xukZ6+2nnh49Q==", + "dev": true, "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" @@ -6143,6 +6230,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.11.0.tgz", "integrity": "sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==", + "dev": true, "license": "MIT", "peerDependencies": { "@xterm/xterm": "^5.0.0" @@ -6152,6 +6240,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "dev": true, "license": "MIT" }, "node_modules/7zip-bin": { @@ -6241,9 +6330,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -6277,6 +6366,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6308,81 +6398,199 @@ "license": "MIT" }, "node_modules/app-builder-lib": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.0.12.tgz", - "integrity": "sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", "dev": true, "license": "MIT", "dependencies": { "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.2.18", + "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.1", - "@electron/rebuild": "3.7.0", - "@electron/universal": "2.0.1", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", - "config-file-ts": "0.2.8-rc1", + "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", - "electron-publish": "26.0.11", + "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", - "is-ci": "^3.0.0", "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", - "minimatch": "^10.0.0", + "minimatch": "^10.0.3", "plist": "3.1.0", + "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", - "semver": "^7.3.8", - "tar": "^6.1.12", + "semver": "~7.7.3", + "tar": "^7.5.7", "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "dmg-builder": "26.0.12", - "electron-builder-squirrel-windows": "26.0.12" + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" } }, - "node_modules/app-builder-lib/node_modules/@electron/rebuild": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz", - "integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==", + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/rebuild": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", + "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", "dev": true, "license": "MIT", "dependencies": { - "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", "got": "^11.7.0", - "node-abi": "^3.45.0", - "node-api-version": "^0.2.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", - "tar": "^6.0.5", + "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" + "node": ">=22.12.0" + } + }, + "node_modules/app-builder-lib/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/app-builder-lib/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/app-builder-lib/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/app-builder-lib/node_modules/dotenv": { @@ -6413,6 +6621,111 @@ "node": ">=12" } }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/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/app-builder-lib/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/app-builder-lib/node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/app-builder-lib/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -6423,13 +6736,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -6445,14 +6758,6 @@ "dev": true, "license": "MIT" }, - "node_modules/arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "engines": { - "node": ">=8" - } - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -6518,20 +6823,21 @@ } }, "node_modules/axios": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", - "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -6542,6 +6848,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, "license": "MIT" }, "node_modules/base32.js": { @@ -6580,45 +6887,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bcp-47": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", - "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/bcp-47-match": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", - "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/bcp-47-normalize": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", - "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", - "license": "MIT", - "dependencies": { - "bcp-47": "^2.0.0", - "bcp-47-match": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -6651,14 +6919,6 @@ "node": "20.x || 22.x || 23.x || 24.x" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "engines": { - "node": "*" - } - }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -6694,23 +6954,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -6726,6 +6986,26 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/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/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -6742,9 +7022,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -6855,23 +7135,22 @@ } }, "node_modules/builder-util": { - "version": "26.0.11", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.0.11.tgz", - "integrity": "sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", "dev": true, "license": "MIT", "dependencies": { "@types/debug": "^4.1.6", "7zip-bin": "~5.2.0", "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.3.1", + "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", - "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", @@ -6881,9 +7160,9 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", - "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6960,9 +7239,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -7001,9 +7280,9 @@ } }, "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -7118,34 +7397,17 @@ ], "license": "CC-BY-4.0" }, - "node_modules/castable-video": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.1.11.tgz", - "integrity": "sha512-LCRTK6oe7SB1SiUQFzZCo6D6gcEzijqBTVIuj3smKpQdesXM18QTbCVqWgh9MfOeQgTx/i9ji5jGcdqNPeWg2g==", - "license": "MIT", - "dependencies": { - "custom-media-element": "~1.4.5" - } - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ce-la-react": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/ce-la-react/-/ce-la-react-0.3.1.tgz", - "integrity": "sha512-g0YwpZDPIwTwFumGTzNHcgJA6VhFfFCJkSNdUdC04br2UfU+56JDrJrJva3FZ7MToB4NDHAFBiPE/PZdNl1mQA==", - "license": "BSD-3-Clause", - "peerDependencies": { - "react": ">=17.0.0" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -7166,6 +7428,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7176,6 +7439,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7186,6 +7450,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7196,6 +7461,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7220,9 +7486,9 @@ "license": "MIT" }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -7239,6 +7505,7 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "clsx": "^2.1.1" @@ -7308,6 +7575,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -7322,6 +7590,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7331,6 +7600,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7343,6 +7613,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7379,16 +7650,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cloudflare-video-element": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/cloudflare-video-element/-/cloudflare-video-element-1.3.4.tgz", - "integrity": "sha512-F9g+tXzGEXI6v6L48qXxr8vnR8+L6yy7IhpJxK++lpzuVekMHTixxH7/dzLuq6OacVGziU4RB5pzZYJ7/LYtJg==", - "license": "MIT" - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7398,6 +7664,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "dev": true, "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", @@ -7410,16 +7677,11 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/codem-isoboxer": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz", - "integrity": "sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==", - "license": "MIT" - }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -7435,6 +7697,7 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/codemirror-lang-mermaid/-/codemirror-lang-mermaid-0.5.0.tgz", "integrity": "sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.9.0", @@ -7483,6 +7746,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -7597,74 +7861,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/config-file-ts": { - "version": "0.2.8-rc1", - "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", - "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob": "^10.3.12", - "typescript": "^5.4.3" - } - }, - "node_modules/config-file-ts/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/config-file-ts/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/config-file-ts/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -7859,6 +8055,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, "license": "MIT" }, "node_modules/cross-dirname": { @@ -7874,6 +8071,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7888,18 +8086,14 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/custom-media-element": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/custom-media-element/-/custom-media-element-1.4.5.tgz", - "integrity": "sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==", + "dev": true, "license": "MIT" }, "node_modules/cytoscape": { "version": "3.33.1", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10" @@ -7909,6 +8103,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -7921,6 +8116,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7930,6 +8126,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -7939,6 +8136,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7948,6 +8146,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -7960,6 +8159,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7969,6 +8169,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -7985,6 +8186,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -7997,6 +8199,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -8009,6 +8212,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -8021,6 +8225,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -8039,35 +8244,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dash-video-element": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dash-video-element/-/dash-video-element-0.2.0.tgz", - "integrity": "sha512-dgmhBOte6JgvSvowvrh0Q/vhSrB52Q/AUl/KqminAUkPuUT3CCUNhto1X8ANigWkmNwhktFc/PCe0lF/4tBFwQ==", - "license": "MIT", - "dependencies": { - "custom-media-element": "^1.4.5", - "dashjs": "^5.0.3", - "media-tracks": "^0.3.3" - } - }, - "node_modules/dashjs": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.0.3.tgz", - "integrity": "sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==", - "license": "BSD-3-Clause", - "dependencies": { - "@svta/common-media-library": "^0.12.4", - "bcp-47-match": "^2.0.3", - "bcp-47-normalize": "^2.3.0", - "codem-isoboxer": "0.3.10", - "fast-deep-equal": "3.1.3", - "html-entities": "^2.5.2", - "imsc": "^1.1.5", - "localforage": "^1.10.0", - "path-browserify": "^1.0.1", - "ua-parser-js": "^1.0.37" - } - }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -8107,12 +8283,14 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "dev": true, "license": "MIT" }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "dev": true, "license": "MIT", "dependencies": { "character-entities": "^2.0.0" @@ -8165,11 +8343,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-object-diff": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/deep-object-diff/-/deep-object-diff-1.1.9.tgz", - "integrity": "sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==" - }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -8253,6 +8426,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8289,12 +8463,14 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "dev": true, "license": "MIT" }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -8321,29 +8497,15 @@ "p-limit": "^3.1.0 " } }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/dmg-builder": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.0.12.tgz", - "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.0.12", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -8408,9 +8570,9 @@ } }, "node_modules/dmg-license/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "optional": true, @@ -8447,11 +8609,15 @@ } }, "node_modules/dompurify": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", - "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "dev": true, "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/dot-prop": { "version": "5.3.0", @@ -8646,34 +8812,11 @@ "node": ">= 0.4" } }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/duplexify/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==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, "license": "MIT" }, "node_modules/ecdsa-sig-formatter": { @@ -8708,9 +8851,9 @@ } }, "node_modules/electron": { - "version": "38.5.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-38.5.0.tgz", - "integrity": "sha512-dbC7V+eZweerYMJfxQldzHOg37a1VdNMCKxrJxlkp3cA30gOXtXSg4ZYs07L5+QwI19WOy1uyvtEUgbw1RRsCQ==", + "version": "38.8.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-38.8.6.tgz", + "integrity": "sha512-lyBhcVi9QYAZL6FO6r5twAWAjWnYomo3iVDvrb5SJZlq928BGemHOKG0tPIq41NOLaCu9f3XdEEjMkjQPjprRg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8727,19 +8870,19 @@ } }, "node_modules/electron-builder": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.0.12.tgz", - "integrity": "sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", "dev": true, "license": "MIT", "dependencies": { - "app-builder-lib": "26.0.12", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", - "dmg-builder": "26.0.12", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", - "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" @@ -8753,15 +8896,15 @@ } }, "node_modules/electron-builder-squirrel-windows": { - "version": "26.0.12", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", - "integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "app-builder-lib": "26.0.12", - "builder-util": "26.0.11", + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, @@ -8781,17 +8924,17 @@ } }, "node_modules/electron-publish": { - "version": "26.0.11", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.0.11.tgz", - "integrity": "sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==", + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", "dev": true, "license": "MIT", "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "26.0.11", - "builder-util-runtime": "9.3.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", - "form-data": "^4.0.0", + "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" @@ -8949,6 +9092,7 @@ "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -9047,6 +9191,7 @@ "version": "1.41.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==", + "dev": true, "license": "MIT", "workspaces": [ "docs", @@ -9062,9 +9207,10 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -9074,38 +9220,39 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9244,9 +9391,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -9300,19 +9447,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -9397,6 +9531,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -9426,6 +9561,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, "license": "MIT" }, "node_modules/expand-template": { @@ -9487,23 +9623,27 @@ } }, "node_modules/express/node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/cookie-signature": { @@ -9516,15 +9656,19 @@ } }, "node_modules/express/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/media-typer": { @@ -9536,21 +9680,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/raw-body": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", @@ -9600,6 +9729,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, "license": "MIT" }, "node_modules/extract-zip": { @@ -9638,12 +9768,14 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-equals": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz", "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -9721,6 +9853,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, "license": "MIT", "dependencies": { "format": "^0.2.0" @@ -9783,9 +9916,9 @@ "license": "MIT" }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -9793,9 +9926,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -9803,9 +9936,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -9878,16 +10011,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -9908,6 +10041,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -9921,9 +10055,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -9961,6 +10095,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, "engines": { "node": ">=0.4.x" } @@ -10041,6 +10176,7 @@ "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, @@ -10060,96 +10196,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gaxios/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/gaxios/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gaxios/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gaxios/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/gaxios/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10173,6 +10219,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -10209,6 +10256,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10302,19 +10350,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -10381,134 +10416,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-auth-library/node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-auth-library/node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-gax": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", - "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", - "dependencies": { - "@grpc/grpc-js": "^1.12.6", - "@grpc/proto-loader": "^0.8.0", - "duplexify": "^4.1.3", - "google-auth-library": "^10.1.0", - "google-logging-utils": "^1.1.1", - "node-fetch": "^3.3.2", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^3.0.0", - "protobufjs": "^7.5.3", - "retry-request": "^8.0.0", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-gax/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/google-gax/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-gax/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-gax/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/google-gax/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "engines": { - "node": ">=14" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10551,6 +10458,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -10560,41 +10468,11 @@ "dev": true, "license": "MIT" }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gtoken/node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/gtoken/node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/guacamole-common-js": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/guacamole-common-js/-/guacamole-common-js-1.5.0.tgz", "integrity": "sha512-zxztif3GGhKbg1RgOqwmqot8kXgv2HmHFg1EvWwd4q7UfEKvBcYZ0f+7G8HzvU+FUxF0Psqm9Kl5vCbgfrRgJg==", + "dev": true, "license": "Apache 2.0" }, "node_modules/guacamole-lite": { @@ -10676,6 +10554,7 @@ "version": "2.2.5", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -10686,6 +10565,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -10713,6 +10593,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -10726,6 +10607,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^2.0.0", @@ -10743,6 +10625,7 @@ "version": "2.3.10", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2" @@ -10752,12 +10635,14 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, "license": "MIT" }, "node_modules/hastscript/node_modules/comma-separated-tokens": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -10768,6 +10653,7 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dev": true, "license": "MIT", "dependencies": { "xtend": "^4.0.0" @@ -10781,6 +10667,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -10791,6 +10678,7 @@ "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": "*" @@ -10800,25 +10688,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "dev": true, "license": "CC0-1.0" }, - "node_modules/hls-video-element": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/hls-video-element/-/hls-video-element-1.5.8.tgz", - "integrity": "sha512-DdeX5NzhM2Bj+ls5aaRrzSSnriK+r6lCrDa0YyfviNO4zb10JyAnJHZM214lXBWQghCm+fKmlWW1qpzdNoSAvQ==", - "license": "MIT", - "dependencies": { - "custom-media-element": "^1.4.5", - "hls.js": "^1.6.5", - "media-tracks": "^0.3.3" - } - }, - "node_modules/hls.js": { - "version": "1.6.14", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz", - "integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==", - "license": "Apache-2.0" - }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -10852,46 +10724,21 @@ "dev": true, "license": "ISC" }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dev": true, "license": "MIT", "dependencies": { "void-elements": "3.1.0" } }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -10997,166 +10844,11 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/i18n-auto-translation": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/i18n-auto-translation/-/i18n-auto-translation-2.2.3.tgz", - "integrity": "sha512-Gu3qGOq4mG8qBcEYqfvga5SQGsmfKjTakXdXqFV+FmMk12KGnpOcftzvp/7TcgXM6MvLDPenAf2M61a/R0N9Lw==", - "dependencies": { - "@google-cloud/translate": "9.2.0", - "axios": "1.12.2", - "deep-object-diff": "1.1.9", - "glob": "11.0.3", - "html-entities": "2.6.0", - "just-extend": "6.2.0", - "yargs": "18.0.0", - "yocto-spinner": "1.0.0" - }, - "bin": { - "i18n-auto-translation": "dist/src/index.js" - }, - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/i18n-auto-translation/node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/i18n-auto-translation/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/i18n-auto-translation/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" - }, - "node_modules/i18n-auto-translation/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/i18n-auto-translation/node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/i18n-auto-translation/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/i18n-auto-translation/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/i18n-auto-translation/node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/i18n-auto-translation/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/i18n-auto-translation/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/i18n-auto-translation/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, "node_modules/i18next": { "version": "25.6.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz", "integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==", + "dev": true, "funding": [ { "type": "individual", @@ -11188,6 +10880,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2" @@ -11263,6 +10956,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -11307,21 +11001,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/imsc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz", - "integrity": "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==", - "license": "BSD-2-Clause", - "dependencies": { - "sax": "1.2.1" - } - }, - "node_modules/imsc/node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", - "license": "ISC" - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -11381,12 +11060,14 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "dev": true, "license": "MIT" }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -11414,6 +11095,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -11424,6 +11106,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -11441,23 +11124,11 @@ "dev": true, "license": "MIT" }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -11500,23 +11171,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-html": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-html/-/is-html-2.0.0.tgz", - "integrity": "sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==", - "dependencies": { - "html-tags": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -11558,6 +11219,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -11605,9 +11267,9 @@ "license": "MIT" }, "node_modules/isbinaryfile": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz", - "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", "dev": true, "license": "MIT", "engines": { @@ -11621,12 +11283,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -11660,29 +11324,12 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/joi": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", - "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/address": "^5.1.1", - "@hapi/formula": "^3.0.2", - "@hapi/hoek": "^11.0.7", - "@hapi/pinpoint": "^2.0.1", - "@hapi/tlds": "^1.1.1", - "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.0.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/jose": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", @@ -11696,19 +11343,20 @@ "version": "3.7.8", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "dev": true, "license": "BSD-3-Clause" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -11730,14 +11378,6 @@ "node": ">=6" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -11861,11 +11501,6 @@ "setimmediate": "^1.0.5" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" - }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", @@ -11878,12 +11513,12 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -11931,6 +11566,7 @@ "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -11963,6 +11599,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -11983,6 +11620,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12003,6 +11641,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12023,6 +11662,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12043,6 +11683,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12063,6 +11704,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12083,6 +11725,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12103,6 +11746,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12123,6 +11767,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12143,6 +11788,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12163,6 +11809,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -12316,24 +11963,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/localforage": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", - "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "license": "Apache-2.0", - "dependencies": { - "lie": "3.1.1" - } - }, - "node_modules/localforage/node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/locate-path": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", @@ -12351,15 +11980,17 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, "license": "MIT" }, "node_modules/lodash.get": { @@ -12552,15 +12183,11 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" - }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -12571,6 +12198,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -12593,6 +12221,7 @@ "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dev": true, "license": "MIT", "dependencies": { "fault": "^1.0.0", @@ -12617,6 +12246,7 @@ "version": "0.525.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "dev": true, "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -12626,6 +12256,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -12635,6 +12266,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz", "integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" @@ -12644,6 +12276,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz", "integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" @@ -12743,6 +12376,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -12753,6 +12387,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "dev": true, "license": "MIT", "peer": true, "bin": { @@ -12789,6 +12424,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12805,6 +12441,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -12817,6 +12454,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12841,6 +12479,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dev": true, "license": "MIT", "dependencies": { "mdast-util-from-markdown": "^2.0.0", @@ -12860,6 +12499,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12877,6 +12517,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12894,6 +12535,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12909,6 +12551,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12926,6 +12569,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -12942,6 +12586,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -12960,6 +12605,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -12984,6 +12630,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree-jsx": "^1.0.0", @@ -13002,6 +12649,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -13013,9 +12661,10 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -13037,6 +12686,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -13058,6 +12708,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0" @@ -13067,21 +12718,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/media-chrome": { - "version": "4.15.1", - "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.15.1.tgz", - "integrity": "sha512-Hxqr0qQ67ewmRaLJBqe5ayu53txFX+DODb9xBSHgTbw7j+gITGZ4llbPPEmqMlDnatw7IsF+AUh9rJYbpnn4ZQ==", - "license": "MIT", - "dependencies": { - "ce-la-react": "^0.3.0" - } - }, - "node_modules/media-tracks": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/media-tracks/-/media-tracks-0.3.3.tgz", - "integrity": "sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w==", - "license": "MIT" - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -13120,6 +12756,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz", "integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" @@ -13147,6 +12784,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13182,6 +12820,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13216,6 +12855,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, "license": "MIT", "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", @@ -13236,6 +12876,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", @@ -13252,6 +12893,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -13272,6 +12914,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -13290,6 +12933,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -13307,6 +12951,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" @@ -13320,6 +12965,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -13337,6 +12983,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13358,6 +13005,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13380,6 +13028,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13400,6 +13049,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13422,6 +13072,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13444,6 +13095,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13464,6 +13116,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13483,6 +13136,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13504,6 +13158,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13524,6 +13179,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13543,6 +13199,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13565,6 +13222,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13581,6 +13239,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13597,6 +13256,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13616,6 +13276,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13635,6 +13296,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13656,6 +13318,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13678,6 +13341,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13694,6 +13358,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -13788,18 +13453,16 @@ } }, "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "license": "BlueOak-1.0.0", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -13942,13 +13605,14 @@ "license": "MIT" }, "node_modules/monaco-editor": { - "version": "0.54.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", - "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { - "dompurify": "3.1.7", + "dompurify": "3.2.7", "marked": "14.0.0" } }, @@ -13959,41 +13623,24 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.18" }, "engines": { "node": ">= 10.16.0" - } - }, - "node_modules/multer/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" }, - "bin": { - "mkdirp": "bin/cmd.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/mux-embed": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/mux-embed/-/mux-embed-5.9.0.tgz", - "integrity": "sha512-wmunL3uoPhma/tWy8PrDPZkvJpXvSFBwbD3KkC4PG8Ztjfb1X3hRJwGUAQyRz7z99b/ovLm2UTTitrkvStjH4w==", - "license": "MIT" - }, "node_modules/nan": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", @@ -14038,12 +13685,6 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, - "node_modules/native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==", - "license": "MIT" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14064,6 +13705,7 @@ "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", @@ -14138,6 +13780,364 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/node-gyp/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/node-gyp/node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-gyp/node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -14183,14 +14183,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -14474,6 +14466,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/pako": { @@ -14499,6 +14492,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", @@ -14518,6 +14512,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, "license": "MIT" }, "node_modules/parse-json": { @@ -14548,12 +14543,6 @@ "node": ">= 0.8" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -14578,6 +14567,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14587,6 +14577,7 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -14603,21 +14594,23 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, "license": "ISC" }, "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -14628,6 +14621,7 @@ "version": "5.4.296", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=20.16.0 || >=22.3.0" @@ -14662,12 +14656,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "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": { @@ -14690,31 +14685,6 @@ "node": ">=0.10" } }, - "node_modules/player.style": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/player.style/-/player.style-0.3.0.tgz", - "integrity": "sha512-ny1TbqA2ZsUd6jzN+F034+UMXVK7n5SrwepsrZ2gIqVz00Hn0ohCUbbUdst/2IOFCy0oiTbaOXkSFxRw1RmSlg==", - "license": "MIT", - "workspaces": [ - ".", - "site", - "examples/*", - "scripts/*", - "themes/*" - ], - "dependencies": { - "media-chrome": "~4.14.0" - } - }, - "node_modules/player.style/node_modules/media-chrome": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.14.0.tgz", - "integrity": "sha512-IEdFb4blyF15vLvQzLIn6USJBv7Kf2ne+TfLQKBYI5Z0f9VEBVZz5MKy4Uhi0iA9lStl2S9ENIujJRuJIa5OiA==", - "license": "MIT", - "dependencies": { - "ce-la-react": "^0.3.0" - } - }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -14743,6 +14713,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -14771,6 +14742,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -14871,6 +14843,7 @@ "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14927,6 +14900,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -14938,52 +14912,39 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proto3-json-serializer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", - "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", - "dependencies": { - "protobufjs": "^7.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14998,10 +14959,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pump": { "version": "3.0.3", @@ -15189,12 +15153,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -15247,20 +15211,40 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "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.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/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/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -15295,6 +15279,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15304,6 +15289,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-cytoscapejs/-/react-cytoscapejs-2.0.0.tgz", "integrity": "sha512-t3SSl1DQy7+JQjN+8QHi1anEJlM3i3aAeydHTsJwmjo/isyKK7Rs7oCvU6kZsB9NwZidzZQR21Vm2PcBLG/Tjg==", + "dev": true, "license": "MIT", "dependencies": { "prop-types": "^15.8.1" @@ -15317,6 +15303,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" @@ -15329,6 +15316,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -15343,6 +15331,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^2.1.1", @@ -15361,6 +15350,7 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/react-h5-audio-player/-/react-h5-audio-player-3.10.1.tgz", "integrity": "sha512-r6fSj9WXR6af1kxH5qQ/tawwDK4KrMfayiVCUettLYGX/KZ3BH8OGuaZP4O5KD0AxwsKAXtBv4kVQCWFzaIrUA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.2", @@ -15375,6 +15365,7 @@ "version": "7.66.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -15391,6 +15382,7 @@ "version": "15.7.4", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.27.6", @@ -15417,6 +15409,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "*" @@ -15426,6 +15419,7 @@ "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "dev": true, "license": "MIT", "peer": true }, @@ -15433,6 +15427,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -15460,6 +15455,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.2.0.tgz", "integrity": "sha512-zk0DIL31oCh8cuQycM0SJKfwh4Onz0/Nwi6wTOjgtEjWGUY6eM+/vuzvOP3j70qtEULn7m1JtaeGzud1w5fY2Q==", + "dev": true, "license": "MIT", "dependencies": { "clsx": "^2.0.0", @@ -15489,39 +15485,18 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/react-photo-view/-/react-photo-view-1.2.7.tgz", "integrity": "sha512-MfOWVPxuibncRLaycZUNxqYU8D9IA+rbGDDaq6GM8RIoGJal592hEJoRAyRSI7ZxyyJNJTLMUWWL3UIXHJJOpw==", + "dev": true, "license": "Apache-2.0", "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, - "node_modules/react-player": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/react-player/-/react-player-3.3.3.tgz", - "integrity": "sha512-6U2ziVohA3WLdKI/WEQ7v27CIive0TCNIro55lJZka06fjB2kC4lJqBrvddG0yBvTDcn1owiUf2hRNaIzHAjIg==", - "license": "MIT", - "dependencies": { - "@mux/mux-player-react": "^3.6.0", - "cloudflare-video-element": "^1.3.4", - "dash-video-element": "^0.2.0", - "hls-video-element": "^1.5.8", - "spotify-audio-element": "^1.0.3", - "tiktok-video-element": "^0.1.1", - "twitch-video-element": "^0.1.4", - "vimeo-video-element": "^1.5.5", - "wistia-video-element": "^1.3.4", - "youtube-video-element": "^1.6.2" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18 || ^19", - "react": "^17.0.2 || ^18 || ^19", - "react-dom": "^17.0.2 || ^18 || ^19" - } - }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dev": true, "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -15555,6 +15530,7 @@ "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "dev": true, "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.7", @@ -15580,6 +15556,7 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dev": true, "license": "MIT", "dependencies": { "react-style-singleton": "^2.2.2", @@ -15602,6 +15579,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.1.3.tgz", "integrity": "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw==", + "dev": true, "license": "MIT", "dependencies": { "prop-types": "15.x", @@ -15616,6 +15594,7 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz", "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", @@ -15626,6 +15605,7 @@ "version": "3.8.132", "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.132.tgz", "integrity": "sha512-GoXK+6SRu72Jn8qT8fy+PxstIdZEACyIi/7zy0qXcrB6EJaN6zZk0/w3Sv3ALLwXqQd/3t3yUL4DQOwoNO1cbw==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", @@ -15636,6 +15616,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dev": true, "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", @@ -15658,6 +15639,7 @@ "version": "15.6.6", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -15675,6 +15657,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/react-xtermjs/-/react-xtermjs-1.0.10.tgz", "integrity": "sha512-+xpKEKbmsypWzRKE0FR1LNIGcI8gx+R6VMHe8IQW7iTbgeqp3Qg7SbiVNOzR+Ovb1QK4DPA3KqsIQV+XP0iRUA==", + "dev": true, "license": "ISC", "peerDependencies": { "@xterm/xterm": "^5.5.0" @@ -15718,6 +15701,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz", "integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==", + "dev": true, "license": "MIT", "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", @@ -15745,12 +15729,14 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true, "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, "license": "MIT", "peerDependencies": { "redux": "^5.0.0" @@ -15760,6 +15746,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dev": true, "license": "MIT", "dependencies": { "hastscript": "^6.0.0", @@ -15775,6 +15762,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -15785,6 +15773,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -15795,6 +15784,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -15805,6 +15795,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -15815,6 +15806,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", @@ -15829,6 +15821,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -15839,6 +15832,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -15849,6 +15843,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, "license": "MIT", "dependencies": { "character-entities": "^1.0.0", @@ -15867,6 +15862,7 @@ "version": "1.27.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -15876,6 +15872,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -15894,6 +15891,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -15910,6 +15908,7 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -15927,6 +15926,7 @@ "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -15985,12 +15985,14 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true, "license": "MIT" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true, "license": "MIT" }, "node_modules/resolve-alpn": { @@ -16050,18 +16052,6 @@ "node": ">= 4" } }, - "node_modules/retry-request": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.2.tgz", - "integrity": "sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==", - "dependencies": { - "extend": "^3.0.2", - "teeny-request": "^10.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -16117,9 +16107,10 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -16132,28 +16123,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" } }, @@ -16201,6 +16195,7 @@ "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -16233,9 +16228,9 @@ "license": "MIT" }, "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", "dev": true, "license": "WTFPL OR ISC", "dependencies": { @@ -16243,16 +16238,20 @@ } }, "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -16351,6 +16350,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -16363,6 +16363,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16457,6 +16458,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -16595,6 +16597,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", @@ -16615,6 +16618,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -16635,6 +16639,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -16663,12 +16668,6 @@ "node": ">= 10.x" } }, - "node_modules/spotify-audio-element": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/spotify-audio-element/-/spotify-audio-element-1.0.3.tgz", - "integrity": "sha512-I1/qD8cg/UnTlCIMiKSdZUJTyYfYhaqFK7LIVElc48eOqUUbVCaw1bqL8I6mJzdMJTh3eoNyF/ewvB7NoS/g9A==", - "license": "MIT" - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -16721,6 +16720,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "dev": true, "license": "MIT" }, "node_modules/statuses": { @@ -16732,19 +16732,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-events": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", - "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", - "dependencies": { - "stubs": "^3.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -16797,6 +16784,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -16811,6 +16799,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16820,6 +16809,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -16853,6 +16843,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -16867,6 +16858,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -16883,6 +16875,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -16895,6 +16888,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16913,21 +16907,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stubs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==" - }, "node_modules/style-mod": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, "license": "MIT" }, "node_modules/style-to-js": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz", "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==", + "dev": true, "license": "MIT", "dependencies": { "style-to-object": "1.0.11" @@ -16937,6 +16928,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz", "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==", + "dev": true, "license": "MIT", "dependencies": { "inline-style-parser": "0.2.4" @@ -16955,12 +16947,6 @@ "node": ">= 8.0" } }, - "node_modules/super-media-element": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/super-media-element/-/super-media-element-1.4.2.tgz", - "integrity": "sha512-9pP/CVNp4NF2MNlRzLwQkjiTgKKe9WYXrLh9+8QokWmMxz+zt2mf1utkWLco26IuA3AfVcTb//qtlTIjY3VHxA==", - "license": "MIT" - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17026,19 +17012,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/swagger-jsdoc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/swagger-jsdoc/node_modules/yaml": { "version": "2.0.0-1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", @@ -17066,6 +17039,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -17076,12 +17050,14 @@ "version": "4.1.16", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -17174,56 +17150,6 @@ "dev": true, "license": "ISC" }, - "node_modules/teeny-request": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz", - "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==", - "dependencies": { - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^3.3.2", - "stream-events": "^1.0.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/teeny-request/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==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/teeny-request/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/teeny-request/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==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -17314,12 +17240,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tiktok-video-element": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/tiktok-video-element/-/tiktok-video-element-0.1.1.tgz", - "integrity": "sha512-BaiVzvNz2UXDKTdSrXzrNf4q6Ecc+/utYUh7zdEu2jzYcJVDoqYbVfUl0bCfMoOeeAqg28vD/yN63Y3E9jOrlA==", - "license": "MIT" - }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -17344,6 +17264,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, "license": "MIT" }, "node_modules/tinyexec": { @@ -17357,6 +17278,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -17373,6 +17295,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -17387,9 +17310,10 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -17454,6 +17378,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -17464,6 +17389,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -17497,6 +17423,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/tunnel-agent": { @@ -17517,12 +17444,6 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", "license": "Unlicense" }, - "node_modules/twitch-video-element": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/twitch-video-element/-/twitch-video-element-0.1.4.tgz", - "integrity": "sha512-SDpZ4f7sZmwHF6XG5PF0KWuP18pH/kNG04MhTcpqJby7Lk/D3TS/lCYd+RSg0rIAAVi1LDgSIo1yJs9kmHlhgw==", - "license": "MIT" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -17594,7 +17515,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -17628,36 +17549,11 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/ua-parser-js": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -17677,6 +17573,7 @@ "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17722,6 +17619,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -17735,6 +17633,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -17748,6 +17647,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -17761,6 +17661,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17776,6 +17677,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17850,6 +17752,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -17871,6 +17774,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dev": true, "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", @@ -17893,6 +17797,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -17958,6 +17863,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17972,6 +17878,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -17986,6 +17893,7 @@ "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "dev": true, "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -18004,22 +17912,14 @@ "d3-timer": "^3.0.1" } }, - "node_modules/vimeo-video-element": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vimeo-video-element/-/vimeo-video-element-1.6.0.tgz", - "integrity": "sha512-Vs+WWvd6ph6FtY+DqrVO5OHUUS02An87QydUcCUtAsIiXnYhZl0yiDC0XxWiFluo+S6keG38i4xCaQfS1LgeSg==", - "license": "MIT", - "dependencies": { - "@vimeo/player": "2.29.0" - } - }, "node_modules/vite": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", - "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -18091,6 +17991,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -18105,9 +18006,10 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -18120,6 +18022,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18129,31 +18032,14 @@ "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, "license": "MIT" }, - "node_modules/wait-on": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.1.tgz", - "integrity": "sha512-noeCAI+XbqWMXY23sKril0BSURhuLYarkVXwJv1uUWwoojZJE7pmX3vJ7kh7SZaNgPGzfsCSQIZM/AGvu0Q9pA==", - "license": "MIT", - "dependencies": { - "axios": "^1.12.2", - "joi": "^18.0.1", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "rxjs": "^7.8.2" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" @@ -18169,15 +18055,6 @@ "defaults": "^1.0.3" } }, - "node_modules/weakmap-polyfill": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/weakmap-polyfill/-/weakmap-polyfill-2.0.4.tgz", - "integrity": "sha512-ZzxBf288iALJseijWelmECm/1x7ZwQn3sMYIkDr2VvZp7r6SEKuT8D0O9Wiq6L9Nl5mazrOMcmiZE/2NCenaxw==", - "license": "MIT", - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -18191,6 +18068,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -18208,15 +18086,6 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, - "node_modules/wistia-video-element": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/wistia-video-element/-/wistia-video-element-1.3.4.tgz", - "integrity": "sha512-2l22oaQe4jUfi3yvsh2m2oCEgvbqTzaSYx6aJnZAvV5hlMUJlyZheFUnaj0JU2wGlHdVGV7xNY+5KpKu+ruLYA==", - "license": "MIT", - "dependencies": { - "super-media-element": "~1.4.2" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -18231,6 +18100,7 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -18249,6 +18119,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -18266,6 +18137,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18275,6 +18147,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -18287,6 +18160,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -18299,12 +18173,14 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, "license": "MIT" }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -18359,6 +18235,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4" @@ -18368,6 +18245,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -18381,22 +18259,26 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "devOptional": true, + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -18415,6 +18297,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -18444,37 +18327,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yocto-spinner": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.0.0.tgz", - "integrity": "sha512-VPX8P/+Z2Fnpx8PC/JELbxp3QRrBxjAekio6yulGtA5gKt9YyRc5ycCb+NHgZCbZ0kx9KxwZp7gC6UlrCcCdSQ==", - "dependencies": { - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18.19" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/youtube-video-element": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/youtube-video-element/-/youtube-video-element-1.6.2.tgz", - "integrity": "sha512-YHDIOAqgRpfl1Ois9HcB8UFtWOxK8KJrV5TXpImj4BKYP1rWT04f/fMM9tQ9SYZlBKukT7NR+9wcI3UpB5BMDQ==", - "license": "MIT" - }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", @@ -18511,6 +18363,7 @@ "version": "4.1.12", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -18520,6 +18373,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index 29a0f2c4..1b132d23 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "termix", "private": true, - "version": "2.0.0", + "version": "2.1.0", "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities", "author": "Karmaa", "main": "electron/main.cjs", @@ -31,11 +31,42 @@ "build:mac": "npm run build && npm run electron:rebuild && electron-builder --mac --universal" }, "dependencies": { + "axios": "^1.10.0", + "bcryptjs": "^3.0.2", + "better-sqlite3": "^12.2.0", + "body-parser": "^1.20.2", + "chalk": "^4.1.2", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "dotenv": "^17.2.0", + "drizzle-orm": "^0.44.3", + "express": "^5.1.0", + "guacamole-lite": "^1.2.0", + "https-proxy-agent": "^7.0.6", + "js-yaml": "^4.1.1", + "jose": "^5.2.3", + "jsonwebtoken": "^9.0.2", + "jszip": "^3.10.1", + "multer": "^2.0.2", + "nanoid": "^5.1.5", + "node-fetch": "^3.3.2", + "qrcode": "^1.5.4", + "socks": "^2.8.7", + "speakeasy": "^2.0.0", + "ssh2": "^1.16.0", + "ws": "^8.18.3" + }, + "devDependencies": { "@codemirror/autocomplete": "^6.18.7", "@codemirror/commands": "^6.3.3", "@codemirror/search": "^6.5.11", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.23.1", + "@commitlint/cli": "^20.1.0", + "@commitlint/config-conventional": "^20.0.0", + "@electron/notarize": "^2.5.0", + "@electron/rebuild": "^3.7.2", + "@eslint/js": "^9.34.0", "@hookform/resolvers": "^5.1.1", "@monaco-editor/react": "^4.7.0", "@radix-ui/react-accordion": "^1.2.11", @@ -56,51 +87,52 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.14", "@types/bcryptjs": "^2.4.6", + "@types/better-sqlite3": "^7.6.13", "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", "@types/cytoscape": "^3.21.9", + "@types/express": "^5.0.3", "@types/guacamole-common-js": "^1.5.5", + "@types/js-yaml": "^4.0.9", + "@types/jsonwebtoken": "^9.0.10", "@types/jszip": "^3.4.0", "@types/multer": "^2.0.0", + "@types/node": "^24.3.0", "@types/qrcode": "^1.5.5", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", "@types/react-grid-layout": "^1.3.6", "@types/speakeasy": "^2.0.10", + "@types/ssh2": "^1.15.5", + "@types/ws": "^8.18.1", "@uiw/codemirror-extensions-langs": "^4.24.1", "@uiw/codemirror-theme-github": "^4.25.4", "@uiw/react-codemirror": "^4.24.1", + "@vitejs/plugin-react": "^4.3.4", "@xterm/addon-clipboard": "^0.2.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/xterm": "^5.5.0", - "axios": "^1.10.0", - "bcryptjs": "^3.0.2", - "better-sqlite3": "^12.2.0", - "body-parser": "^1.20.2", - "chalk": "^4.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", + "concurrently": "^9.2.1", "cytoscape": "^3.33.1", - "dotenv": "^17.2.0", - "drizzle-orm": "^0.44.3", - "express": "^5.1.0", + "electron": "^38.0.0", + "electron-builder": "^26.0.12", + "eslint": "^9.34.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", "guacamole-common-js": "^1.5.0", - "guacamole-lite": "^1.2.0", - "https-proxy-agent": "^7.0.6", - "i18n-auto-translation": "^2.2.3", + "husky": "^9.1.7", "i18next": "^25.4.2", "i18next-browser-languagedetector": "^8.2.0", - "jose": "^5.2.3", - "jsonwebtoken": "^9.0.2", - "jszip": "^3.10.1", + "lint-staged": "^16.2.3", "lucide-react": "^0.525.0", - "multer": "^2.0.2", - "nanoid": "^5.1.5", "next-themes": "^0.4.6", - "node-fetch": "^3.3.2", - "qrcode": "^1.5.4", + "prettier": "3.6.2", "react": "^19.1.0", "react-cytoscapejs": "^2.0.0", "react-dom": "^19.1.0", @@ -112,53 +144,20 @@ "react-markdown": "^10.1.0", "react-pdf": "^10.1.0", "react-photo-view": "^1.2.7", - "react-player": "^3.3.3", "react-resizable-panels": "^3.0.3", "react-simple-keyboard": "^3.8.120", "react-syntax-highlighter": "^15.6.6", "react-xtermjs": "^1.0.10", "recharts": "^3.2.1", "remark-gfm": "^4.0.1", - "socks": "^2.8.7", "sonner": "^2.0.7", - "speakeasy": "^2.0.0", - "ssh2": "^1.16.0", + "swagger-jsdoc": "^6.2.8", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.14", - "wait-on": "^9.0.1", - "ws": "^8.18.3", - "zod": "^4.0.5" - }, - "devDependencies": { - "@commitlint/cli": "^20.1.0", - "@commitlint/config-conventional": "^20.0.0", - "@electron/notarize": "^2.5.0", - "@electron/rebuild": "^3.7.2", - "@eslint/js": "^9.34.0", - "@types/better-sqlite3": "^7.6.13", - "@types/cors": "^2.8.19", - "@types/express": "^5.0.3", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^24.3.0", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@types/ssh2": "^1.15.5", - "@types/ws": "^8.18.1", - "@vitejs/plugin-react": "^4.3.4", - "concurrently": "^9.2.1", - "electron": "^38.0.0", - "electron-builder": "^26.0.12", - "eslint": "^9.34.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", - "husky": "^9.1.7", - "lint-staged": "^16.2.3", - "prettier": "3.6.2", - "swagger-jsdoc": "^6.2.8", "typescript": "~5.9.2", "typescript-eslint": "^8.40.0", - "vite": "^7.1.5" + "vite": "^7.1.5", + "zod": "^4.0.5" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/public/sw.js b/public/sw.js index b1d1799f..a15adc8d 100644 --- a/public/sw.js +++ b/public/sw.js @@ -55,8 +55,8 @@ self.addEventListener("fetch", (event) => { } if ( - url.pathname.startsWith("/ssh/opkssh-chooser/") || - url.pathname.startsWith("/ssh/opkssh-callback/") + url.pathname.startsWith("/host/opkssh-chooser/") || + url.pathname.startsWith("/host/opkssh-callback/") ) { return; } diff --git a/readme/README-AR.md b/readme/README-AR.md index ce7c53c7..205818eb 100644 --- a/readme/README-AR.md +++ b/readme/README-AR.md @@ -44,33 +44,33 @@ Termix Banner

-Termix هي منصة مفتوحة المصدر ومجانية للأبد وذاتية الاستضافة لإدارة الخوادم بشكل شامل. توفر حلاً متعدد المنصات لإدارة خوادمك وبنيتك التحتية من خلال واجهة واحدة وسهلة الاستخدام. يوفر Termix الوصول إلى طرفية SSH، وقدرات إنشاء أنفاق SSH، وإدارة الملفات عن بُعد، والعديد من الأدوات الأخرى. يُعد Termix البديل المثالي المجاني وذاتي الاستضافة لـ Termius المتاح لجميع المنصات. +Termix هي منصة مفتوحة المصدر ومجانية للأبد وذاتية الاستضافة لإدارة الخوادم بشكل شامل. توفر حلاً متعدد المنصات لإدارة خوادمك وبنيتك التحتية من خلال واجهة واحدة وسهلة الاستخدام. يوفر Termix الوصول إلى طرفية SSH، والتحكم في سطح المكتب البعيد (RDP، VNC، Telnet)، وقدرات إنشاء أنفاق SSH، وإدارة ملفات SSH عن بُعد، والعديد من الأدوات الأخرى. يُعد Termix البديل المثالي المجاني وذاتي الاستضافة لـ Termius المتاح لجميع المنصات. # الميزات -- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك السمات الشائعة والخطوط والمكونات الأخرى -- **الوصول إلى سطح المكتب البعيد** - دعم RDP و VNC و Telnet عبر المتصفح مع تخصيص كامل وتقسيم الشاشة -- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r -- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo -- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها -- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH -- **إحصائيات الخادم** - عرض استخدام المعالج والذاكرة والقرص إلى جانب الشبكة ووقت التشغيل ومعلومات النظام وجدار الحماية ومراقب المنافذ على معظم الخوادم المبنية على Linux -- **لوحة التحكم** - عرض معلومات الخادم بنظرة واحدة على لوحة التحكم -- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار -- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً -- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات -- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات -- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS -- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين الوضع الداكن أو الفاتح. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة -- **اللغات** - دعم مدمج لحوالي 30 لغة (تُدار بواسطة [Crowdin](https://docs.termix.site/translations)) -- **دعم المنصات** - متاح كتطبيق ويب، وتطبيق سطح مكتب (Windows و Linux و macOS)، و PWA، وتطبيق مخصص للهاتف المحمول/الجهاز اللوحي لـ iOS و Android -- **أدوات SSH** - إنشاء مقتطفات أوامر قابلة لإعادة الاستخدام تُنفَّذ بنقرة واحدة. تشغيل أمر واحد في وقت واحد عبر عدة طرفيات مفتوحة -- **سجل الأوامر** - الإكمال التلقائي وعرض أوامر SSH التي تم تنفيذها سابقاً -- **الاتصال السريع** - الاتصال بخادم دون الحاجة إلى حفظ بيانات الاتصال -- **لوحة الأوامر** - اضغط مرتين على Shift الأيسر للوصول السريع إلى اتصالات SSH باستخدام لوحة المفاتيح -- **ميزات SSH الغنية** - دعم مضيفات القفز، Warpgate، الاتصالات المبنية على TOTP، SOCKS5، التحقق من مفتاح المضيف، الملء التلقائي لكلمة المرور، [OPKSSH](https://github.com/openpubkey/opkssh)، وغيرها -- **الرسم البياني للشبكة** - تخصيص لوحة التحكم لتصور مختبرك المنزلي بناءً على اتصالات SSH مع دعم الحالة -- **علامات التبويب الدائمة** - تبقى جلسات SSH وعلامات التبويب مفتوحة عبر الأجهزة/التحديثات إذا تم تفعيلها في ملف تعريف المستخدم +- **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك سمات الطرفية الشائعة والخطوط والمكونات الأخرى. +- **الوصول إلى سطح المكتب البعيد** - دعم RDP و VNC و Telnet عبر المتصفح مع تخصيص كامل وتقسيم الشاشة. +- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r. +- **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo. +- **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها. +- **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH. +- **إحصائيات الخادم** - عرض استخدام المعالج والذاكرة والقرص إلى جانب الشبكة ووقت التشغيل ومعلومات النظام وجدار الحماية ومراقب المنافذ على معظم الخوادم المبنية على Linux. +- **لوحة التحكم** - عرض معلومات الخادم بنظرة واحدة على لوحة التحكم. +- **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار. +- **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC (مع التحكم في الوصول) و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً. +- **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات. +- **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات. +- **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS. +- **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين العديد من سمات واجهة المستخدم بما في ذلك الفاتح والداكن و Dracula وغيرها. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة. +- **اللغات** - دعم مدمج لحوالي 30 لغة (تُدار بواسطة [Crowdin](https://docs.termix.site/translations)). +- **دعم المنصات** - متاح كتطبيق ويب، وتطبيق سطح مكتب (Windows و Linux و macOS، يمكن تشغيله بشكل مستقل بدون خادم Termix الخلفي)، و PWA، وتطبيق مخصص للهاتف المحمول/الجهاز اللوحي لـ iOS و Android. +- **أدوات SSH** - إنشاء مقتطفات أوامر قابلة لإعادة الاستخدام تُنفَّذ بنقرة واحدة. تشغيل أمر واحد في وقت واحد عبر عدة طرفيات مفتوحة. +- **سجل الأوامر** - الإكمال التلقائي وعرض أوامر SSH التي تم تنفيذها سابقاً. +- **الاتصال السريع** - الاتصال بخادم دون الحاجة إلى حفظ بيانات الاتصال. +- **لوحة الأوامر** - اضغط مرتين على Shift الأيسر للوصول السريع إلى اتصالات SSH باستخدام لوحة المفاتيح. +- **ميزات SSH الغنية** - دعم مضيفات القفز، Warpgate، الاتصالات المبنية على TOTP، SOCKS5، التحقق من مفتاح المضيف، الملء التلقائي لكلمة المرور، [OPKSSH](https://github.com/openpubkey/opkssh)، tmux، port knocking، إلخ. +- **الرسم البياني للشبكة** - تخصيص لوحة التحكم لتصور مختبرك المنزلي بناءً على اتصالات SSH مع دعم الحالة. +- **علامات التبويب الدائمة** - تبقى جلسات SSH وعلامات التبويب مفتوحة عبر الأجهزة/التحديثات إذا تم تفعيلها في ملف تعريف المستخدم. # الميزات المخططة @@ -102,7 +102,7 @@ Termix هي منصة مفتوحة المصدر ومجانية للأبد وذا - Google Play Store - APK -قم بزيارة [وثائق](https://docs.termix.site/install) Termix للحصول على مزيد من المعلومات حول كيفية تثبيت Termix على جميع المنصات. بخلاف ذلك، يمكنك الاطلاع على نموذج ملف Docker Compose هنا: +قم بزيارة [وثائق](https://docs.termix.site/install) Termix للحصول على مزيد من المعلومات حول كيفية تثبيت Termix على جميع المنصات. بخلاف ذلك، يمكنك الاطلاع على نموذج ملف Docker Compose هنا (يمكنك حذف guacd والشبكة إذا كنت لا تخطط لاستخدام ميزات سطح المكتب البعيد): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

diff --git a/readme/README-CN.md b/readme/README-CN.md index 3afa78a8..53dbd30b 100644 --- a/readme/README-CN.md +++ b/readme/README-CN.md @@ -25,7 +25,7 @@

Repo of the Day Achievement
- 2025年9月1日获得 + 获得于 2025年9月1日


@@ -44,66 +44,65 @@ Termix Banner

-Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个多平台解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix -提供 SSH 终端访问、远程桌面控制(RDP、VNC、Telnet)、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。 +Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个多平台解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix 提供 SSH 终端访问、远程桌面控制(RDP、VNC、Telnet)、SSH 隧道功能、远程 SSH 文件管理以及许多其他工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。 # 功能 -- **SSH 终端访问** - 功能齐全的终端,具有分屏支持(最多 4 个面板)和类似浏览器的选项卡系统。包括对自定义终端的支持,包括常见终端主题、字体和其他组件 -- **远程桌面访问** - 通过浏览器支持 RDP、VNC 和 Telnet,具有完整的自定义和分屏功能 -- **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能,支持 -l 或 -r 连接 -- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件,支持 sudo -- **Docker 管理** - 启动、停止、暂停、删除容器。查看容器统计信息。使用 docker exec 终端控制容器。它不是用来替代 Portainer 或 Dockge,而是用于简单管理你的容器而不是创建它们 -- **SSH 主机管理器** - 保存、组织和管理您的 SSH 连接,支持标签和文件夹,并轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥 -- **服务器统计** - 在大多数 Linux 服务器上查看 CPU、内存和磁盘使用情况以及网络、正常运行时间、系统信息、防火墙、端口监控 -- **仪表板** - 在仪表板上一目了然地查看服务器信息 -- **RBAC** - 创建角色并在用户/角色之间共享主机 -- **用户认证** - 安全的用户管理,具有管理员控制以及 OIDC 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地帐户链接在一起 -- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多信息 -- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据 -- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向 -- **现代用户界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁的桌面/移动设备友好界面。可选择基于深色或浅色模式的用户界面。使用 URL 路由以全屏方式打开任何连接 -- **语言** - 内置支持约 30 种语言(由 [Crowdin](https://docs.termix.site/translations) 管理) -- **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 macOS)、PWA 以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序 -- **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令 -- **命令历史** - 自动完成并查看以前运行的 SSH 命令 -- **快速连接** - 无需保存连接数据即可连接到服务器 -- **命令面板** - 双击左 Shift 键可快速使用键盘访问 SSH 连接 -- **SSH 功能丰富** - 支持跳板机、Warpgate、基于 TOTP 的连接、SOCKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.com/openpubkey/opkssh)等 -- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,支持状态显示 -- **持久标签页** - 如果在用户配置文件中启用,SSH 会话和标签页在设备/刷新后保持打开状态 +- **SSH 终端访问** - 功能齐全的终端,支持分屏(最多 4 个面板),并配有类似浏览器的标签系统。包括对自定义终端的支持,如常用的终端主题、字体和其他组件。 +- **远程桌面访问** - 通过浏览器支持 RDP、VNC 和 Telnet,具有完整的自定义和分屏功能。 +- **SSH 隧道管理** - 创建和管理具有自动重连和健康监测功能的 SSH 隧道,支持 -l 或 -r 连接。 +- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。支持通过 sudo 无缝上传、下载、重命名、删除和移动文件。 +- **Docker 管理** - 启动、停止、暂停、移除容器。查看容器统计信息。通过 docker exec 终端控制容器。它的初衷不是取代 Portainer 或 Dockge,而是为了比直接创建容器更简单地管理它们。 +- **SSH 主机管理器** - 通过标签和文件夹保存、组织和管理您的 SSH 连接,轻松保存可重用的登录信息,并能自动化部署 SSH 密钥。 +- **服务器统计** - 在大多数基于 Linux 的服务器上查看 CPU、内存、磁盘使用情况以及网络、运行时间、系统信息、防火墙和端口监控。 +- **仪表板** - 在仪表板上一目了然地查看服务器信息。 +- **RBAC** - 创建角色并在用户/角色之间共享主机。 +- **用户认证** - 安全的用户管理,具有管理员控制、OIDC(带访问控制)和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地账户链接在一起。 +- **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多。 +- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据。 +- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向。 +- **现代 UI** - 使用 React、Tailwind CSS 和 Shadcn 构建的整洁的桌面/移动友好界面。有多种 UI 主题可选,包括浅色、深色、Dracula 等。使用 URL 路由全屏打开任何连接。 +- **语言** - 内置支持约 30 种语言(由 [Crowdin](https://docs.termix.site/translations) 管理)。 +- **平台支持** - 提供 Web 应用、桌面应用(Windows、Linux 和 macOS,可脱离 Termix 后端独立运行)、PWA 以及 iOS 和 Android 专用移动/平板应用。 +- **SSH 工具** - 创建可重用的命令片段,只需点击一下即可执行。在多个打开的终端中同时运行一个命令。 +- **命令历史** - 自动完成并查看之前运行过的 SSH 命令。 +- **快速连接** - 无需保存连接数据即可连接到服务器。 +- **命令面板** - 双击左 Shift 键即可通过键盘快速访问 SSH 连接。 +- **丰富的功能** - 支持跳转主机、Warpgate、基于 TOTP 的连接、SOCKS5、主机密钥验证、密码自动填充、[OPKSSH](https://github.com/openpubkey/opkssh)、tmux、端口敲击等。 +- **网络图** - 自定义您的仪表板,根据您的 SSH 连接可视化您的家庭实验室,并支持状态监测。 +- **持久标签页** - 如果在用户个人资料中启用,SSH 会话和标签页将在设备/刷新后保持打开状态。 # 计划功能 -查看 [项目](https://github.com/orgs/Termix-SSH/projects/2) 了解所有计划功能。如果你想贡献代码,请参阅 [贡献指南](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)。 +查看 [Projects](https://github.com/orgs/Termix-SSH/projects/2) 了解所有计划功能。如果您想贡献代码,请参阅 [Contributing](https://github.com/Termix-SSH/Termix/blob/main/CONTRIBUTING.md)。 # 安装 支持的设备: - 网站(任何平台上的任何现代浏览器,如 Chrome、Safari 和 Firefox)(包括 PWA 支持) -- Windows(x64/ia32) +- Windows (x64/ia32) - 便携版 - MSI 安装程序 - Chocolatey 软件包管理器 -- Linux(x64/ia32) +- Linux (x64/ia32) - 便携版 - AUR - AppImage - Deb - Flatpak -- macOS(x64/ia32 on v12.0+) +- macOS (x64/ia32, v12.0+) - Apple App Store - DMG - Homebrew -- iOS/iPadOS(v15.1+) +- iOS/iPadOS (v15.1+) - Apple App Store - IPA -- Android(v7.0+) +- Android (v7.0+) - Google Play 商店 - APK -访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。或者,在此处查看示例 Docker Compose 文件(如果不打算使用远程桌面功能,可以省略 guacd 和 network): +访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。此外,这里有一个示例 Docker Compose 文件(如果您不打算使用远程桌面功能,可以省略 guacd 和网络部分): ```yaml services: @@ -158,13 +157,24 @@ networks: Cloudflare +          + + TailScale + +          + + Akamai + +          + + AWS +

# 支持 -如果你需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.com/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`。 -请尽可能详细地描述你的问题,最好使用英语。你也可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 服务器并访问支持 -频道,但响应时间可能较长。 +如果您需要 Termix 的帮助或想要请求功能,请访问 [Issues](https://github.com/Termix-SSH/Support/issues) 页面,登录并点击 `New Issue`。 +请尽可能详细地描述您的问题,建议使用英语。您也可以加入 [Discord](https://discord.gg/jVQGdvHDrf) 服务器并访问支持频道,但响应时间可能较长。 # 展示 @@ -200,7 +210,7 @@ networks: Termix Demo 12

-某些视频和图像可能已过时或可能无法完美展示功能。 +某些视频和图像可能已过时,或者可能无法完美展示功能。 # 许可证 diff --git a/readme/README-DE.md b/readme/README-DE.md index 9567a164..cccbd6ed 100644 --- a/readme/README-DE.md +++ b/readme/README-DE.md @@ -44,33 +44,33 @@ Wenn Sie möchten, können Sie das Projekt hier unterstützen!\ Termix Banner

-Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-Serververwaltungsplattform. Sie bietet eine plattformübergreifende Lösung zur Verwaltung Ihrer Server und Infrastruktur über eine einzige, intuitive Oberfläche. Termix bietet SSH-Terminalzugriff, SSH-Tunneling-Funktionen, Remote-Dateiverwaltung und viele weitere Werkzeuge. Termix ist die perfekte kostenlose und selbst gehostete Alternative zu Termius, verfügbar für alle Plattformen. +Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-Serververwaltungsplattform. Sie bietet eine plattformübergreifende Lösung zur Verwaltung Ihrer Server und Infrastruktur über eine einzige, intuitive Oberfläche. Termix bietet SSH-Terminalzugriff, Remote-Desktop-Steuerung (RDP, VNC, Telnet), SSH-Tunneling-Funktionen, Remote-SSH-Dateiverwaltung und viele weitere Werkzeuge. Termix ist die perfekte kostenlose und selbst gehostete Alternative zu Termius, verfügbar für alle Plattformen. # Funktionen -- **SSH-Terminalzugriff** - Voll ausgestattetes Terminal mit Split-Screen-Unterstützung (bis zu 4 Panels) mit einem browserähnlichen Tab-System. Enthält Unterstützung für die Anpassung des Terminals einschließlich gängiger Terminal-Themes, Schriftarten und anderer Komponenten -- **Remote-Desktop-Zugriff** - RDP-, VNC- und Telnet-Unterstützung über den Browser mit vollständiger Anpassung und Split-Screen -- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie Unterstützung für -l oder -r Verbindungen +- **SSH-Terminalzugriff** - Voll ausgestattetes Terminal mit Split-Screen-Unterstützung (bis zu 4 Panels) mit einem browserähnlichen Tab-System. Enthält Unterstützung für die Anpassung des Terminals einschließlich gängiger Terminal-Themes, Schriftarten und anderer Komponenten. +- **Remote-Desktop-Zugriff** - RDP-, VNC- und Telnet-Unterstützung über den Browser mit vollständiger Anpassung und Split-Screen. +- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie Unterstützung für -l oder -r Verbindungen. - **Remote-Dateimanager** - Verwalten Sie Dateien direkt auf Remote-Servern mit Unterstützung für das Anzeigen und Bearbeiten von Code, Bildern, Audio und Video. Laden Sie Dateien hoch, herunter, benennen Sie sie um, löschen oder verschieben Sie sie nahtlos mit Sudo-Unterstützung. - **Docker-Verwaltung** - Container starten, stoppen, pausieren, entfernen. Container-Statistiken anzeigen. Container über Docker-Exec-Terminal steuern. Es wurde nicht entwickelt, um Portainer oder Dockge zu ersetzen, sondern um Ihre Container einfach zu verwalten, anstatt sie zu erstellen. -- **SSH-Host-Manager** - Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Möglichkeit, die Bereitstellung von SSH-Schlüsseln zu automatisieren -- **Serverstatistiken** - CPU-, Arbeitsspeicher- und Festplattenauslastung sowie Netzwerk, Betriebszeit, Systeminformationen, Firewall, Port-Monitor auf den meisten Linux-basierten Servern anzeigen -- **Dashboard** - Serverinformationen auf einen Blick auf Ihrem Dashboard anzeigen -- **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen -- **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- sowie 2FA (TOTP)-Unterstützung. Aktive Benutzersitzungen über alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknüpfen. +- **SSH-Host-Manager** - Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Möglichkeit, die Bereitstellung von SSH-Schlüsseln zu automatisieren. +- **Serverstatistiken** - CPU-, Arbeitsspeicher- und Festplattenauslastung sowie Netzwerk, Betriebszeit, Systeminformationen, Firewall, Port-Monitor auf den meisten Linux-basierten Servern anzeigen. +- **Dashboard** - Serverinformationen auf einen Blick auf Ihrem Dashboard anzeigen. +- **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen. +- **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- (mit Zugriffskontrolle) sowie 2FA (TOTP)-Unterstützung. Aktive Benutzersitzungen über alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknüpfen. - **Datenbankverschlüsselung** - Backend gespeichert als verschlüsselte SQLite-Datenbankdateien. Weitere Informationen in der [Dokumentation](https://docs.termix.site/security). -- **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren -- **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen -- **Moderne Benutzeroberfläche** - Saubere desktop-/mobilfreundliche Oberfläche, erstellt mit React, Tailwind CSS und Shadcn. Wählen Sie zwischen dunklem oder hellem Modus. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen. -- **Sprachen** - Integrierte Unterstützung für ~30 Sprachen (verwaltet über [Crowdin](https://docs.termix.site/translations)) -- **Plattformunterstützung** - Verfügbar als Web-App, Desktop-Anwendung (Windows, Linux und macOS), PWA und dedizierte Mobil-/Tablet-App für iOS und Android. +- **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren. +- **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen. +- **Moderne Benutzeroberfläche** - Saubere desktop-/mobilfreundliche Oberfläche, erstellt mit React, Tailwind CSS und Shadcn. Wählen Sie zwischen vielen verschiedenen UI-Themes einschließlich Hell, Dunkel, Dracula usw. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen. +- **Sprachen** - Integrierte Unterstützung für ~30 Sprachen (verwaltet über [Crowdin](https://docs.termix.site/translations)). +- **Plattformunterstützung** - Verfügbar als Web-App, Desktop-Anwendung (Windows, Linux und macOS, kann eigenständig ohne Termix-Backend ausgeführt werden), PWA und dedizierte Mobil-/Tablet-App für iOS und Android. - **SSH-Werkzeuge** - Erstellen Sie wiederverwendbare Befehlsvorlagen, die mit einem einzigen Klick ausgeführt werden. Führen Sie einen Befehl gleichzeitig in mehreren geöffneten Terminals aus. -- **Befehlsverlauf** - Autovervollständigung und Anzeige zuvor ausgeführter SSH-Befehle -- **Schnellverbindung** - Verbinden Sie sich mit einem Server, ohne die Verbindungsdaten speichern zu müssen -- **Befehlspalette** - Doppeltippen Sie die linke Umschalttaste, um schnell auf SSH-Verbindungen mit Ihrer Tastatur zuzugreifen -- **SSH-Funktionsreich** - Unterstützt Jump-Hosts, Warpgate, TOTP-basierte Verbindungen, SOCKS5, Host-Key-Verifizierung, automatisches Ausfüllen von Passwörtern, [OPKSSH](https://github.com/openpubkey/opkssh) usw. -- **Netzwerkgraph** - Passen Sie Ihr Dashboard an, um Ihr Homelab basierend auf Ihren SSH-Verbindungen mit Statusunterstützung zu visualisieren -- **Persistente Tabs** - SSH-Sitzungen und Tabs bleiben über Geräte/Aktualisierungen hinweg offen, wenn im Benutzerprofil aktiviert +- **Befehlsverlauf** - Autovervollständigung und Anzeige zuvor ausgeführter SSH-Befehle. +- **Schnellverbindung** - Verbinden Sie sich mit einem Server, ohne die Verbindungsdaten speichern zu müssen. +- **Befehlspalette** - Doppeltippen Sie die linke Umschalttaste, um schnell auf SSH-Verbindungen mit Ihrer Tastatur zuzugreifen. +- **SSH-Funktionsreich** - Unterstützt Jump-Hosts, Warpgate, TOTP-basierte Verbindungen, SOCKS5, Host-Key-Verifizierung, automatisches Ausfüllen von Passwörtern, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, Port Knocking usw. +- **Netzwerkgraph** - Passen Sie Ihr Dashboard an, um Ihr Homelab basierend auf Ihren SSH-Verbindungen mit Statusunterstützung zu visualisieren. +- **Persistente Tabs** - SSH-Sitzungen und Tabs bleiben über Geräte/Aktualisierungen hinweg offen, wenn im Benutzerprofil aktiviert. # Geplante Funktionen @@ -102,7 +102,7 @@ Unterstützte Geräte: - Google Play Store - APK -Besuchen Sie die Termix-[Dokumentation](https://docs.termix.site/install) für weitere Informationen zur Installation von Termix auf allen Plattformen. Alternativ finden Sie hier eine Docker Compose-Beispieldatei: +Besuchen Sie die Termix-[Dokumentation](https://docs.termix.site/install) für weitere Informationen zur Installation von Termix auf allen Plattformen. Alternativ finden Sie hier eine Docker Compose-Beispieldatei (Sie können guacd und das Netzwerk weglassen, wenn Sie keine Remote-Desktop-Funktionen nutzen möchten): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

diff --git a/readme/README-ES.md b/readme/README-ES.md index 27c159cf..f0a9f029 100644 --- a/readme/README-ES.md +++ b/readme/README-ES.md @@ -44,33 +44,33 @@ Si lo desea, puede apoyar el proyecto aquí.\ Termix Banner

-Termix es una plataforma de gestión de servidores todo en uno, de código abierto, siempre gratuita y autoalojada. Proporciona una solución multiplataforma para gestionar sus servidores e infraestructura a través de una interfaz única e intuitiva. Termix ofrece acceso a terminal SSH, capacidades de túneles SSH, gestión remota de archivos y muchas otras herramientas. Termix es la alternativa perfecta, gratuita y autoalojada a Termius, disponible para todas las plataformas. +Termix es una plataforma de gestión de servidores todo en uno, de código abierto, siempre gratuita y autoalojada. Proporciona una solución multiplataforma para gestionar sus servidores e infraestructura a través de una interfaz única e intuitiva. Termix ofrece acceso a terminal SSH, control de escritorio remoto (RDP, VNC, Telnet), capacidades de túneles SSH, gestión remota de archivos SSH y muchas otras herramientas. Termix es la alternativa perfecta, gratuita y autoalojada a Termius, disponible para todas las plataformas. # Características -- **Acceso a Terminal SSH** - Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestañas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes -- **Acceso a Escritorio Remoto** - Soporte RDP, VNC y Telnet a través del navegador con personalización completa y pantalla dividida -- **Gestión de Túneles SSH** - Cree y gestione túneles SSH con reconexión automática y monitoreo de estado, con soporte para conexiones -l o -r +- **Acceso a Terminal SSH** - Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestañas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes. +- **Acceso a Escritorio Remoto** - Soporte RDP, VNC y Telnet a través del navegador con personalización completa y pantalla dividida. +- **Gestión de Túneles SSH** - Cree y gestione túneles SSH con reconexión automática y monitoreo de estado, con soporte para conexiones -l o -r. - **Gestor Remoto de Archivos** - Gestione archivos directamente en servidores remotos con soporte para visualizar y editar código, imágenes, audio y video. Suba, descargue, renombre, elimine y mueva archivos sin problemas con soporte sudo. -- **Gestión de Docker** - Inicie, detenga, pause, elimine contenedores. Vea estadísticas de contenedores. Controle contenedores usando el terminal Docker Exec. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos. -- **Gestor de Hosts SSH** - Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas, y guarde fácilmente información de inicio de sesión reutilizable con la capacidad de automatizar el despliegue de claves SSH -- **Estadísticas del Servidor** - Vea el uso de CPU, memoria y disco junto con red, tiempo de actividad, información del sistema, firewall, monitor de puertos en la mayoría de los servidores basados en Linux -- **Dashboard** - Vea la información del servidor de un vistazo en su dashboard -- **RBAC** - Cree roles y comparta hosts entre usuarios/roles -- **Autenticación de Usuarios** - Gestión segura de usuarios con controles de administrador y soporte para OIDC y 2FA (TOTP). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre sí. +- **Gestión de Docker** - Inicie, detenga, pause, elimine contenedores. Vea estadísticas de contenedores. Controle contenedores usando el terminal docker exec. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos. +- **Gestor de Hosts SSH** - Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas, y guarde fácilmente información de inicio de sesión reutilizable con la capacidad de automatizar el despliegue de claves SSH. +- **Estadísticas del Servidor** - Vea el uso de CPU, memoria y disco junto con red, tiempo de actividad, información del sistema, firewall, monitor de puertos en la mayoría de los servidores basados en Linux. +- **Dashboard** - Vea la información del servidor de un vistazo en su dashboard. +- **RBAC** - Cree roles y comparta hosts entre usuarios/roles. +- **Autenticación de Usuarios** - Gestión segura de usuarios con controles de administrador y soporte para OIDC (con control de acceso) y 2FA (TOTP). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre sí. - **Cifrado de Base de Datos** - Backend almacenado como archivos de base de datos SQLite cifrados. Consulte la [documentación](https://docs.termix.site/security) para más información. -- **Exportación/Importación de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos -- **Configuración Automática de SSL** - Generación y gestión integrada de certificados SSL con redirecciones HTTPS -- **Interfaz Moderna** - Interfaz limpia compatible con escritorio/móvil construida con React, Tailwind CSS y Shadcn. Elija entre modo oscuro o claro. Use rutas URL para abrir cualquier conexión en pantalla completa. -- **Idiomas** - Soporte integrado para ~30 idiomas (gestionado por [Crowdin](https://docs.termix.site/translations)) -- **Soporte de Plataformas** - Disponible como aplicación web, aplicación de escritorio (Windows, Linux y macOS), PWA y aplicación dedicada para móviles/tablets en iOS y Android. +- **Exportación/Importación de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos. +- **Configuración Automática de SSL** - Generación y gestión integrada de certificados SSL con redirecciones HTTPS. +- **Interfaz Moderna** - Interfaz limpia compatible con escritorio/móvil construida con React, Tailwind CSS y Shadcn. Elija entre muchos temas de UI diferentes, incluyendo claro, oscuro, Dracula, etc. Use rutas URL para abrir cualquier conexión en pantalla completa. +- **Idiomas** - Soporte integrado para ~30 idiomas (gestionado por [Crowdin](https://docs.termix.site/translations)). +- **Soporte de Plataformas** - Disponible como aplicación web, aplicación de escritorio (Windows, Linux y macOS, se puede ejecutar de forma independiente sin el backend de Termix), PWA y aplicación dedicada para móviles/tablets en iOS y Android. - **Herramientas SSH** - Cree fragmentos de comandos reutilizables que se ejecutan con un solo clic. Ejecute un comando simultáneamente en múltiples terminales abiertos. -- **Historial de Comandos** - Autocompletado y visualización de comandos SSH ejecutados anteriormente -- **Conexión Rápida** - Conéctese a un servidor sin necesidad de guardar los datos de conexión -- **Paleta de Comandos** - Pulse dos veces la tecla Shift izquierda para acceder rápidamente a las conexiones SSH con su teclado -- **SSH Rico en Funciones** - Soporta jump hosts, Warpgate, conexiones basadas en TOTP, SOCKS5, verificación de clave de host, autocompletado de contraseñas, [OPKSSH](https://github.com/openpubkey/opkssh), etc. -- **Gráfico de Red** - Personalice su Dashboard para visualizar su homelab basado en sus conexiones SSH con soporte de estado -- **Pestañas Persistentes** - Las sesiones SSH y pestañas permanecen abiertas entre dispositivos/actualizaciones si está habilitado en el perfil de usuario +- **Historial de Comandos** - Autocompletado y visualización de comandos SSH ejecutados anteriormente. +- **Conexión Rápida** - Conéctese a un servidor sin necesidad de guardar los datos de conexión. +- **Paleta de Comandos** - Pulse dos veces la tecla Shift izquierda para acceder rápidamente a las conexiones SSH con su teclado. +- **SSH Rico en Funciones** - Soporta jump hosts, Warpgate, conexiones basadas en TOTP, SOCKS5, verificación de clave de host, autocompletado de contraseñas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc. +- **Gráfico de Red** - Personalice su Dashboard para visualizar su homelab basado en sus conexiones SSH con soporte de estado. +- **Pestañas Persistentes** - Las sesiones SSH y pestañas permanecen abiertas entre dispositivos/actualizaciones si está habilitado en el perfil de usuario. # Características Planeadas @@ -102,7 +102,7 @@ Dispositivos soportados: - Google Play Store - APK -Visite la [documentación](https://docs.termix.site/install) de Termix para más información sobre cómo instalar Termix en todas las plataformas. De lo contrario, vea un archivo Docker Compose de ejemplo aquí: +Visite la [documentación](https://docs.termix.site/install) de Termix para más información sobre cómo instalar Termix en todas las plataformas. De lo contrario, vea un archivo Docker Compose de ejemplo aquí (puede omitir guacd y la red si no planea usar funciones de escritorio remoto): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

diff --git a/readme/README-FR.md b/readme/README-FR.md index 9f844a0f..adfb6b1b 100644 --- a/readme/README-FR.md +++ b/readme/README-FR.md @@ -44,33 +44,33 @@ Si vous le souhaitez, vous pouvez soutenir le projet ici !\ Termix Banner

-Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jamais gratuite et auto-hébergée. Elle fournit une solution multiplateforme pour gérer vos serveurs et votre infrastructure à travers une interface unique et intuitive. Termix offre un accès terminal SSH, des capacités de tunneling SSH, la gestion de fichiers à distance, et de nombreux autres outils. Termix est l'alternative parfaite, gratuite et auto-hébergée à Termius, disponible sur toutes les plateformes. +Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jamais gratuite et auto-hébergée. Elle fournit une solution multiplateforme pour gérer vos serveurs et votre infrastructure à travers une interface unique et intuitive. Termix offre un accès terminal SSH, le contrôle de bureau à distance (RDP, VNC, Telnet), des capacités de tunneling SSH, la gestion de fichiers SSH à distance et de nombreux autres outils. Termix est l'alternative parfaite, gratuite et auto-hébergée à Termius, disponible sur toutes les plateformes. # Fonctionnalités -- **Accès terminal SSH** - Terminal complet avec support d'écran partagé (jusqu'à 4 panneaux) et un système d'onglets inspiré des navigateurs. Inclut la personnalisation du terminal avec des thèmes courants, des polices et d'autres composants -- **Accès Bureau à Distance** - Support RDP, VNC et Telnet via navigateur avec personnalisation complète et écran partagé -- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH avec reconnexion automatique et surveillance de l'état, avec support des connexions -l ou -r -- **Gestionnaire de fichiers distant** - Gérez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'édition de code, images, audio et vidéo. Téléversez, téléchargez, renommez, supprimez et déplacez des fichiers de manière fluide avec support sudo -- **Gestion Docker** - Démarrez, arrêtez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Contrôlez les conteneurs via le terminal docker exec. Non conçu pour remplacer Portainer ou Dockge, mais plutôt pour gérer simplement vos conteneurs plutôt que de les créer -- **Gestionnaire d'hôtes SSH** - Enregistrez, organisez et gérez vos connexions SSH avec des tags et des dossiers, et sauvegardez facilement les informations de connexion réutilisables tout en automatisant le déploiement des clés SSH -- **Statistiques serveur** - Visualisez l'utilisation du CPU, de la mémoire et du disque ainsi que le réseau, le temps de fonctionnement, les informations système, le pare-feu et le moniteur de ports sur la plupart des serveurs Linux -- **Tableau de bord** - Consultez les informations de vos serveurs en un coup d'œil depuis votre tableau de bord -- **RBAC** - Créez des rôles et partagez des hôtes entre utilisateurs/rôles -- **Authentification des utilisateurs** - Gestion sécurisée des utilisateurs avec contrôles administrateur et support OIDC et 2FA (TOTP). Visualisez les sessions utilisateur actives sur toutes les plateformes et révoquez les permissions. Liez vos comptes OIDC/locaux ensemble -- **Chiffrement de la base de données** - Le backend est stocké sous forme de fichiers de base de données SQLite chiffrés. Consultez la [documentation](https://docs.termix.site/security) pour plus de détails -- **Export/Import de données** - Exportez et importez les hôtes SSH, les identifiants et les données du gestionnaire de fichiers -- **Configuration SSL automatique** - Génération et gestion intégrées de certificats SSL avec redirections HTTPS -- **Interface moderne** - Interface épurée compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez entre un thème sombre ou clair. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran -- **Langues** - Support intégré d'environ 30 langues (géré par [Crowdin](https://docs.termix.site/translations)) -- **Support multiplateforme** - Disponible en tant qu'application web, application de bureau (Windows, Linux et macOS), PWA, et application mobile/tablette dédiée pour iOS et Android -- **Outils SSH** - Créez des extraits de commandes réutilisables exécutables en un seul clic. Exécutez une commande simultanément sur plusieurs terminaux ouverts -- **Historique des commandes** - Auto-complétion et consultation des commandes SSH précédemment exécutées -- **Connexion rapide** - Connectez-vous à un serveur sans avoir à sauvegarder les données de connexion -- **Palette de commandes** - Appuyez deux fois sur Shift gauche pour accéder rapidement aux connexions SSH avec votre clavier -- **SSH riche en fonctionnalités** - Support des hôtes de rebond, Warpgate, connexions basées sur TOTP, SOCKS5, vérification des clés d'hôte, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), etc. -- **Graphe réseau** - Personnalisez votre tableau de bord pour visualiser votre homelab basé sur vos connexions SSH avec support des statuts -- **Onglets Persistants** - Les sessions SSH et les onglets restent ouverts sur tous les appareils/actualisations si activé dans le profil utilisateur +- **Accès terminal SSH** - Terminal complet avec support d'écran partagé (jusqu'à 4 panneaux) et un système d'onglets inspiré des navigateurs. Inclut la personnalisation du terminal avec des thèmes courants, des polices et d'autres composants. +- **Accès Bureau à Distance** - Support RDP, VNC et Telnet via navigateur avec personnalisation complète et écran partagé. +- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH avec reconnexion automatique et surveillance de l'état, avec support des connexions -l ou -r. +- **Gestionnaire de fichiers distant** - Gérez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'édition de code, images, audio et vidéo. Téléversez, téléchargez, renommez, supprimez et déplacez des fichiers de manière fluide avec support sudo. +- **Gestion Docker** - Démarrez, arrêtez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Contrôlez les conteneurs via le terminal docker exec. Non conçu pour remplacer Portainer ou Dockge, mais plutôt pour gérer simplement vos conteneurs plutôt que de les créer. +- **Gestionnaire d'hôtes SSH** - Enregistrez, organisez et gérez vos connexions SSH avec des tags et des dossiers, et sauvegardez facilement les informations de connexion réutilisables tout en automatisant le déploiement des clés SSH. +- **Statistiques serveur** - Visualisez l'utilisation du CPU, de la mémoire et du disque ainsi que le réseau, le temps de fonctionnement, les informations système, le pare-feu et le moniteur de ports sur la plupart des serveurs Linux. +- **Tableau de bord** - Consultez les informations de vos serveurs en un coup d'œil depuis votre tableau de bord. +- **RBAC** - Créez des rôles et partagez des hôtes entre utilisateurs/rôles. +- **Authentification des utilisateurs** - Gestion sécurisée des utilisateurs avec contrôles administrateur et support OIDC (avec contrôle d'accès) et 2FA (TOTP). Visualisez les sessions utilisateur actives sur toutes les plateformes et révoquez les permissions. Liez vos comptes OIDC/locaux ensemble. +- **Chiffrement de la base de données** - Le backend est stocké sous forme de fichiers de base de données SQLite chiffrés. Consultez la [documentation](https://docs.termix.site/security) pour plus de détails. +- **Export/Import de données** - Exportez et importez les hôtes SSH, les identifiants et les données du gestionnaire de fichiers. +- **Configuration SSL automatique** - Génération et gestion intégrées de certificats SSL avec redirections HTTPS. +- **Interface moderne** - Interface épurée compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez parmi de nombreux thèmes d'interface utilisateur, notamment clair, sombre, Dracula, etc. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran. +- **Langues** - Support intégré d'environ 30 langues (géré par [Crowdin](https://docs.termix.site/translations)). +- **Support multiplateforme** - Disponible en tant qu'application web, application de bureau (Windows, Linux et macOS, peut être exécutée de manière autonome sans backend Termix), PWA, et application mobile/tablette dédiée pour iOS et Android. +- **Outils SSH** - Créez des extraits de commandes réutilisables exécutables en un seul clic. Exécutez une commande simultanément sur plusieurs terminaux ouverts. +- **Historique des commandes** - Auto-complétion et consultation des commandes SSH précédemment exécutées. +- **Connexion rapide** - Connectez-vous à un serveur sans avoir à sauvegarder les données de connexion. +- **Palette de commandes** - Appuyez deux fois sur Shift gauche pour accéder rapidement aux connexions SSH avec votre clavier. +- **SSH riche en fonctionnalités** - Support des hôtes de rebond, Warpgate, connexions basées sur TOTP, SOCKS5, vérification des clés d'hôte, remplissage automatique des mots de passe, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc. +- **Graphe réseau** - Personnalisez votre tableau de bord pour visualiser votre homelab basé sur vos connexions SSH avec support des statuts. +- **Onglets Persistants** - Les sessions SSH et les onglets restent ouverts sur tous les appareils/actualisations si activé dans le profil utilisateur. # Fonctionnalités prévues @@ -102,7 +102,7 @@ Appareils supportés : - Google Play Store - APK -Visitez la [documentation](https://docs.termix.site/install) de Termix pour plus d'informations sur l'installation de Termix sur toutes les plateformes. Sinon, voici un exemple de fichier Docker Compose : +Visitez la [documentation](https://docs.termix.site/install) de Termix pour plus d'informations sur l'installation de Termix sur toutes les plateformes. Sinon, voici un exemple de fichier Docker Compose (vous pouvez omettre guacd et le réseau si vous ne prévoyez pas d'utiliser les fonctionnalités de bureau à distance) : ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

diff --git a/readme/README-HI.md b/readme/README-HI.md index 543a7475..8cdb908e 100644 --- a/readme/README-HI.md +++ b/readme/README-HI.md @@ -44,33 +44,33 @@ Termix Banner

-Termix एक ओपन-सोर्स, हमेशा के लिए मुफ़्त, सेल्फ-होस्टेड ऑल-इन-वन सर्वर प्रबंधन प्लेटफ़ॉर्म है। यह एक एकल, सहज इंटरफ़ेस के माध्यम से आपके सर्वर और बुनियादी ढाँचे के प्रबंधन के लिए एक मल्टी-प्लेटफ़ॉर्म समाधान प्रदान करता है। Termix SSH टर्मिनल एक्सेस, SSH टनलिंग क्षमताएँ, रिमोट फ़ाइल प्रबंधन, और कई अन्य उपकरण प्रदान करता है। Termix सभी प्लेटफ़ॉर्म पर उपलब्ध Termius का सही मुफ़्त और सेल्फ-होस्टेड विकल्प है। +Termix एक ओपन-सोर्स, हमेशा के लिए मुफ़्त, सेल्फ-होस्टेड ऑल-इन-वन सर्वर प्रबंधन प्लेटफ़ॉर्म है। यह एक एकल, सहज इंटरफ़ेस के माध्यम से आपके सर्वर और बुनियादी ढाँचे के प्रबंधन के लिए एक मल्टी-प्लेटफ़ॉर्म समाधान प्रदान करता है। Termix SSH टर्मिनल एक्सेस, रिमोट डेस्कटॉप कंट्रोल (RDP, VNC, Telnet), SSH टनलिंग क्षमताएँ, रिमोट SSH फ़ाइल प्रबंधन, और कई अन्य उपकरण प्रदान करता है। Termix सभी प्लेटफ़ॉर्म पर उपलब्ध Termius का सही मुफ़्त और सेल्फ-होस्टेड विकल्प है। # विशेषताएँ -- **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है -- **रिमोट डेस्कटॉप एक्सेस** - ब्राउज़र पर RDP, VNC और Telnet सपोर्ट, पूर्ण कस्टमाइज़ेशन और स्प्लिट स्क्रीन के साथ -- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ -- **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें -- **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है -- **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें -- **सर्वर आँकड़े** - अधिकांश Linux आधारित सर्वर पर नेटवर्क, अपटाइम, सिस्टम जानकारी, फ़ायरवॉल, पोर्ट मॉनिटर के साथ CPU, मेमोरी और डिस्क उपयोग देखें -- **डैशबोर्ड** - अपने डैशबोर्ड पर एक नज़र में सर्वर की जानकारी देखें -- **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें -- **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें -- **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें -- **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें -- **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन -- **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। डार्क या लाइट मोड UI के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें -- **भाषाएँ** - लगभग 30 भाषाओं का बिल्ट-इन सपोर्ट ([Crowdin](https://docs.termix.site/translations) द्वारा प्रबंधित) -- **प्लेटफ़ॉर्म सपोर्ट** - वेब ऐप, डेस्कटॉप एप्लिकेशन (Windows, Linux, और macOS), PWA, और iOS और Android के लिए समर्पित मोबाइल/टैबलेट ऐप के रूप में उपलब्ध -- **SSH टूल्स** - एक क्लिक से निष्पादित होने वाले पुन: उपयोग योग्य कमांड स्निपेट बनाएँ। एक साथ कई खुले टर्मिनलों में एक कमांड चलाएँ -- **कमांड इतिहास** - पहले चलाए गए SSH कमांड का ऑटो-कम्प्लीट और दृश्य -- **क्विक कनेक्ट** - कनेक्शन डेटा सहेजे बिना सर्वर से कनेक्ट करें -- **कमांड पैलेट** - अपने कीबोर्ड से SSH कनेक्शन तक त्वरित पहुँच के लिए बाएँ Shift को दो बार टैप करें -- **SSH सुविधाओं से भरपूर** - जम्प होस्ट, Warpgate, TOTP आधारित कनेक्शन, SOCKS5, होस्ट की वेरिफ़िकेशन, पासवर्ड ऑटोफ़िल, [OPKSSH](https://github.com/openpubkey/opkssh) आदि का सपोर्ट -- **नेटवर्क ग्राफ़** - स्थिति सपोर्ट के साथ अपने SSH कनेक्शन के आधार पर अपने होमलैब को विज़ुअलाइज़ करने के लिए अपना डैशबोर्ड कस्टमाइज़ करें -- **परसिस्टेंट टैब** - उपयोगकर्ता प्रोफ़ाइल में सक्षम होने पर SSH सेशन और टैब डिवाइस/रीफ्रेश के पार खुले रहते हैं +- **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है। +- **रिमोट डेस्कटॉप एक्सेस** - ब्राउज़र पर RDP, VNC और Telnet सपोर्ट, पूर्ण कस्टमाइज़ेशन और स्प्लिट स्क्रीन के साथ। +- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ। +- **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें। +- **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है। +- **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें। +- **सर्वर आँकड़े** - अधिकांश Linux आधारित सर्वर पर नेटवर्क, अपटाइम, सिस्टम जानकारी, फ़ायरवॉल, पोर्ट मॉनिटर के साथ CPU, मेमोरी और डिस्क उपयोग देखें। +- **डैशबोर्ड** - अपने डैशबोर्ड पर एक नज़र में सर्वर की जानकारी देखें। +- **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें। +- **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC (एक्सेस कंट्रोल के साथ) और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें। +- **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें। +- **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें। +- **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन। +- **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। लाइट, डार्क, ड्रैकुला आदि सहित कई अलग-अलग UI थीम के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें। +- **भाषाएँ** - लगभग 30 भाषाओं का बिल्ट-इन सपोर्ट ([Crowdin](https://docs.termix.site/translations) द्वारा प्रबंधित)। +- **प्लेटफ़ॉर्म सपोर्ट** - वेब ऐप, डेस्कटॉप एप्लिकेशन (Windows, Linux, और macOS, Termix बैकएंड के बिना स्टैंडअलोन चलाया जा सकता है), PWA, और iOS और Android के लिए समर्पित मोबाइल/टैबलेट ऐप के रूप में उपलब्ध। +- **SSH टूल्स** - एक क्लिक से निष्पादित होने वाले पुन: उपयोग योग्य कमांड स्निपेट बनाएँ। एक साथ कई खुले टर्मिनलों में एक कमांड चलाएँ। +- **कमांड इतिहास** - पहले चलाए गए SSH कमांड का ऑटो-कम्प्लीट और दृश्य। +- **क्विक कनेक्ट** - कनेक्शन डेटा सहेजे बिना सर्वर से कनेक्ट करें। +- **कमांड पैलेट** - अपने कीबोर्ड से SSH कनेक्शन तक त्वरित पहुँच के लिए बाएँ Shift को दो बार टैप करें। +- **SSH सुविधाओं से भरपूर** - जम्प होस्ट, Warpgate, TOTP आधारित कनेक्शन, SOCKS5, होस्ट की वेरिफ़िकेशन, पासवर्ड ऑटोफ़िल, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, पोर्ट नॉकिंग आदि का सपोर्ट। +- **नेटवर्क ग्राफ़** - स्थिति सपोर्ट के साथ अपने SSH कनेक्शन के आधार पर अपने होमलैब को विज़ुअलाइज़ करने के लिए अपना डैशबोर्ड कस्टमाइज़ करें। +- **परसिस्टेंट टैब** - उपयोगकर्ता प्रोफ़ाइल में सक्षम होने पर SSH सेशन और टैब डिवाइस/रीफ्रेश के पार खुले रहते हैं। # नियोजित विशेषताएँ @@ -102,7 +102,7 @@ Termix एक ओपन-सोर्स, हमेशा के लिए मु - Google Play Store - APK -सभी प्लेटफ़ॉर्म पर Termix इंस्टॉल करने के बारे में अधिक जानकारी के लिए Termix [डॉक्स](https://docs.termix.site/install) पर जाएँ। अन्यथा, यहाँ एक नमूना Docker Compose फ़ाइल देखें: +सभी प्लेटफ़ॉर्म पर Termix इंस्टॉल करने के बारे में अधिक जानकारी के लिए Termix [डॉक्स](https://docs.termix.site/install) पर जाएँ। अन्यथा, यहाँ एक नमूना Docker Compose फ़ाइल देखें (यदि आप रिमोट डेस्कटॉप सुविधाओं का उपयोग करने की योजना नहीं बना रहे हैं तो आप guacd और नेटवर्क को हटा सकते हैं): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

diff --git a/readme/README-IT.md b/readme/README-IT.md index 2b6e2d97..d87e1cac 100644 --- a/readme/README-IT.md +++ b/readme/README-IT.md @@ -44,33 +44,33 @@ Se lo desideri, puoi supportare il progetto qui!\ Termix Banner

-Termix è una piattaforma di gestione server tutto-in-uno, open-source, per sempre gratuita e self-hosted. Fornisce una soluzione multipiattaforma per gestire i tuoi server e la tua infrastruttura attraverso un'unica interfaccia intuitiva. Termix offre accesso al terminale SSH, funzionalità di tunneling SSH, gestione remota dei file e molti altri strumenti. Termix è la perfetta alternativa gratuita e self-hosted a Termius, disponibile per tutte le piattaforme. +Termix è una piattaforma di gestione server tutto-in-uno, open-source, per sempre gratuita e self-hosted. Fornisce una soluzione multipiattaforma per gestire i tuoi server e la tua infrastruttura attraverso un'unica interfaccia intuitiva. Termix offre accesso al terminale SSH, controllo remoto del desktop (RDP, VNC, Telnet), funzionalità di tunneling SSH, gestione remota dei file SSH e molti altri strumenti. Termix è la perfetta alternativa gratuita e self-hosted a Termius, disponibile per tutte le piattaforme. # Funzionalità -- **Accesso Terminale SSH** - Terminale completo con supporto schermo diviso (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni -- **Accesso Desktop Remoto** - Supporto RDP, VNC e Telnet tramite browser con personalizzazione completa e schermo diviso -- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH con riconnessione automatica e monitoraggio dello stato, con supporto per connessioni -l o -r +- **Accesso Terminale SSH** - Terminale completo con supporto schermo diviso (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni. +- **Accesso Desktop Remoto** - Supporto RDP, VNC e Telnet tramite browser con personalizzazione completa e schermo diviso. +- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH con riconnessione automatica e monitoraggio dello stato, con supporto per connessioni -l o -r. - **Gestore File Remoto** - Gestisci i file direttamente sui server remoti con supporto per la visualizzazione e la modifica di codice, immagini, audio e video. Carica, scarica, rinomina, elimina e sposta file senza problemi con supporto sudo. - **Gestione Docker** - Avvia, ferma, metti in pausa, rimuovi container. Visualizza le statistiche dei container. Controlla i container tramite terminale docker exec. Non è stato creato per sostituire Portainer o Dockge, ma piuttosto per gestire semplicemente i tuoi container rispetto alla loro creazione. -- **Gestore Host SSH** - Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle, salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH -- **Statistiche Server** - Visualizza l'utilizzo di CPU, memoria e disco insieme a rete, uptime, informazioni di sistema, firewall, monitoraggio porte sulla maggior parte dei server basati su Linux -- **Dashboard** - Visualizza le informazioni del server a colpo d'occhio sulla tua dashboard -- **RBAC** - Crea ruoli e condividi host tra utenti/ruoli -- **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC e 2FA (TOTP). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro. +- **Gestore Host SSH** - Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle, salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH. +- **Statistiche Server** - Visualizza l'utilizzo di CPU, memoria e disco insieme a rete, uptime, informazioni di sistema, firewall, monitoraggio porte sulla maggior parte dei server basati su Linux. +- **Dashboard** - Visualizza le informazioni del server a colpo d'occhio sulla tua dashboard. +- **RBAC** - Crea ruoli e condividi host tra utenti/ruoli. +- **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC (con controllo degli accessi) e 2FA (TOTP). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro. - **Crittografia Database** - Il backend è archiviato come file di database SQLite crittografati. Consulta la [documentazione](https://docs.termix.site/security) per maggiori informazioni. -- **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file -- **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS -- **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra modalità scura o chiara. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero. -- **Lingue** - Supporto integrato per ~30 lingue (gestito da [Crowdin](https://docs.termix.site/translations)) -- **Supporto Piattaforme** - Disponibile come app web, applicazione desktop (Windows, Linux e macOS), PWA e app dedicata per mobile/tablet su iOS e Android. +- **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file. +- **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS. +- **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra molti temi UI diversi, inclusi chiaro, scuro, Dracula, ecc. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero. +- **Lingue** - Supporto integrato per ~30 lingue (gestito da [Crowdin](https://docs.termix.site/translations)). +- **Supporto Piattaforme** - Disponibile come app web, applicazione desktop (Windows, Linux e macOS, può essere eseguito in modo autonomo senza il backend Termix), PWA e app dedicata per mobile/tablet su iOS e Android. - **Strumenti SSH** - Crea snippet di comandi riutilizzabili che si eseguono con un singolo clic. Esegui un comando simultaneamente su più terminali aperti. -- **Cronologia Comandi** - Autocompletamento e visualizzazione dei comandi SSH eseguiti in precedenza -- **Connessione Rapida** - Connettiti a un server senza dover salvare i dati di connessione -- **Palette Comandi** - Premi due volte shift sinistro per accedere rapidamente alle connessioni SSH con la tastiera -- **SSH Ricco di Funzionalità** - Supporta jump host, Warpgate, connessioni basate su TOTP, SOCKS5, verifica chiave host, compilazione automatica password, [OPKSSH](https://github.com/openpubkey/opkssh), ecc. -- **Grafico di Rete** - Personalizza la tua Dashboard per visualizzare il tuo homelab basato sulle connessioni SSH con supporto dello stato -- **Schede Persistenti** - Le sessioni SSH e le schede rimangono aperte tra dispositivi/aggiornamenti se abilitato nel profilo utente +- **Cronologia Comandi** - Autocompletamento e visualizzazione dei comandi SSH eseguiti in precedenza. +- **Connessione Rapida** - Connettiti a un server senza dover salvare i dati di connessione. +- **Palette Comandi** - Premi due volte shift sinistro per accedere rapidamente alle connessioni SSH con la tastiera. +- **SSH Ricco di Funzionalità** - Supporta jump host, Warpgate, connessioni basate su TOTP, SOCKS5, verifica chiave host, compilazione automatica password, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, ecc. +- **Grafico di Rete** - Personalizza la tua Dashboard per visualizzare il tuo homelab basato sulle connessioni SSH con supporto dello stato. +- **Schede Persistenti** - Le sessioni SSH e le schede rimangono aperte tra dispositivi/aggiornamenti se abilitato nel profilo utente. # Funzionalità Pianificate @@ -102,7 +102,7 @@ Dispositivi Supportati: - Google Play Store - APK -Visita la [Documentazione](https://docs.termix.site/install) di Termix per maggiori informazioni su come installare Termix su tutte le piattaforme. In alternativa, visualizza un file Docker Compose di esempio qui: +Visita la [Documentazione](https://docs.termix.site/install) di Termix per maggiori informazioni su come installare Termix su tutte le piattaforme. In alternativa, visualizza un file Docker Compose di esempio qui (puoi omettere guacd e la rete se non prevedi di utilizzare le funzioni di desktop remoto): ```yaml services: @@ -151,17 +151,29 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

# Supporto -Se hai bIPAgno di aiuto o vuoi richiedere una funzionalità per Termix, visita la pagina [Segnalazioni](https://github.com/Termix-SSH/Support/issues), accedi e premi `New Issue`. +Se hai bisogno di aiuto o vuoi richiedere una funzionalità per Termix, visita la pagina [Segnalazioni](https://github.com/Termix-SSH/Support/issues), accedi e premi `New Issue`. Per favore, sii il più dettagliato possibile nella tua segnalazione, preferibilmente scritta in inglese. Puoi anche unirti al server [Discord](https://discord.gg/jVQGdvHDrf) e visitare il canale di supporto, tuttavia i tempi di risposta potrebbero essere più lunghi. # Screenshot diff --git a/readme/README-JA.md b/readme/README-JA.md index d0c96140..a9d8810e 100644 --- a/readme/README-JA.md +++ b/readme/README-JA.md @@ -44,33 +44,33 @@ Termix Banner

-Termixは、オープンソースで永久無料のセルフホスト型オールインワンサーバー管理プラットフォームです。単一の直感的なインターフェースを通じて、サーバーとインフラストラクチャを管理するマルチプラットフォームソリューションを提供します。Termixは、SSHターミナルアクセス、SSHトンネリング機能、リモートファイル管理、その他多くのツールを提供します。Termixは、すべてのプラットフォームで利用可能なTermiusの完全無料でセルフホスト可能な代替ソリューションです。 +Termixは、オープンソースで永久無料のセルフホスト型オールインワンサーバー管理プラットフォームです。単一の直感的なインターフェースを通じて、サーバーとインフラストラクチャを管理するマルチプラットフォームソリューションを提供します。Termixは、SSHターミナルアクセス、リモートデスクトップ制御(RDP、VNC、Telnet)、SSHトンネリング機能、リモートSSHファイル管理、およびその他多くのツールを提供します。Termixは、すべてのプラットフォームで利用可能なTermiusの完全無料でセルフホスト可能な代替ソリューションです。 # 機能 -- **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応 -- **リモートデスクトップアクセス** - ブラウザ上でRDP、VNC、Telnetをサポート、完全なカスタマイズと分割画面に対応 -- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応 -- **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行 -- **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易管理を目的としています -- **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化 -- **サーバー統計** - ほとんどのLinuxベースのサーバーで、CPU、メモリ、ディスク使用量、ネットワーク、アップタイム、システム情報、ファイアウォール、ポートモニターを表示 -- **ダッシュボード** - ダッシュボードでサーバー情報を一目で確認 -- **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有 -- **ユーザー認証** - 管理者コントロールとOIDCおよび2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携 -- **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください -- **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポート -- **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理 -- **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ダーク/ライトモードの切り替え対応。URLルートで任意の接続をフルスクリーンで開くことが可能 -- **多言語対応** - 約30言語の組み込みサポート([Crowdin](https://docs.termix.site/translations)で管理) -- **プラットフォーム対応** - Webアプリ、デスクトップアプリケーション(Windows、Linux、macOS)、PWA、iOS・Android専用モバイル/タブレットアプリとして利用可能 -- **SSHツール** - ワンクリックで実行できる再利用可能なコマンドスニペットの作成。複数の開いているターミナルに対して同時にコマンドを実行 -- **コマンド履歴** - 過去に実行したSSHコマンドの自動補完と表示 -- **クイック接続** - 接続データを保存せずにサーバーに接続 -- **コマンドパレット** - 左Shiftキーを2回押すことで、キーボードからSSH接続に素早くアクセス -- **SSH機能充実** - ジャンプホスト、Warpgate、TOTPベースの接続、SOCKS5、ホストキー検証、パスワード自動入力、[OPKSSH](https://github.com/openpubkey/opkssh)などに対応 -- **ネットワークグラフ** - ダッシュボードをカスタマイズして、SSH接続に基づくホームラボのネットワークをステータス表示付きで可視化 -- **永続タブ** - ユーザープロフィールで有効にすると、SSHセッションとタブがデバイス/更新をまたいで開いたまま保持されます +- **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応しています。 +- **リモートデスクトップアクセス** - ブラウザ上でRDP、VNC、Telnetをサポート、完全なカスタマイズと分割画面に対応しています。 +- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応しています。 +- **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理できます。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行できます。 +- **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易的な管理を目的としています。 +- **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化できます。 +- **サーバー統計** - ほとんどのLinuxベースのサーバーで、CPU、メモリ、ディスク使用量、ネットワーク、アップタイム、システム情報、ファイアウォール、ポートモニターを表示できます。 +- **ダッシュボード** - ダッシュボードでサーバー情報を一目で確認できます。 +- **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有できます。 +- **ユーザー認証** - 管理者コントロールとOIDC(アクセス制御付き)および2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携が可能です。 +- **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存されます。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください。 +- **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポートが可能です。 +- **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理が可能です。 +- **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ライト、ダーク、Draculaなど、多くの異なるUIテーマから選択可能。URLルートで任意の接続をフルスクリーンで開くことができます。 +- **多言語対応** - 約30言語の組み込みサポート([Crowdin](https://docs.termix.site/translations)で管理されています)。 +- **プラットフォーム対応** - Webアプリ、デスクトップアプリケーション(Windows、Linux、macOS。Termixバックエンドなしでスタンドアロン動作可能)、PWA、iOS・Android専用モバイル/タブレットアプリとして利用可能です。 +- **SSHツール** - ワンクリックで実行できる再利用可能なコマンドスニペットの作成。複数の開いているターミナルに対して同時にコマンドを実行できます。 +- **コマンド履歴** - 過去に実行したSSHコマンドの自動補完と表示が可能です。 +- **クイック接続** - 接続データを保存せずにサーバーに接続できます。 +- **コマンドパレット** - 左Shiftキーを2回押すことで、キーボードからSSH接続に素早くアクセスできます。 +- **SSH機能充実** - ジャンプホスト、Warpgate、TOTPベースの接続、SOCKS5、ホストキー検証、パスワード自動入力、[OPKSSH](https://github.com/openpubkey/opkssh)、tmux、ポート敲き(port knocking)などに対応しています。 +- **ネットワークグラフ** - ダッシュボードをカスタマイズして、SSH接続に基づくホームラボのネットワークをステータス表示付きで可視化できます。 +- **永続タブ** - ユーザープロフィールで有効にすると、SSHセッションとタブがデバイス/更新をまたいで開いたまま保持されます。 # 予定されている機能 @@ -102,7 +102,7 @@ Termixは、オープンソースで永久無料のセルフホスト型オー - Google Play Store - APK -すべてのプラットフォームへのTermixのインストール方法については、Termixの[ドキュメント](https://docs.termix.site/install)をご覧ください。以下はDocker Composeファイルのサンプルです: +すべてのプラットフォームへのTermixのインストール方法については、Termixの[ドキュメント](https://docs.termix.site/install)をご覧ください。または、以下のサンプルDocker Composeファイルをご覧ください(リモートデスクトップ機能を使用する予定がない場合は、guacdとネットワークの設定を省略できます): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

@@ -197,7 +209,7 @@ Termixに関するヘルプや機能リクエストが必要な場合は、[Issu Termix Demo 12

-一部の動画や画像は古い場合や、機能を完全に紹介していない場合があります。 +動画や画像の一部は最新ではない場合や、機能を完全に紹介できていない場合があります。 # ライセンス diff --git a/readme/README-KO.md b/readme/README-KO.md index 7deeb859..5b6a9975 100644 --- a/readme/README-KO.md +++ b/readme/README-KO.md @@ -44,33 +44,33 @@ Termix Banner

-Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 관리 플랫폼입니다. 단일 직관적인 인터페이스를 통해 서버와 인프라를 관리할 수 있는 멀티 플랫폼 솔루션을 제공합니다. Termix는 SSH 터미널 접속, SSH 터널링 기능, 원격 파일 관리 및 기타 다양한 도구를 제공합니다. Termix는 모든 플랫폼에서 사용 가능한 Termius의 완벽한 무료 셀프 호스팅 대안입니다. +Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 관리 플랫폼입니다. 단일 직관적인 인터페이스를 통해 서버와 인프라를 관리할 수 있는 멀티 플랫폼 솔루션을 제공합니다. Termix는 SSH 터미널 접속, 원격 데스크톱 제어(RDP, VNC, Telnet), SSH 터널링 기능, 원격 SSH 파일 관리 및 기타 다양한 도구를 제공합니다. Termix는 모든 플랫폼에서 사용 가능한 Termius의 완벽한 무료 셀프 호스팅 대안입니다. # 기능 -- **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원 -- **원격 데스크톱 접속** - 완전한 사용자 정의와 분할 화면을 지원하는 브라우저 기반 RDP, VNC, Telnet 지원 -- **SSH 터널 관리** - 자동 재연결 및 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원 -- **원격 파일 관리자** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행 -- **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다 -- **SSH 호스트 관리자** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화 -- **서버 통계** - 대부분의 Linux 기반 서버에서 CPU, 메모리, 디스크 사용량과 함께 네트워크, 업타임, 시스템 정보, 방화벽, 포트 모니터를 표시 -- **대시보드** - 대시보드에서 서버 정보를 한눈에 확인 -- **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유 -- **사용자 인증** - 관리자 제어와 OIDC 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동 -- **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요 -- **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기 -- **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리 -- **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 다크 또는 라이트 모드 기반 UI 선택. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능 -- **다국어 지원** - 약 30개 언어 내장 지원([Crowdin](https://docs.termix.site/translations)으로 관리) -- **플랫폼 지원** - 웹 앱, 데스크톱 애플리케이션(Windows, Linux, macOS), PWA, iOS 및 Android 전용 모바일/태블릿 앱으로 제공 -- **SSH 도구** - 한 번의 클릭으로 실행 가능한 재사용 가능 명령어 스니펫 생성. 여러 열린 터미널에서 동시에 하나의 명령어 실행 -- **명령어 기록** - 이전에 실행한 SSH 명령어의 자동 완성 및 조회 -- **빠른 연결** - 연결 데이터를 저장하지 않고 서버에 접속 -- **명령어 팔레트** - 왼쪽 Shift 키를 두 번 눌러 키보드로 SSH 연결에 빠르게 접근 -- **풍부한 SSH 기능** - 점프 호스트, Warpgate, TOTP 기반 연결, SOCKS5, 호스트 키 검증, 비밀번호 자동 입력, [OPKSSH](https://github.com/openpubkey/opkssh) 등 지원 -- **네트워크 그래프** - 대시보드를 사용자 정의하여 SSH 연결 기반의 홈랩 네트워크를 상태 표시와 함께 시각화 -- **지속 탭** - 사용자 프로필에서 활성화된 경우 SSH 세션 및 탭이 기기/새로 고침 간에 열린 상태 유지 +- **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원. +- **원격 데스크톱 접속** - 완전한 사용자 정의와 분할 화면을 지원하는 브라우저 기반 RDP, VNC, Telnet 지원. +- **SSH 터널 관리** - 자동 재연결 및 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원. +- **원격 파일 관리자** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행. +- **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다. +- **SSH 호스트 관리자** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화. +- **서버 통계** - 대부분의 Linux 기반 서버에서 CPU, 메모리, 디스크 사용량과 함께 네트워크, 업타임, 시스템 정보, 방화벽, 포트 모니터를 표시. +- **대시보드** - 대시보드에서 서버 정보를 한눈에 확인. +- **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유. +- **사용자 인증** - 관리자 제어와 OIDC(액세스 제어 포함) 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동. +- **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요. +- **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기. +- **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리. +- **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 라이트, 다크, 드라큘라 등 다양한 UI 테마 선택 가능. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능. +- **다국어 지원** - 약 30개 언어 내장 지원([Crowdin](https://docs.termix.site/translations)으로 관리). +- **플랫폼 지원** - 웹 앱, 데스크톱 애플리케이션(Windows, Linux, macOS, Termix 백엔드 없이 독립 실행 가능), PWA, iOS 및 Android 전용 모바일/태블릿 앱으로 제공. +- **SSH 도구** - 한 번의 클릭으로 실행 가능한 재사용 가능 명령어 스니펫 생성. 여러 열린 터미널에서 동시에 하나의 명령어 실행. +- **명령어 기록** - 이전에 실행한 SSH 명령어의 자동 완성 및 조회. +- **빠른 연결** - 연결 데이터를 저장하지 않고 서버에 접속. +- **명령어 팔레트** - 왼쪽 Shift 키를 두 번 눌러 키보드로 SSH 연결에 빠르게 접근. +- **풍부한 SSH 기능** - 점프 호스트, Warpgate, TOTP 기반 연결, SOCKS5, 호스트 키 검증, 비밀번호 자동 입력, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, 포트 노킹 등 지원. +- **네트워크 그래프** - 대시보드를 사용자 정의하여 SSH 연결 기반의 홈랩 네트워크를 상태 표시와 함께 시각화. +- **지속 탭** - 사용자 프로필에서 활성화된 경우 SSH 세션 및 탭이 기기/새로 고침 간에 열린 상태 유지. # 계획된 기능 @@ -102,7 +102,7 @@ Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 - Google Play Store - APK -모든 플랫폼에 Termix를 설치하는 방법에 대한 자세한 내용은 Termix [문서](https://docs.termix.site/install)를 방문하세요. 다음은 Docker Compose 파일 예시입니다: +모든 플랫폼에 Termix를 설치하는 방법에 대한 자세한 내용은 Termix [문서](https://docs.termix.site/install)를 방문하세요. 다음은 Docker Compose 파일 예시입니다(원격 데스크톱 기능을 사용할 계획이 없다면 guacd와 네트워크를 생략할 수 있습니다): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

diff --git a/readme/README-PT.md b/readme/README-PT.md index 40fedffe..2432a13e 100644 --- a/readme/README-PT.md +++ b/readme/README-PT.md @@ -44,11 +44,11 @@ Se desejar, você pode apoiar o projeto aqui!\ Termix Banner

-Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código aberto, sempre gratuita e auto-hospedada. Ela fornece uma solução multiplataforma para gerenciar seus servidores e infraestrutura através de uma interface única e intuitiva. Termix oferece acesso a terminal SSH, capacidades de tunelamento SSH, gerenciamento remoto de arquivos e muitas outras ferramentas. Termix é a alternativa perfeita, gratuita e auto-hospedada ao Termius, disponível para todas as plataformas. +Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código aberto, sempre gratuita e auto-hospedada. Ela fornece uma solução multiplataforma para gerenciar seus servidores e infraestrutura através de uma interface única e intuitiva. Termix oferece acesso a terminal SSH, controle de desktop remoto (RDP, VNC, Telnet), capacidades de tunelamento SSH, gerenciamento remoto de arquivos SSH e muitas outras ferramentas. Termix é a alternativa perfeita, gratuita e auto-hospedada ao Termius, disponível para todas as plataformas. # Funcionalidades -- **Acesso ao Terminal SSH** - Terminal completo com suporte a tela dividida (até 4 painéis) com um sistema de abas similar ao navegador. Inclui suporte para personalização do terminal incluindo temas comuns de terminal, fontes e outros componentes +- **Acesso ao Terminal SSH** - Terminal completo com suporte a tela dividida (até 4 painéis) com um sistema de abas similar ao navegador. Inclui suporte para personalização do terminal incluindo temas comuns de terminal, fontes e outros componentes. - **Acesso à Área de Trabalho Remota** - Suporte a RDP, VNC e Telnet pelo navegador com personalização completa e tela dividida - **Gerenciamento de Túneis SSH** - Crie e gerencie túneis SSH com reconexão automática e monitoramento de saúde, com suporte para conexões -l ou -r - **Gerenciador Remoto de Arquivos** - Gerencie arquivos diretamente em servidores remotos com suporte para visualizar e editar código, imagens, áudio e vídeo. Faça upload, download, renomeie, exclua e mova arquivos facilmente com suporte sudo. @@ -57,18 +57,18 @@ Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código a - **Estatísticas do Servidor** - Visualize o uso de CPU, memória e disco junto com rede, tempo de atividade, informações do sistema, firewall, monitor de portas na maioria dos servidores baseados em Linux - **Dashboard** - Visualize informações do servidor de relance no seu dashboard - **RBAC** - Crie funções e compartilhe hosts entre usuários/funções -- **Autenticação de Usuários** - Gerenciamento seguro de usuários com controles de administrador e suporte para OIDC e 2FA (TOTP). Visualize sessões ativas de usuários em todas as plataformas e revogue permissões. Vincule suas contas OIDC/Locais entre si. +- **Autenticação de Usuários** - Gerenciamento seguro de usuários com controles de administrador e suporte para OIDC (com controle de acesso) e 2FA (TOTP). Visualize sessões ativas de usuários em todas as plataformas e revogue permissões. Vincule suas contas OIDC/Locais entre si. - **Criptografia de Banco de Dados** - Backend armazenado como arquivos de banco de dados SQLite criptografados. Consulte a [documentação](https://docs.termix.site/security) para mais informações. - **Exportação/Importação de Dados** - Exporte e importe hosts SSH, credenciais e dados do gerenciador de arquivos - **Configuração Automática de SSL** - Geração e gerenciamento integrado de certificados SSL com redirecionamentos HTTPS -- **Interface Moderna** - Interface limpa compatível com desktop/mobile construída com React, Tailwind CSS e Shadcn. Escolha entre modo escuro ou claro. Use rotas de URL para abrir qualquer conexão em tela cheia. +- **Interface Moderna** - Interface limpa compatível com desktop/mobile construída com React, Tailwind CSS e Shadcn. Escolha entre muitos temas de interface diferentes, incluindo claro, escuro, Drácula, etc. Use rotas de URL para abrir qualquer conexão em tela cheia. - **Idiomas** - Suporte integrado para ~30 idiomas (gerenciado pelo [Crowdin](https://docs.termix.site/translations)) -- **Suporte a Plataformas** - Disponível como aplicação web, aplicação desktop (Windows, Linux e macOS), PWA e aplicativo dedicado para celular/tablet para iOS e Android. +- **Suporte a Plataformas** - Disponível como aplicação web, aplicação desktop (Windows, Linux e macOS, pode ser executado de forma independente sem o backend Termix), PWA e aplicativo dedicado para celular/tablet para iOS e Android. - **Ferramentas SSH** - Crie trechos de comandos reutilizáveis que são executados com um único clique. Execute um comando simultaneamente em múltiplos terminais abertos. - **Histórico de Comandos** - Autocompletar e visualizar comandos SSH executados anteriormente - **Conexão Rápida** - Conecte-se a um servidor sem precisar salvar os dados de conexão - **Paleta de Comandos** - Pressione duas vezes a tecla Shift esquerda para acessar rapidamente as conexões SSH com seu teclado -- **SSH Rico em Funcionalidades** - Suporta jump hosts, Warpgate, conexões baseadas em TOTP, SOCKS5, verificação de chave do host, preenchimento automático de senhas, [OPKSSH](https://github.com/openpubkey/opkssh), etc. +- **SSH Rico em Funcionalidades** - Suporta jump hosts, Warpgate, conexões baseadas em TOTP, SOCKS5, verificação de chave do host, preenchimento automático de senhas, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, etc. - **Gráfico de Rede** - Personalize seu Dashboard para visualizar seu homelab baseado nas suas conexões SSH com suporte de status - **Abas Persistentes** - Sessões SSH e abas permanecem abertas entre dispositivos/atualizações se habilitado no perfil do usuário @@ -102,7 +102,7 @@ Dispositivos suportados: - Google Play Store - APK -Visite a [documentação](https://docs.termix.site/install) do Termix para mais informações sobre como instalar o Termix em todas as plataformas. Caso contrário, veja um arquivo Docker Compose de exemplo aqui: +Visite a [documentação](https://docs.termix.site/install) do Termix para mais informações sobre como instalar o Termix em todas as plataformas. Caso contrário, veja um arquivo Docker Compose de exemplo aqui (você pode omitir o guacd e a rede se não planeja usar recursos de área de trabalho remota): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

@@ -169,33 +181,33 @@ Por favor, seja o mais detalhado possível no seu relato, preferencialmente escr [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 - Termix Demo 2 + Termix Demo 1 + Termix Demo 2

- Termix Demo 3 - Termix Demo 4 + Termix Demo 3 + Termix Demo 4

- Termix Demo 5 - Termix Demo 6 + Termix Demo 5 + Termix Demo 6

- Termix Demo 7 - Termix Demo 8 + Termix Demo 7 + Termix Demo 8

- Termix Demo 9 - Termix Demo 10 + Termix Demo 9 + Termix Demo 10

- Termix Demo 11 - Termix Demo 12 + Termix Demo 11 + Termix Demo 12

Alguns vídeos e imagens podem estar desatualizados ou podem não mostrar perfeitamente as funcionalidades. diff --git a/readme/README-RU.md b/readme/README-RU.md index 23b2a490..78b43e5a 100644 --- a/readme/README-RU.md +++ b/readme/README-RU.md @@ -44,31 +44,31 @@ Termix Banner

-Termix — это платформа для управления серверами с открытым исходным кодом, навсегда бесплатная и размещаемая на собственном сервере. Она предоставляет мультиплатформенное решение для управления вашими серверами и инфраструктурой через единый интуитивно понятный интерфейс. Termix предлагает доступ к SSH-терминалу, возможности SSH-туннелирования, удалённое управление файлами и множество других инструментов. Termix — это идеальная бесплатная альтернатива Termius с возможностью размещения на собственном сервере, доступная для всех платформ. +Termix — это платформа для управления серверами с открытым исходным кодом, навсегда бесплатная и размещаемая на собственном сервере. Она предоставляет мультиплатформенное решение для управления вашими серверами и инфраструктурой через единый интуитивно понятный интерфейс. Termix предлагает доступ к SSH-терминалу, управление удаленным рабочим столом (RDP, VNC, Telnet), возможности SSH-туннелирования, удаленное управление файлами SSH и множество других инструментов. Termix — это идеальная бесплатная альтернатива Termius с возможностью размещения на собственном сервере, доступная для всех платформ. # Возможности -- **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты +- **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты. - **Доступ к удалённому рабочему столу** — Поддержка RDP, VNC и Telnet через браузер с полной настройкой и разделением экрана - **Управление SSH-туннелями** — Создание и управление SSH-туннелями с автоматическим переподключением и мониторингом состояния, с поддержкой соединений -l и -r -- **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo -- **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием +- **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo. +- **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием. - **Менеджер SSH-хостов** — Сохранение, организация и управление SSH-подключениями с помощью тегов и папок, с возможностью сохранения данных для повторного входа и автоматизации развёртывания SSH-ключей - **Статистика сервера** — Просмотр использования CPU, памяти и диска, а также сети, времени работы, информации о системе, файрвола и монитора портов на большинстве серверов на базе Linux - **Панель управления** — Просмотр информации о сервере на панели управления одним взглядом - **RBAC** — Создание ролей и предоставление общего доступа к хостам для пользователей/ролей -- **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов +- **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC (с контролем доступа) и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов. - **Шифрование базы данных** — Бэкенд хранится в виде зашифрованных файлов базы данных SQLite. Подробнее в [документации](https://docs.termix.site/security) - **Экспорт/импорт данных** — Экспорт и импорт SSH-хостов, учётных данных и данных файлового менеджера - **Автоматическая настройка SSL** — Встроенная генерация и управление SSL-сертификатами с перенаправлением на HTTPS -- **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между тёмной и светлой темой. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме +- **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между множеством различных тем интерфейса, включая светлую, тёмную, Dracula и т. д. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме. - **Языки** — Встроенная поддержка ~30 языков (управляется через [Crowdin](https://docs.termix.site/translations)) -- **Поддержка платформ** — Доступен как веб-приложение, настольное приложение (Windows, Linux и macOS), PWA и специализированное мобильное/планшетное приложение для iOS и Android -- **Инструменты SSH** — Создание переиспользуемых фрагментов команд, выполняемых одним нажатием. Запуск одной команды одновременно в нескольких открытых терминалах +- **Поддержка платформ** — Доступен как веб-приложение, настольное приложение (Windows, Linux и macOS, может работать автономно без бэкенда Termix), PWA и специализированное мобильное/планшетное приложение для iOS и Android. +- **Инструменты SSH** — Создание переиспользуемых фрагментов команд, выполняемых одним нажатием. Запуск одной команды одновременно в нескольких открытых терминалах. - **История команд** — Автодополнение и просмотр ранее выполненных SSH-команд - **Быстрое подключение** — Подключение к серверу без необходимости сохранения данных подключения - **Командная палитра** — Двойное нажатие левого Shift для быстрого доступа к SSH-подключениям с клавиатуры -- **Богатый функционал SSH** — Поддержка jump-хостов, Warpgate, подключений на основе TOTP, SOCKS5, верификации ключей хоста, автозаполнения паролей, [OPKSSH](https://github.com/openpubkey/opkssh) и др. +- **Богатый функционал SSH** — Поддержка jump-хостов, Warpgate, подключений на основе TOTP, SOCKS5, верификации ключей хоста, автозаполнения паролей, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking и др. - **Сетевой граф** — Настройте панель управления для визуализации вашей домашней лаборатории на основе SSH-подключений с поддержкой статусов - **Постоянные вкладки** — SSH-сессии и вкладки остаются открытыми на всех устройствах/при обновлении страницы, если включено в профиле пользователя @@ -102,7 +102,7 @@ Termix — это платформа для управления сервера - Google Play Store - APK -Посетите [документацию](https://docs.termix.site/install) Termix для получения дополнительной информации об установке Termix на всех платформах. Также вы можете ознакомиться с примером файла Docker Compose здесь: +Посетите [документацию](https://docs.termix.site/install) Termix для получения дополнительной информации об установке Termix на всех платформах. Также вы можете ознакомиться с примером файла Docker Compose здесь (вы можете опустить guacd и сеть, если не планируете использовать функции удаленного рабочего стола): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

@@ -169,33 +181,33 @@ networks: [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 - Termix Demo 2 + Termix Demo 1 + Termix Demo 2

- Termix Demo 3 - Termix Demo 4 + Termix Demo 3 + Termix Demo 4

- Termix Demo 5 - Termix Demo 6 + Termix Demo 5 + Termix Demo 6

- Termix Demo 7 - Termix Demo 8 + Termix Demo 7 + Termix Demo 8

- Termix Demo 9 - Termix Demo 10 + Termix Demo 9 + Termix Demo 10

- Termix Demo 11 - Termix Demo 12 + Termix Demo 11 + Termix Demo 12

Некоторые видео и изображения могут быть устаревшими или не полностью отражать функциональность. diff --git a/readme/README-TR.md b/readme/README-TR.md index 855058c4..206a841a 100644 --- a/readme/README-TR.md +++ b/readme/README-TR.md @@ -44,11 +44,11 @@ Projeyi desteklemek isterseniz, buradan destek olabilirsiniz!\ Termix Banner

-Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındırabileceğiniz hepsi bir arada sunucu yönetim platformudur. Sunucularınızı ve altyapınızı tek bir sezgisel arayüz üzerinden yönetmek için çok platformlu bir çözüm sunar. Termix, SSH terminal erişimi, SSH tünelleme yetenekleri, uzak dosya yönetimi ve daha birçok araç sağlar. Termix, tüm platformlarda kullanılabilen Termius'un mükemmel ücretsiz ve kendi barındırmalı alternatifidir. +Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındırabileceğiniz hepsi bir arada sunucu yönetim platformudur. Sunucularınızı ve altyapınızı tek bir sezgisel arayüz üzerinden yönetmek için çok platformlu bir çözüm sunar. Termix, SSH terminal erişimi, uzak masaüstü kontrolü (RDP, VNC, Telnet), SSH tünelleme yetenekleri, uzak SSH dosya yönetimi ve daha birçok araç sağlar. Termix, tüm platformlarda kullanılabilen Termius'un mükemmel ücretsiz ve kendi barındırmalı alternatifidir. # Özellikler -- **SSH Terminal Erişimi** - Tarayıcı benzeri sekme sistemiyle bölünmüş ekran desteğine sahip (4 panele kadar) tam özellikli terminal. Yaygın terminal temaları, yazı tipleri ve diğer bileşenler dahil olmak üzere terminal özelleştirme desteği içerir +- **SSH Terminal Erişimi** - Tarayıcı benzeri sekme sistemiyle bölünmüş ekran desteğine sahip (4 panele kadar) tam özellikli terminal. Yaygın terminal temaları, yazı tipleri ve diğer bileşenler dahil olmak üzere terminal özelleştirme desteği içerir. - **Uzak Masaüstü Erişimi** - Tam özelleştirme ve bölünmüş ekran ile tarayıcı üzerinden RDP, VNC ve Telnet desteği - **SSH Tünel Yönetimi** - Otomatik yeniden bağlanma ve sağlık izleme ile SSH tünelleri oluşturun ve yönetin, -l veya -r bağlantıları desteğiyle - **Uzak Dosya Yöneticisi** - Uzak sunuculardaki dosyaları doğrudan yönetin; kod, görüntü, ses ve video görüntüleme ve düzenleme desteğiyle. Sudo desteğiyle dosyaları sorunsuzca yükleyin, indirin, yeniden adlandırın, silin ve taşıyın. @@ -57,18 +57,18 @@ Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındıra - **Sunucu İstatistikleri** - Çoğu Linux tabanlı sunucularda CPU, bellek ve disk kullanımını ağ, çalışma süresi, sistem bilgisi, güvenlik duvarı, port izleme ile birlikte görüntüleyin - **Kontrol Paneli** - Kontrol panelinizde sunucu bilgilerini bir bakışta görüntüleyin - **RBAC** - Roller oluşturun ve ana bilgisayarları kullanıcılar/roller arasında paylaşın -- **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC ve 2FA (TOTP) desteğiyle güvenli kullanıcı yönetimi. Tüm platformlardaki aktif kullanıcı oturumlarını görüntüleyin ve izinleri iptal edin. OIDC/Yerel hesaplarınızı birbirine bağlayın. +- **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC (erişim kontrollü) ve 2FA (TOTP) desteğiyle güvenli kullanıcı yönetimi. Tüm platformlardaki aktif kullanıcı oturumlarını görüntüleyin ve izinleri iptal edin. OIDC/Yerel hesaplarınızı birbirine bağlayın. - **Veritabanı Şifreleme** - Arka uç, şifrelenmiş SQLite veritabanı dosyaları olarak depolanır. Daha fazla bilgi için [belgelere](https://docs.termix.site/security) bakın. - **Veri Dışa/İçe Aktarma** - SSH ana bilgisayarlarını, kimlik bilgilerini ve dosya yöneticisi verilerini dışa ve içe aktarın - **Otomatik SSL Kurulumu** - HTTPS yönlendirmeleriyle yerleşik SSL sertifika oluşturma ve yönetimi -- **Modern Arayüz** - React, Tailwind CSS ve Shadcn ile oluşturulmuş temiz masaüstü/mobil uyumlu arayüz. Karanlık veya açık tema arasında seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın. +- **Modern Arayüz** - React, Tailwind CSS ve Shadcn ile oluşturulmuş temiz masaüstü/mobil uyumlu arayüz. Işık, karanlık, Dracula vb. dahil olmak üzere birçok farklı UI teması arasından seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın. - **Diller** - ~30 dil için yerleşik destek ([Crowdin](https://docs.termix.site/translations) tarafından yönetilir) -- **Platform Desteği** - Web uygulaması, masaüstü uygulaması (Windows, Linux ve macOS), PWA ve iOS ile Android için özel mobil/tablet uygulaması olarak kullanılabilir. +- **Platform Desteği** - Web uygulaması, masaüstü uygulaması (Windows, Linux ve macOS, Termix arka ucu olmadan tek başına çalıştırılabilir), PWA ve iOS ile Android için özel mobil/tablet uygulaması olarak kullanılabilir. - **SSH Araçları** - Tek tıklamayla çalıştırılan yeniden kullanılabilir komut parçacıkları oluşturun. Birden fazla açık terminalde aynı anda tek bir komut çalıştırın. - **Komut Geçmişi** - Daha önce çalıştırılan SSH komutlarını otomatik tamamlayın ve görüntüleyin - **Hızlı Bağlantı** - Bağlantı verilerini kaydetmeden bir sunucuya bağlanın - **Komut Paleti** - Sol shift tuşuna iki kez basarak SSH bağlantılarına klavyenizle hızlıca erişin -- **SSH Zengin Özellikler** - Atlama ana bilgisayarları, Warpgate, TOTP tabanlı bağlantılar, SOCKS5, ana bilgisayar anahtar doğrulama, otomatik şifre doldurma, [OPKSSH](https://github.com/openpubkey/opkssh) vb. destekler. +- **SSH Zengin Özellikler** - Atlama ana bilgisayarları, Warpgate, TOTP tabanlı bağlantılar, SOCKS5, ana bilgisayar anahtar doğrulama, otomatik şifre doldurma, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking vb. destekler. - **Ağ Grafiği** - Kontrol panelinizi, SSH bağlantılarınıza dayalı olarak ev laboratuvarınızı durum desteğiyle görselleştirmek için özelleştirin - **Kalıcı Sekmeler** - Kullanıcı profilinde etkinleştirilmişse SSH oturumları ve sekmeler cihazlar/yenilemeler arasında açık kalır @@ -102,7 +102,7 @@ Desteklenen Cihazlar: - Google Play Store - APK -Termix'i tüm platformlara nasıl kuracağınız hakkında daha fazla bilgi için Termix [Belgelerine](https://docs.termix.site/install) bakın. Aksi takdirde, örnek bir Docker Compose dosyasını burada görüntüleyin: +Termix'i tüm platformlara nasıl kuracağınız hakkında daha fazla bilgi için Termix [Belgelerine](https://docs.termix.site/install) bakın. Aksi takdirde, örnek bir Docker Compose dosyasını burada görüntüleyin (uzak masaüstü özelliklerini kullanmayı planlamıyorsanız guacd'yi ve ağı çıkarabilirsiniz): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

@@ -169,33 +181,33 @@ Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 - Termix Demo 2 + Termix Demo 1 + Termix Demo 2

- Termix Demo 3 - Termix Demo 4 + Termix Demo 3 + Termix Demo 4

- Termix Demo 5 - Termix Demo 6 + Termix Demo 5 + Termix Demo 6

- Termix Demo 7 - Termix Demo 8 + Termix Demo 7 + Termix Demo 8

- Termix Demo 9 - Termix Demo 10 + Termix Demo 9 + Termix Demo 10

- Termix Demo 11 - Termix Demo 12 + Termix Demo 11 + Termix Demo 12

Bazı videolar ve görseller güncel olmayabilir veya özellikleri tam olarak yansıtmayabilir. diff --git a/readme/README-VI.md b/readme/README-VI.md index 1f084725..bf721d80 100644 --- a/readme/README-VI.md +++ b/readme/README-VI.md @@ -44,11 +44,11 @@ Nếu bạn muốn, bạn có thể hỗ trợ dự án tại đây!\ Termix Banner

-Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồn mở, miễn phí vĩnh viễn, tự lưu trữ. Nó cung cấp giải pháp đa nền tảng để quản lý máy chủ và cơ sở hạ tầng của bạn thông qua một giao diện trực quan duy nhất. Termix cung cấp quyền truy cập terminal SSH, khả năng tạo đường hầm SSH, quản lý tệp từ xa và nhiều công cụ khác. Termix là giải pháp thay thế miễn phí và tự lưu trữ hoàn hảo cho Termius, khả dụng trên tất cả các nền tảng. +Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồn mở, miễn phí vĩnh viễn, tự lưu trữ. Nó cung cấp giải pháp đa nền tảng để quản lý máy chủ và cơ sở hạ tầng của bạn thông qua một giao diện trực quan duy nhất. Termix cung cấp quyền truy cập terminal SSH, điều khiển máy tính từ xa (RDP, VNC, Telnet), khả năng tạo đường hầm SSH, quản lý tệp SSH từ xa và nhiều công cụ khác. Termix là giải pháp thay thế miễn phí và tự lưu trữ hoàn hảo cho Termius, khả dụng trên tất cả các nền tảng. # Tính Năng -- **Truy Cập Terminal SSH** - Terminal đầy đủ tính năng với hỗ trợ chia màn hình (lên đến 4 bảng) với hệ thống tab kiểu trình duyệt. Bao gồm hỗ trợ tùy chỉnh terminal bao gồm các chủ đề terminal phổ biến, phông chữ và các thành phần khác +- **Truy Cập Terminal SSH** - Terminal đầy đủ tính năng với hỗ trợ chia màn hình (lên đến 4 bảng) với hệ thống tab kiểu trình duyệt. Bao gồm hỗ trợ tùy chỉnh terminal bao gồm các chủ đề terminal phổ biến, phông chữ và các thành phần khác. - **Truy Cập Màn Hình Từ Xa** - Hỗ trợ RDP, VNC và Telnet qua trình duyệt với đầy đủ tùy chỉnh và chia màn hình - **Quản Lý Đường Hầm SSH** - Tạo và quản lý đường hầm SSH với tự động kết nối lại và giám sát sức khỏe, hỗ trợ kết nối -l hoặc -r - **Trình Quản Lý Tệp Từ Xa** - Quản lý tệp trực tiếp trên máy chủ từ xa với hỗ trợ xem và chỉnh sửa mã, hình ảnh, âm thanh và video. Tải lên, tải xuống, đổi tên, xóa và di chuyển tệp liền mạch với hỗ trợ sudo. @@ -57,18 +57,18 @@ Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồ - **Thống Kê Máy Chủ** - Xem mức sử dụng CPU, bộ nhớ và ổ đĩa cùng với mạng, thời gian hoạt động, thông tin hệ thống, tường lửa, giám sát cổng trên hầu hết các máy chủ chạy Linux - **Bảng Điều Khiển** - Xem thông tin máy chủ trong nháy mắt trên bảng điều khiển của bạn - **RBAC** - Tạo vai trò và chia sẻ máy chủ giữa người dùng/vai trò -- **Xác Thực Người Dùng** - Quản lý người dùng an toàn với quyền quản trị và hỗ trợ OIDC và 2FA (TOTP). Xem phiên hoạt động của người dùng trên tất cả các nền tảng và thu hồi quyền. Liên kết tài khoản OIDC/Nội bộ của bạn với nhau. +- **Xác Thực Người Dùng** - Quản lý người dùng an toàn với quyền quản trị và hỗ trợ OIDC (có kiểm soát truy cập) và 2FA (TOTP). Xem phiên hoạt động của người dùng trên tất cả các nền tảng và thu hồi quyền. Liên kết tài khoản OIDC/Nội bộ của bạn với nhau. - **Mã Hóa Cơ Sở Dữ Liệu** - Backend được lưu trữ dưới dạng tệp cơ sở dữ liệu SQLite được mã hóa. Xem [tài liệu](https://docs.termix.site/security) để biết thêm. - **Xuất/Nhập Dữ Liệu** - Xuất và nhập máy chủ SSH, thông tin xác thực và dữ liệu trình quản lý tệp - **Thiết Lập SSL Tự Động** - Tạo và quản lý chứng chỉ SSL tích hợp với chuyển hướng HTTPS -- **Giao Diện Hiện Đại** - Giao diện sạch sẽ, thân thiện với máy tính/di động được xây dựng bằng React, Tailwind CSS và Shadcn. Chọn giữa giao diện chế độ tối hoặc sáng. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình. +- **Giao Diện Hiện Đại** - Giao diện sạch sẽ, thân thiện với máy tính/di động được xây dựng bằng React, Tailwind CSS và Shadcn. Chọn giữa nhiều chủ đề UI khác nhau bao gồm sáng, tối, Dracula, v.v. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình. - **Ngôn Ngữ** - Hỗ trợ tích hợp ~30 ngôn ngữ (được quản lý bởi [Crowdin](https://docs.termix.site/translations)) -- **Hỗ Trợ Nền Tảng** - Khả dụng dưới dạng ứng dụng web, ứng dụng máy tính (Windows, Linux và macOS), PWA và ứng dụng di động/máy tính bảng chuyên dụng cho iOS và Android. +- **Hỗ Trợ Nền Tảng** - Khả dụng dưới dạng ứng dụng web, ứng dụng máy tính (Windows, Linux và macOS, có thể chạy độc lập mà không cần backend Termix), PWA và ứng dụng di động/máy tính bảng chuyên dụng cho iOS và Android. - **Công Cụ SSH** - Tạo đoạn lệnh có thể tái sử dụng, thực thi chỉ với một cú nhấp chuột. Chạy một lệnh đồng thời trên nhiều terminal đang mở. - **Lịch Sử Lệnh** - Tự động hoàn thành và xem các lệnh SSH đã chạy trước đó - **Kết Nối Nhanh** - Kết nối đến máy chủ mà không cần lưu dữ liệu kết nối - **Bảng Lệnh** - Nhấn đúp phím shift trái để truy cập nhanh các kết nối SSH bằng bàn phím -- **SSH Giàu Tính Năng** - Hỗ trợ jump host, Warpgate, kết nối dựa trên TOTP, SOCKS5, xác minh khóa máy chủ, tự động điền mật khẩu, [OPKSSH](https://github.com/openpubkey/opkssh), v.v. +- **SSH Giàu Tính Năng** - Hỗ trợ jump host, Warpgate, kết nối dựa trên TOTP, SOCKS5, xác minh khóa máy chủ, tự động điền mật khẩu, [OPKSSH](https://github.com/openpubkey/opkssh), tmux, port knocking, v.v. - **Biểu Đồ Mạng** - Tùy chỉnh Bảng Điều Khiển để trực quan hóa homelab của bạn dựa trên các kết nối SSH với hỗ trợ trạng thái - **Tab Liên Tục** - Các phiên SSH và tab vẫn mở trên các thiết bị/lần làm mới nếu được bật trong hồ sơ người dùng @@ -102,7 +102,7 @@ Thiết Bị Được Hỗ Trợ: - Google Play Store - APK -Truy cập [Tài Liệu](https://docs.termix.site/install) Termix để biết thêm thông tin về cách cài đặt Termix trên tất cả các nền tảng. Ngoài ra, xem tệp Docker Compose mẫu tại đây: +Truy cập [Tài Liệu](https://docs.termix.site/install) Termix để biết thêm thông tin về cách cài đặt Termix trên tất cả các nền tảng. Ngoài ra, xem tệp Docker Compose mẫu tại đây (bạn có thể bỏ qua guacd và mạng nếu không có ý định sử dụng các tính năng điều khiển máy tính từ xa): ```yaml services: @@ -151,11 +151,23 @@ networks:          - Crowdin + Blacksmith          - Crowdin + Cloudflare + +          + + TailScale + +          + + Akamai + +          + + AWS

@@ -169,33 +181,33 @@ Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết b [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 - Termix Demo 2 + Termix Demo 1 + Termix Demo 2

- Termix Demo 3 - Termix Demo 4 + Termix Demo 3 + Termix Demo 4

- Termix Demo 5 - Termix Demo 6 + Termix Demo 5 + Termix Demo 6

- Termix Demo 7 - Termix Demo 8 + Termix Demo 7 + Termix Demo 8

- Termix Demo 9 - Termix Demo 10 + Termix Demo 9 + Termix Demo 10

- Termix Demo 11 - Termix Demo 12 + Termix Demo 11 + Termix Demo 12

Một số video và hình ảnh có thể đã lỗi thời hoặc không thể hiện chính xác hoàn toàn các tính năng. diff --git a/src/backend/dashboard.ts b/src/backend/dashboard.ts index dd6adb91..f8963b7c 100644 --- a/src/backend/dashboard.ts +++ b/src/backend/dashboard.ts @@ -1,6 +1,6 @@ import express from "express"; -import cors from "cors"; import cookieParser from "cookie-parser"; +import { createCorsMiddleware } from "./utils/cors-config.js"; import { getDb, DatabaseSaveTrigger } from "./database/db/index.js"; import { recentActivity, @@ -8,7 +8,7 @@ import { hostAccess, dashboardPreferences, } from "./database/db/schema.js"; -import { eq, and, desc, sql } from "drizzle-orm"; +import { eq, and, desc, sql, inArray } from "drizzle-orm"; import { dashboardLogger } from "./utils/logger.js"; import { SimpleDBOps } from "./utils/simple-db-ops.js"; import { AuthManager } from "./utils/auth-manager.js"; @@ -22,37 +22,7 @@ const serverStartTime = Date.now(); const activityRateLimiter = new Map(); const RATE_LIMIT_MS = 1000; -app.use( - cors({ - origin: (origin, callback) => { - if (!origin) return callback(null, true); - - const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - if (origin.startsWith("https://")) { - return callback(null, true); - } - - if (origin.startsWith("http://")) { - return callback(null, true); - } - - callback(new Error("Not allowed by CORS")); - }, - credentials: true, - methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allowedHeaders: [ - "Content-Type", - "Authorization", - "User-Agent", - "X-Electron-App", - ], - }), -); +app.use(createCorsMiddleware()); app.use(cookieParser()); app.use(express.json({ limit: "1mb" })); app.use((_req, res, next) => { @@ -288,21 +258,48 @@ app.post("/activity/log", async (req, res) => { userId, )) as unknown as { id: number }; - const allActivities = await SimpleDBOps.select( - getDb() - .select() - .from(recentActivity) - .where(eq(recentActivity.userId, userId)) - .orderBy(desc(recentActivity.timestamp)), - "recent_activity", - userId, - ); + // Best-effort trim of old activity entries; failures here should not + // cause the primary /activity/log request to 500. + try { + const allActivities = await SimpleDBOps.select<{ + id: number; + timestamp: string; + }>( + getDb() + .select({ + id: recentActivity.id, + timestamp: recentActivity.timestamp, + }) + .from(recentActivity) + .where(eq(recentActivity.userId, userId)) + .orderBy(desc(recentActivity.timestamp)), + "recent_activity", + userId, + ); - if (allActivities.length > 100) { - const toDelete = allActivities.slice(100); - for (let i = 0; i < toDelete.length; i++) { - await SimpleDBOps.delete(recentActivity, "recent_activity", userId); + if (allActivities.length > 100) { + const idsToDelete = allActivities + .slice(100) + .map((a) => a.id) + .filter((id) => typeof id === "number"); + + if (idsToDelete.length > 0) { + await SimpleDBOps.delete( + recentActivity, + "recent_activity", + and( + eq(recentActivity.userId, userId), + inArray(recentActivity.id, idsToDelete), + ), + ); + } } + } catch (trimErr) { + dashboardLogger.warn("Failed to trim recent_activity (non-fatal)", { + operation: "trim_recent_activity", + userId, + error: trimErr instanceof Error ? trimErr.message : String(trimErr), + }); } res.json({ message: "Activity logged", id: result.id }); diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 2ea0690d..59920818 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -1,4 +1,5 @@ import express from "express"; +import http from "http"; import bodyParser from "body-parser"; import multer from "multer"; import cookieParser from "cookie-parser"; @@ -11,7 +12,7 @@ import terminalRoutes from "./routes/terminal.js"; import guacamoleRoutes from "../guacamole/routes.js"; import networkTopologyRoutes from "./routes/network-topology.js"; import rbacRoutes from "./routes/rbac.js"; -import cors from "cors"; +import { createCorsMiddleware } from "../utils/cors-config.js"; import fetch from "node-fetch"; import fs from "fs"; import path from "path"; @@ -58,39 +59,7 @@ app.set("trust proxy", true); const authManager = AuthManager.getInstance(); const authenticateJWT = authManager.createAuthMiddleware(); const requireAdmin = authManager.createAdminMiddleware(); -app.use( - cors({ - origin: (origin, callback) => { - if (!origin) return callback(null, true); - - const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - if (origin.startsWith("https://")) { - return callback(null, true); - } - - if (origin.startsWith("http://")) { - return callback(null, true); - } - - callback(new Error("Not allowed by CORS")); - }, - credentials: true, - methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allowedHeaders: [ - "Content-Type", - "Authorization", - "User-Agent", - "X-Electron-App", - "Accept", - "Origin", - ], - }), -); +app.use(createCorsMiddleware()); const storage = multer.diskStorage({ destination: (req, file, cb) => { @@ -307,6 +276,10 @@ app.get("/version", authenticateJWT, async (req, res) => { return res.status(404).send("Local Version Not Set"); } + if (req.query.checkRemote === "false") { + return res.json({ localVersion, status: "update_check_disabled" }); + } + try { const cacheKey = "latest_release"; const releaseData = await fetchGitHubAPI( @@ -603,7 +576,6 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => { app.post("/database/export", authenticateJWT, async (req, res) => { try { const userId = (req as AuthenticatedRequest).userId; - const { password } = req.body; const deviceInfo = parseUserAgent(req); const user = await getDb().select().from(users).where(eq(users.id, userId)); @@ -613,30 +585,20 @@ app.post("/database/export", authenticateJWT, async (req, res) => { const isOidcUser = !!user[0].isOidc; - if (!isOidcUser) { - if (!password) { - return res.status(400).json({ - error: "Password required for export", - code: "PASSWORD_REQUIRED", - }); - } - - const unlocked = await authManager.authenticateUser( - userId, - password, - deviceInfo.type, - ); - if (!unlocked) { - return res.status(401).json({ error: "Invalid password" }); - } - } else if (!DataCrypto.getUserDataKey(userId)) { - const oidcUnlocked = await authManager.authenticateOIDCUser( - userId, - deviceInfo.type, - ); - if (!oidcUnlocked) { + if (!DataCrypto.getUserDataKey(userId)) { + if (isOidcUser) { + const oidcUnlocked = await authManager.authenticateOIDCUser( + userId, + deviceInfo.type, + ); + if (!oidcUnlocked) { + return res.status(403).json({ + error: "Failed to unlock user data with SSO credentials", + }); + } + } else { return res.status(403).json({ - error: "Failed to unlock user data with SSO credentials", + error: "User data is locked. Please log in again.", }); } } @@ -651,10 +613,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { throw new Error("User data not unlocked"); } - const tempDir = - process.env.NODE_ENV === "production" - ? path.join(process.env.DATA_DIR || "./db/data", ".temp", "exports") - : path.join(os.tmpdir(), "termix-exports"); + const tempDir = path.join(os.tmpdir(), "termix-exports"); try { if (!fs.existsSync(tempDir)) { @@ -710,6 +669,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { CREATE TABLE ssh_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, + connection_type TEXT NOT NULL DEFAULT 'ssh', name TEXT, ip TEXT NOT NULL, port INTEGER NOT NULL, @@ -742,6 +702,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { show_server_stats_in_sidebar INTEGER NOT NULL DEFAULT 0, default_path TEXT, stats_config TEXT, + docker_config TEXT, terminal_config TEXT, quick_actions TEXT, notes TEXT, @@ -751,6 +712,12 @@ app.post("/database/export", authenticateJWT, async (req, res) => { socks5_username TEXT, socks5_password TEXT, socks5_proxy_chain TEXT, + domain TEXT, + security TEXT, + ignore_cert INTEGER NOT NULL DEFAULT 0, + guacamole_config TEXT, + mac_address TEXT, + port_knock_sequence TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); @@ -763,7 +730,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { folder TEXT, tags TEXT, auth_type TEXT NOT NULL, - username TEXT NOT NULL, + username TEXT, password TEXT, key TEXT, private_key TEXT, @@ -850,8 +817,8 @@ app.post("/database/export", authenticateJWT, async (req, res) => { .from(hosts) .where(eq(hosts.userId, userId)); const insertHost = exportDb.prepare(` - INSERT INTO ssh_data (id, user_id, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO ssh_data (id, user_id, connection_type, name, ip, port, username, folder, tags, pin, auth_type, force_keyboard_interactive, password, key, key_password, key_type, sudo_password, autostart_password, autostart_key, autostart_key_password, credential_id, override_credential_username, enable_terminal, enable_tunnel, tunnel_connections, jump_hosts, enable_file_manager, enable_docker, show_terminal_in_sidebar, show_file_manager_in_sidebar, show_tunnel_in_sidebar, show_docker_in_sidebar, show_server_stats_in_sidebar, default_path, stats_config, docker_config, terminal_config, quick_actions, notes, use_socks5, socks5_host, socks5_port, socks5_username, socks5_password, socks5_proxy_chain, domain, security, ignore_cert, guacamole_config, mac_address, port_knock_sequence, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); for (const host of sshHosts) { @@ -864,6 +831,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { insertHost.run( decrypted.id, decrypted.userId, + decrypted.connectionType || "ssh", decrypted.name || null, decrypted.ip, decrypted.port, @@ -896,6 +864,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.showServerStatsInSidebar ? 1 : 0, decrypted.defaultPath || null, decrypted.statsConfig || null, + decrypted.dockerConfig || null, decrypted.terminalConfig || null, decrypted.quickActions || null, decrypted.notes || null, @@ -905,6 +874,12 @@ app.post("/database/export", authenticateJWT, async (req, res) => { decrypted.socks5Username || null, decrypted.socks5Password || null, decrypted.socks5ProxyChain || null, + decrypted.domain || null, + decrypted.security || null, + decrypted.ignoreCert ? 1 : 0, + decrypted.guacamoleConfig || null, + decrypted.macAddress || null, + decrypted.portKnockSequence || null, decrypted.createdAt, decrypted.updatedAt, ); @@ -1146,7 +1121,6 @@ app.post( } const userId = (req as AuthenticatedRequest).userId; - const { password } = req.body; const mainDb = getDb(); const deviceInfo = parseUserAgent(req); @@ -1161,30 +1135,20 @@ app.post( const isOidcUser = !!userRecords[0].isOidc; - if (!isOidcUser) { - if (!password) { - return res.status(400).json({ - error: "Password required for import", - code: "PASSWORD_REQUIRED", - }); - } - - const unlocked = await authManager.authenticateUser( - userId, - password, - deviceInfo.type, - ); - if (!unlocked) { - return res.status(401).json({ error: "Invalid password" }); - } - } else if (!DataCrypto.getUserDataKey(userId)) { - const oidcUnlocked = await authManager.authenticateOIDCUser( - userId, - deviceInfo.type, - ); - if (!oidcUnlocked) { + if (!DataCrypto.getUserDataKey(userId)) { + if (isOidcUser) { + const oidcUnlocked = await authManager.authenticateOIDCUser( + userId, + deviceInfo.type, + ); + if (!oidcUnlocked) { + return res.status(403).json({ + error: "Failed to unlock user data with SSO credentials", + }); + } + } else { return res.status(403).json({ - error: "Failed to unlock user data with SSO credentials", + error: "User data is locked. Please log in again.", }); } } @@ -1198,15 +1162,6 @@ app.post( }); let userDataKey = DataCrypto.getUserDataKey(userId); - if (!userDataKey && isOidcUser) { - const oidcUnlocked = await authManager.authenticateOIDCUser( - userId, - deviceInfo.type, - ); - if (oidcUnlocked) { - userDataKey = DataCrypto.getUserDataKey(userId); - } - } if (!userDataKey) { throw new Error("User data not unlocked"); } @@ -1982,7 +1937,24 @@ app.get( }, ); -app.listen(HTTP_PORT, async () => { +const httpServer = http.createServer(app); + +httpServer.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + databaseLogger.error( + `Port ${HTTP_PORT} is already in use. Kill the existing process and retry.`, + err, + { + operation: "http_server_port_conflict", + port: HTTP_PORT, + }, + ); + process.exit(1); + } + throw err; +}); + +httpServer.listen(HTTP_PORT, async () => { const uploadsDir = path.join(process.cwd(), "uploads"); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index ff47e6cb..34d99252 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -775,6 +775,34 @@ const migrateSchema = () => { } } + try { + sqlite.prepare("SELECT id FROM snippet_access LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS snippet_access ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + snippet_id INTEGER NOT NULL, + user_id TEXT, + role_id INTEGER, + granted_by TEXT NOT NULL, + permission_level TEXT NOT NULL DEFAULT 'view', + expires_at TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (snippet_id) REFERENCES snippets (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE, + FOREIGN KEY (granted_by) REFERENCES users (id) ON DELETE CASCADE + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create snippet_access table", { + operation: "schema_migration", + error: createError, + }); + } + } + try { sqlite .prepare("SELECT id FROM sessions LIMIT 1") @@ -955,6 +983,8 @@ const migrateSchema = () => { { column: "host_key_first_seen", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_first_seen TEXT" }, { column: "host_key_last_verified", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_last_verified TEXT" }, { column: "host_key_changed_count", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_changed_count INTEGER NOT NULL DEFAULT 0" }, + { column: "mac_address", sql: "ALTER TABLE ssh_data ADD COLUMN mac_address TEXT" }, + { column: "port_knock_sequence", sql: "ALTER TABLE ssh_data ADD COLUMN port_knock_sequence TEXT" }, ]; for (const migration of sshDataMigrations) { @@ -1255,7 +1285,7 @@ const migrateSchema = () => { }); }; -async function saveMemoryDatabaseToFile() { +async function saveMemoryDatabaseToFile(): Promise { if (!memoryDatabase) return; try { diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index d77200ce..0bc3ae6f 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -141,6 +141,9 @@ export const hosts = sqliteTable("ssh_data", { socks5Password: text("socks5_password"), socks5ProxyChain: text("socks5_proxy_chain"), + macAddress: text("mac_address"), + portKnockSequence: text("port_knock_sequence"), + hostKeyFingerprint: text("host_key_fingerprint"), hostKeyType: text("host_key_type"), hostKeyAlgorithm: text("host_key_algorithm").default("sha256"), @@ -295,6 +298,30 @@ export const snippetFolders = sqliteTable("snippet_folders", { .default(sql`CURRENT_TIMESTAMP`), }); +export const snippetAccess = sqliteTable("snippet_access", { + id: integer("id").primaryKey({ autoIncrement: true }), + snippetId: integer("snippet_id") + .notNull() + .references(() => snippets.id, { onDelete: "cascade" }), + + userId: text("user_id").references(() => users.id, { onDelete: "cascade" }), + roleId: integer("role_id").references(() => roles.id, { + onDelete: "cascade", + }), + + grantedBy: text("granted_by") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + + permissionLevel: text("permission_level").notNull().default("view"), + + expiresAt: text("expires_at"), + + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + export const sshFolders = sqliteTable("ssh_folders", { id: integer("id").primaryKey({ autoIncrement: true }), userId: text("user_id") diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index bf8931e0..2b06b25a 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -36,6 +36,7 @@ import { DataCrypto } from "../../utils/data-crypto.js"; import { SystemCrypto } from "../../utils/system-crypto.js"; import { DatabaseSaveTrigger } from "../db/index.js"; import { parseSSHKey } from "../../utils/ssh-key-utils.js"; +import { sendWakeOnLan, isValidMac } from "../../utils/wake-on-lan.js"; const router = express.Router(); @@ -49,6 +50,30 @@ function isValidPort(port: unknown): port is number { return typeof port === "number" && port > 0 && port <= 65535; } +const SENSITIVE_FIELDS = [ + "password", + "key", + "keyPassword", + "sudoPassword", + "autostartPassword", + "autostartKey", + "autostartKeyPassword", + "socks5Password", +]; + +function stripSensitiveFields( + host: Record, +): Record { + const result = { ...host }; + result.hasPassword = !!host.password; + result.hasKey = !!host.key; + result.hasSudoPassword = !!host.sudoPassword; + for (const field of SENSITIVE_FIELDS) { + delete result[field]; + } + return result; +} + function transformHostResponse( host: Record, ): Record { @@ -90,6 +115,9 @@ function transformHostResponse( socks5ProxyChain: host.socks5ProxyChain ? JSON.parse(host.socks5ProxyChain as string) : [], + portKnockSequence: host.portKnockSequence + ? JSON.parse(host.portKnockSequence as string) + : [], domain: host.domain || undefined, security: host.security || undefined, ignoreCert: !!host.ignoreCert, @@ -106,7 +134,7 @@ const requireDataAccess = authManager.createDataAccessMiddleware(); /** * @openapi - * /ssh/db/host/internal: + * /host/db/host/internal: * get: * summary: Get internal SSH host data * description: Returns internal SSH host data for autostart tunnels. Requires internal auth token. @@ -171,12 +199,6 @@ router.get("/db/host/internal", async (req: Request, res: Response) => { ip: host.ip, port: host.port, username: host.username, - password: host.autostartPassword, - key: host.autostartKey, - keyPassword: host.autostartKeyPassword, - autostartPassword: host.autostartPassword, - autostartKey: host.autostartKey, - autostartKeyPassword: host.autostartKeyPassword, authType: host.authType, keyType: host.keyType, credentialId: host.credentialId, @@ -206,7 +228,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => { /** * @openapi - * /ssh/db/host/internal/all: + * /host/db/host/internal/all: * get: * summary: Get all internal SSH host data * description: Returns all internal SSH host data. Requires internal auth token. @@ -252,12 +274,6 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => { ip: host.ip, port: host.port, username: host.username, - password: host.autostartPassword || host.password, - key: host.autostartKey || host.key, - keyPassword: host.autostartKeyPassword || host.keyPassword, - autostartPassword: host.autostartPassword, - autostartKey: host.autostartKey, - autostartKeyPassword: host.autostartKeyPassword, authType: host.authType, keyType: host.keyType, credentialId: host.credentialId, @@ -286,7 +302,7 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => { /** * @openapi - * /ssh/db/host: + * /host/db/host: * post: * summary: Create SSH host * description: Creates a new SSH host configuration. @@ -381,7 +397,9 @@ router.post( socks5Username, socks5Password, socks5ProxyChain, + portKnockSequence, overrideCredentialUsername, + macAddress, } = hostData; databaseLogger.info("Creating SSH host", { operation: "host_create", @@ -405,8 +423,11 @@ router.post( return res.status(400).json({ error: "Invalid SSH data" }); } - const effectiveAuthType = authType || authMethod; const effectiveConnectionType = connectionType || "ssh"; + const effectiveAuthType = + authType || + authMethod || + (effectiveConnectionType !== "ssh" ? "password" : undefined); const sshDataObj: Record = { userId: userId, connectionType: effectiveConnectionType, @@ -467,6 +488,10 @@ router.post( socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null, + macAddress: macAddress || null, + portKnockSequence: portKnockSequence + ? JSON.stringify(portKnockSequence) + : null, }; // For non-SSH hosts (RDP, VNC, Telnet), always save password if provided @@ -595,7 +620,7 @@ router.post( /** * @openapi - * /ssh/quick-connect: + * /host/quick-connect: * post: * summary: Create a temporary SSH connection without saving to database * description: Returns a temporary host configuration for immediate use @@ -778,7 +803,7 @@ router.post( /** * @openapi - * /ssh/db/host/{id}: + * /host/db/host/{id}: * put: * summary: Update SSH host * description: Updates an existing SSH host configuration. @@ -888,7 +913,9 @@ router.put( socks5Username, socks5Password, socks5ProxyChain, + portKnockSequence, overrideCredentialUsername, + macAddress, } = hostData; databaseLogger.info("Updating SSH host", { operation: "host_update", @@ -974,6 +1001,10 @@ router.put( socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null, + macAddress: macAddress || null, + portKnockSequence: portKnockSequence + ? JSON.stringify(portKnockSequence) + : null, }; // For non-SSH hosts (RDP, VNC, Telnet), always save password if provided @@ -1198,7 +1229,7 @@ router.put( /** * @openapi - * /ssh/db/host: + * /host/db/host: * get: * summary: Get all SSH hosts * description: Retrieves all SSH hosts for the authenticated user. @@ -1282,10 +1313,13 @@ router.get( socks5Username: hosts.socks5Username, socks5Password: hosts.socks5Password, socks5ProxyChain: hosts.socks5ProxyChain, + portKnockSequence: hosts.portKnockSequence, domain: hosts.domain, security: hosts.security, ignoreCert: hosts.ignoreCert, guacamoleConfig: hosts.guacamoleConfig, + macAddress: hosts.macAddress, + dockerConfig: hosts.dockerConfig, ownerId: hosts.userId, isShared: sql`${hostAccess.id} IS NOT NULL AND ${hosts.userId} != ${userId}`, @@ -1362,7 +1396,8 @@ router.get( }), ); - res.json(result); + const sanitized = result.map((host) => stripSensitiveFields(host)); + res.json(sanitized); } catch (err) { sshLogger.error("Failed to fetch SSH hosts from database", err, { operation: "host_fetch", @@ -1375,7 +1410,7 @@ router.get( /** * @openapi - * /ssh/db/host/{id}: + * /host/db/host/{id}: * get: * summary: Get SSH host by ID * description: Retrieves a specific SSH host by its ID. @@ -1436,8 +1471,9 @@ router.get( const host = data[0]; const result = transformHostResponse(host); + const resolved = (await resolveHostCredentials(result, userId)) || result; - res.json((await resolveHostCredentials(result, userId)) || result); + res.json(stripSensitiveFields(resolved)); } catch (err) { sshLogger.error("Failed to fetch SSH host by ID from database", err, { operation: "host_fetch_by_id", @@ -1451,7 +1487,79 @@ router.get( /** * @openapi - * /ssh/db/host/{id}/export: + * /host/db/host/{id}/password: + * get: + * summary: Get host password for clipboard copy + * description: Returns the password for a specific host. Used by the copy-password feature. + * tags: + * - SSH + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * - in: query + * name: field + * schema: + * type: string + * enum: [password, sudoPassword] + * responses: + * 200: + * description: The requested password value. + * 404: + * description: Host not found or no password set. + */ +router.get( + "/db/host/:id/password", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const hostId = Number(req.params.id); + const userId = (req as AuthenticatedRequest).userId; + const field = (req.query.field as string) || "password"; + + if (!["password", "sudoPassword"].includes(field)) { + return res.status(400).json({ error: "Invalid field" }); + } + + try { + const data = await SimpleDBOps.select( + db + .select() + .from(hosts) + .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))), + "ssh_data", + userId, + ); + + if (data.length === 0) { + return res.status(404).json({ error: "Host not found" }); + } + + const host = data[0]; + const resolved = (await resolveHostCredentials(host, userId)) || host; + const value = resolved[field]; + + if (!value) { + return res.status(404).json({ error: "No password set" }); + } + + res.json({ value }); + } catch (err) { + sshLogger.error("Failed to fetch host password", err, { + operation: "host_password_fetch", + hostId, + userId, + }); + res.status(500).json({ error: "Failed to fetch password" }); + } + }, +); + +/** + * @openapi + * /host/db/host/{id}/export: * get: * summary: Export SSH host * description: Exports a specific SSH host with decrypted credentials. @@ -1585,6 +1693,9 @@ router.get( socks5ProxyChain: resolvedHost.socks5ProxyChain ? JSON.parse(resolvedHost.socks5ProxyChain as string) : null, + portKnockSequence: resolvedHost.portKnockSequence + ? JSON.parse(resolvedHost.portKnockSequence as string) + : null, }; sshLogger.success("Host exported with decrypted credentials", { @@ -1605,6 +1716,148 @@ router.get( }, ); +/** + * @openapi + * /ssh/db/hosts/export: + * get: + * summary: Export all SSH hosts + * description: Exports all SSH hosts for the current user with decrypted credentials. + * tags: + * - SSH + * responses: + * 200: + * description: All exported SSH hosts. + * 400: + * description: Invalid userId. + * 500: + * description: Failed to export SSH hosts. + */ +router.get( + "/db/hosts/export", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + + if (!isNonEmptyString(userId)) { + return res.status(400).json({ error: "Invalid userId" }); + } + + try { + const allHosts = await SimpleDBOps.select( + db.select().from(hosts).where(eq(hosts.userId, userId)), + "ssh_data", + userId, + ); + + const exportedHosts = []; + + for (const host of allHosts) { + const resolvedHost = + (await resolveHostCredentials(host, userId)) || host; + + const exportedConnectionType = + (resolvedHost.connectionType as string) || "ssh"; + const isRemoteDesktop = ["rdp", "vnc", "telnet"].includes( + exportedConnectionType, + ); + + const baseExportData = { + connectionType: exportedConnectionType, + name: resolvedHost.name, + ip: resolvedHost.ip, + port: resolvedHost.port, + username: resolvedHost.username, + password: resolvedHost.password || null, + folder: resolvedHost.folder, + tags: + typeof resolvedHost.tags === "string" + ? resolvedHost.tags.split(",").filter(Boolean) + : resolvedHost.tags || [], + pin: !!resolvedHost.pin, + notes: resolvedHost.notes || null, + }; + + const exportData = isRemoteDesktop + ? { + ...baseExportData, + domain: resolvedHost.domain || null, + security: resolvedHost.security || null, + ignoreCert: !!resolvedHost.ignoreCert, + guacamoleConfig: resolvedHost.guacamoleConfig + ? JSON.parse(resolvedHost.guacamoleConfig as string) + : null, + } + : { + ...baseExportData, + authType: resolvedHost.authType, + key: resolvedHost.key || null, + keyPassword: resolvedHost.keyPassword || null, + keyType: resolvedHost.keyType || null, + credentialId: resolvedHost.credentialId || null, + overrideCredentialUsername: + !!resolvedHost.overrideCredentialUsername, + enableTerminal: !!resolvedHost.enableTerminal, + enableTunnel: !!resolvedHost.enableTunnel, + enableFileManager: !!resolvedHost.enableFileManager, + enableDocker: !!resolvedHost.enableDocker, + showTerminalInSidebar: !!resolvedHost.showTerminalInSidebar, + showFileManagerInSidebar: !!resolvedHost.showFileManagerInSidebar, + showTunnelInSidebar: !!resolvedHost.showTunnelInSidebar, + showDockerInSidebar: !!resolvedHost.showDockerInSidebar, + showServerStatsInSidebar: !!resolvedHost.showServerStatsInSidebar, + defaultPath: resolvedHost.defaultPath, + sudoPassword: resolvedHost.sudoPassword || null, + tunnelConnections: resolvedHost.tunnelConnections + ? JSON.parse(resolvedHost.tunnelConnections as string) + : [], + jumpHosts: resolvedHost.jumpHosts + ? JSON.parse(resolvedHost.jumpHosts as string) + : null, + quickActions: resolvedHost.quickActions + ? JSON.parse(resolvedHost.quickActions as string) + : null, + statsConfig: resolvedHost.statsConfig + ? JSON.parse(resolvedHost.statsConfig as string) + : null, + dockerConfig: resolvedHost.dockerConfig + ? JSON.parse(resolvedHost.dockerConfig as string) + : null, + terminalConfig: resolvedHost.terminalConfig + ? JSON.parse(resolvedHost.terminalConfig as string) + : null, + forceKeyboardInteractive: + resolvedHost.forceKeyboardInteractive === "true", + useSocks5: !!resolvedHost.useSocks5, + socks5Host: resolvedHost.socks5Host || null, + socks5Port: resolvedHost.socks5Port || null, + socks5Username: resolvedHost.socks5Username || null, + socks5Password: resolvedHost.socks5Password || null, + socks5ProxyChain: resolvedHost.socks5ProxyChain + ? JSON.parse(resolvedHost.socks5ProxyChain as string) + : null, + }; + + exportedHosts.push(exportData); + } + + sshLogger.success("All hosts exported with decrypted credentials", { + operation: "hosts_export_all", + count: exportedHosts.length, + userId, + }); + + res.json({ hosts: exportedHosts }); + } catch (err) { + sshLogger.error("Failed to export all SSH hosts", err, { + operation: "hosts_export_all", + userId, + }); + res.status(500).json({ error: "Failed to export SSH hosts" }); + } + }, +); + /** * @openapi * /ssh/db/host/{id}: @@ -1745,7 +1998,7 @@ router.delete( /** * @openapi - * /ssh/file_manager/recent: + * /host/file_manager/recent: * get: * summary: Get recent files * description: Retrieves a list of recent files for a specific host. @@ -1808,7 +2061,7 @@ router.get( /** * @openapi - * /ssh/file_manager/recent: + * /host/file_manager/recent: * post: * summary: Add recent file * description: Adds a file to the list of recent files for a host. @@ -1884,7 +2137,7 @@ router.post( /** * @openapi - * /ssh/file_manager/recent: + * /host/file_manager/recent: * delete: * summary: Remove recent file * description: Removes a file from the list of recent files for a host. @@ -1942,7 +2195,7 @@ router.delete( /** * @openapi - * /ssh/file_manager/pinned: + * /host/file_manager/pinned: * get: * summary: Get pinned files * description: Retrieves a list of pinned files for a specific host. @@ -2004,7 +2257,7 @@ router.get( /** * @openapi - * /ssh/file_manager/pinned: + * /host/file_manager/pinned: * post: * summary: Add pinned file * description: Adds a file to the list of pinned files for a host. @@ -2079,7 +2332,7 @@ router.post( /** * @openapi - * /ssh/file_manager/pinned: + * /host/file_manager/pinned: * delete: * summary: Remove pinned file * description: Removes a file from the list of pinned files for a host. @@ -2137,7 +2390,7 @@ router.delete( /** * @openapi - * /ssh/file_manager/shortcuts: + * /host/file_manager/shortcuts: * get: * summary: Get shortcuts * description: Retrieves a list of shortcuts for a specific host. @@ -2199,7 +2452,7 @@ router.get( /** * @openapi - * /ssh/file_manager/shortcuts: + * /host/file_manager/shortcuts: * post: * summary: Add shortcut * description: Adds a shortcut for a specific host. @@ -2274,7 +2527,7 @@ router.post( /** * @openapi - * /ssh/file_manager/shortcuts: + * /host/file_manager/shortcuts: * delete: * summary: Remove shortcut * description: Removes a shortcut for a specific host. @@ -2332,7 +2585,7 @@ router.delete( /** * @openapi - * /ssh/command-history/{hostId}: + * /host/command-history/{hostId}: * get: * summary: Get command history * description: Retrieves the command history for a specific host. @@ -2401,7 +2654,7 @@ router.get( /** * @openapi - * /ssh/command-history: + * /host/command-history: * delete: * summary: Delete command from history * description: Deletes a specific command from the history of a host. @@ -2559,7 +2812,7 @@ async function resolveHostCredentials( /** * @openapi - * /ssh/folders/rename: + * /host/folders/rename: * put: * summary: Rename folder * description: Renames a folder for SSH hosts and credentials. @@ -2659,7 +2912,7 @@ router.put( /** * @openapi - * /ssh/folders: + * /host/folders: * get: * summary: Get all folders * description: Retrieves all folders for the authenticated user. @@ -2698,7 +2951,7 @@ router.get("/folders", authenticateJWT, async (req: Request, res: Response) => { /** * @openapi - * /ssh/folders/metadata: + * /host/folders/metadata: * put: * summary: Update folder metadata * description: Updates the metadata (color, icon) of a folder. @@ -2789,7 +3042,7 @@ router.put( /** * @openapi - * /ssh/folders/{name}/hosts: + * /host/folders/{name}/hosts: * delete: * summary: Delete all hosts in folder * description: Deletes all SSH hosts within a specific folder. @@ -2935,7 +3188,7 @@ router.delete( /** * @openapi - * /ssh/bulk-import: + * /host/bulk-import: * post: * summary: Bulk import SSH hosts * description: Bulk imports multiple SSH hosts. @@ -2961,7 +3214,7 @@ router.delete( /** * @swagger - * /ssh/bulk-update: + * /host/bulk-update: * patch: * summary: Bulk update partial fields on multiple SSH hosts * tags: [SSH] @@ -3206,6 +3459,41 @@ router.post( continue; } + if ( + effectiveConnectionType === "ssh" && + hostData.authType === "credential" && + hostData.credentialId + ) { + const cred = await db + .select({ id: sshCredentials.id }) + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, hostData.credentialId), + eq(sshCredentials.userId, userId), + ), + ) + .limit(1); + + if (cred.length === 0) { + const fallback = await db + .select({ id: sshCredentials.id }) + .from(sshCredentials) + .where(eq(sshCredentials.userId, userId)) + .limit(1); + + if (fallback.length > 0) { + hostData.credentialId = fallback[0].id; + } else { + results.failed++; + results.errors.push( + `Host ${i + 1}: credentialId ${hostData.credentialId} not found and no fallback credential available`, + ); + continue; + } + } + } + const sshDataObj: Record = { userId: userId, connectionType: effectiveConnectionType, @@ -3257,6 +3545,9 @@ router.post( socks5ProxyChain: hostData.socks5ProxyChain ? JSON.stringify(hostData.socks5ProxyChain) : null, + portKnockSequence: hostData.portKnockSequence + ? JSON.stringify(hostData.portKnockSequence) + : null, overrideCredentialUsername: hostData.overrideCredentialUsername ? 1 : 0, @@ -3331,7 +3622,7 @@ router.post( /** * @openapi - * /ssh/folders/{folderName}/hosts: + * /host/folders/{folderName}/hosts: * delete: * summary: Delete all hosts in a folder * description: Deletes all hosts within a specific folder. @@ -3429,7 +3720,7 @@ router.delete( /** * @openapi - * /ssh/autostart/enable: + * /host/autostart/enable: * post: * summary: Enable autostart for SSH configuration * description: Enables autostart for a specific SSH configuration. @@ -3608,7 +3899,7 @@ router.post( /** * @openapi - * /ssh/autostart/disable: + * /host/autostart/disable: * delete: * summary: Disable autostart for SSH configuration * description: Disables autostart for a specific SSH configuration. @@ -3677,7 +3968,7 @@ router.delete( /** * @openapi - * /ssh/autostart/status: + * /host/autostart/status: * get: * summary: Get autostart status * description: Retrieves the autostart status for the user's SSH configurations. @@ -3733,7 +4024,7 @@ router.get( /** * @openapi - * /ssh/opkssh/token/{hostId}: + * /host/opkssh/token/{hostId}: * get: * summary: Get OPKSSH token status for a host * tags: [SSH] @@ -3832,7 +4123,7 @@ router.get( /** * @openapi - * /ssh/opkssh/token/{hostId}: + * /host/opkssh/token/{hostId}: * delete: * summary: Delete OPKSSH token for a host * tags: [SSH] @@ -3882,14 +4173,165 @@ router.delete( }, ); +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Replicates openpubkey's client/choosers/web_chooser.go IssuerToName(). +// OPKSSH's /select handler keys its providerMap by this derived name, NOT by the +// `alias` field in config.yml. We need the same mapping so we can normalize any +// `op=` query param we receive (which can be alias, issuer with protocol, or +// issuer without protocol depending on client version) to what OPKSSH expects. +function opksshIssuerToName(issuer: string): string | null { + if (!issuer) return null; + const withScheme = + issuer.startsWith("http://") || issuer.startsWith("https://") + ? issuer + : `https://${issuer}`; + if (withScheme.startsWith("https://accounts.google.com")) return "google"; + if (withScheme.startsWith("https://login.microsoftonline.com")) + return "azure"; + if (withScheme.startsWith("https://gitlab.com")) return "gitlab"; + if (withScheme.startsWith("https://issuer.hello.coop")) return "hello"; + if (withScheme.startsWith("https://")) { + const host = withScheme.slice("https://".length).split("/")[0]; + return host || null; + } + return null; +} + +function normalizeSelectOpParam( + rawOp: string, + providers: Array<{ alias: string; issuer: string }>, +): string { + if (!rawOp) return rawOp; + const knownNames = new Set( + providers + .map((p) => opksshIssuerToName(p.issuer)) + .filter((n): n is string => typeof n === "string" && n.length > 0), + ); + if (knownNames.has(rawOp)) return rawOp; + + const derivedFromRaw = opksshIssuerToName(rawOp); + if (derivedFromRaw && knownNames.has(derivedFromRaw)) return derivedFromRaw; + + const matchByAlias = providers.find((p) => p.alias === rawOp); + if (matchByAlias) { + const name = opksshIssuerToName(matchByAlias.issuer); + if (name) return name; + } + + return rawOp; +} + +interface OpksshErrorPageOptions { + title: string; + heading: string; + message: string; + details?: string; + requestId?: string; + statusCode?: number; +} + +function renderOpksshErrorPage(opts: OpksshErrorPageOptions): string { + const title = escapeHtml(opts.title); + const heading = escapeHtml(opts.heading); + const message = escapeHtml(opts.message); + const detailsBlock = opts.details + ? `
${escapeHtml(opts.details)}
` + : ""; + const requestIdBlock = opts.requestId + ? `

Request ID: ${escapeHtml(opts.requestId)}

` + : ""; + + return ` + + + ${title} + + + + +
+

${heading}

+

${message}

+ ${detailsBlock} + ${requestIdBlock} +
+ +`; +} + function rewriteOPKSSHHtml( html: string, requestId: string, routePrefix: "opkssh-chooser" | "opkssh-callback", ): string { - const basePath = `/ssh/${routePrefix}/${requestId}`; + const basePath = `/host/${routePrefix}/${requestId}`; + const localHostPattern = "(?:localhost|127\\.0\\.0\\.1)"; - const attrPatterns = ["action", "href", "src"]; + const attrPatterns = ["action", "href", "src", "formaction"]; for (const attr of attrPatterns) { html = html.replace( new RegExp(`${attr}="(/[^"]*)`, "g"), @@ -3901,48 +4343,75 @@ function rewriteOPKSSHHtml( ); } - html = html.replace( - /href=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g, - `href="${basePath}/$1`, - ); - html = html.replace( - /action=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g, - `action="${basePath}/$1`, - ); - html = html.replace( - /src=["']?http:\/\/localhost:\d+\/([^"'\s]*)/g, - `src="${basePath}/$1`, - ); + for (const attr of ["href", "action", "src", "formaction"]) { + html = html.replace( + new RegExp( + `${attr}=["']?http:\\/\\/${localHostPattern}:\\d+\\/([^"'\\s]*)`, + "g", + ), + `${attr}="${basePath}/$1`, + ); + } html = html.replace( - /(window\.location\.href\s*=\s*["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + new RegExp( + `(window\\.location\\.href\\s*=\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, + "g", + ), `$1${basePath}/$2$3`, ); html = html.replace( - /(window\.location\s*=\s*["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + new RegExp( + `(window\\.location\\s*=\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, + "g", + ), `$1${basePath}/$2$3`, ); html = html.replace( - /(fetch\(["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + new RegExp( + `(fetch\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, + "g", + ), `$1${basePath}/$2$3`, ); html = html.replace( - /(location\.assign\(["'])http:\/\/localhost:\d+\/([^"']*)(["']\))/g, + new RegExp( + `(location\\.assign\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["']\\))`, + "g", + ), `$1${basePath}/$2$3`, ); html = html.replace( - /(location\.replace\(["'])http:\/\/localhost:\d+\/([^"']*)(["']\))/g, + new RegExp( + `(location\\.replace\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["']\\))`, + "g", + ), + `$1${basePath}/$2$3`, + ); + + // XMLHttpRequest.open("GET", "http://localhost:PORT/path", ...) + html = html.replace( + new RegExp( + `(\\.open\\(["']\\w+["']\\s*,\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, + "g", + ), `$1${basePath}/$2$3`, ); html = html.replace( - /(]+http-equiv=["']refresh["'][^>]+content=["'][^;]+;\s*url=)http:\/\/localhost:\d+\/([^"']+)(["'][^>]*>)/gi, + new RegExp( + `(]+http-equiv=["']refresh["'][^>]+content=["'][^;]+;\\s*url=)http:\\/\\/${localHostPattern}:\\d+\\/([^"']+)(["'][^>]*>)`, + "gi", + ), `$1${basePath}/$2$3`, ); html = html.replace( - /(data-[\w-]+=["'])http:\/\/localhost:\d+\/([^"']*)(["'])/g, + new RegExp( + `(data-[\\w-]+=["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, + "g", + ), `$1${basePath}/$2$3`, ); @@ -3985,7 +4454,7 @@ function rewriteOPKSSHHtml( /** * @openapi - * /opkssh-chooser/{requestId}: + * /host/opkssh-chooser/{requestId}: * get: * summary: Proxy OPKSSH provider chooser page and all related resources * tags: [SSH] @@ -4014,7 +4483,7 @@ router.use( const fullPath = req.originalUrl || req.url; const pathAfterRequestIdTemp = - fullPath.split(`/ssh/opkssh-chooser/${requestId}`)[1] || ""; + fullPath.split(`/host/opkssh-chooser/${requestId}`)[1] || ""; sshLogger.info("OPKSSH chooser proxy request", { operation: "opkssh_chooser_proxy_request", @@ -4027,7 +4496,8 @@ router.use( }); try { - const { getActiveAuthSession } = await import("../../ssh/opkssh-auth.js"); + const { getActiveAuthSession, registerOAuthState } = + await import("../../ssh/opkssh-auth.js"); const session = getActiveAuthSession(requestId); if (!session) { @@ -4035,59 +4505,14 @@ router.use( operation: "opkssh_chooser_session_not_found", requestId, }); - res.status(404).send(` - - - - Session Not Found - - - - -
-

Session Not Found

-

This authentication session has expired or is invalid.

-
- - - `); + res.status(404).send( + renderOpksshErrorPage({ + title: "Session Not Found", + heading: "Session Not Found", + message: "This authentication session has expired or is invalid.", + requestId, + }), + ); return; } @@ -4095,7 +4520,7 @@ router.use( const fullPath = req.originalUrl || req.url; const pathAfterRequestId = - fullPath.split(`/ssh/opkssh-chooser/${requestId}`)[1] || ""; + fullPath.split(`/host/opkssh-chooser/${requestId}`)[1] || ""; const targetPath = pathAfterRequestId || "/chooser"; if (!session.localPort || session.localPort === 0) { @@ -4104,64 +4529,316 @@ router.use( requestId, sessionStatus: session.status, }); - res.status(500).send(` - - - - Error - - - - -
-

Authentication Error

-

Failed to load authentication page. OPKSSH process may not be ready yet. Please try again.

-
- - - `); + res.status(500).send( + renderOpksshErrorPage({ + title: "Error", + heading: "Authentication Error", + message: + "Failed to load authentication page. OPKSSH process may not be ready yet. Please try again.", + requestId, + }), + ); return; } - const targetUrl = `http://localhost:${session.localPort}${targetPath}`; + // /select on OPKSSH's chooser redirects (possibly via multiple local hops) to the + // external OAuth provider URL. The hops we may see: + // 1. /select -> /select/ (Go ServeMux canonicalization, same chooser port) + // 2. /select/?op=ALIAS -> http://localhost:CALLBACK_PORT/login (OPKSSH's separate callback listener) + // 3. /login on the callback listener -> https:///authorize?... (external OAuth URL) + if (targetPath.startsWith("/select")) { + const selectaxios = (await import("axios")).default; + const rawQs = targetPath.includes("?") + ? targetPath.slice(targetPath.indexOf("?")) + : ""; + + let qs = rawQs; + let opMappedFrom: string | undefined; + if (rawQs) { + try { + const params = new URLSearchParams(rawQs.replace(/^\?/, "")); + const rawOp = params.get("op"); + if (rawOp) { + const mappedOp = normalizeSelectOpParam( + rawOp, + session.providers || [], + ); + if (mappedOp !== rawOp) { + params.set("op", mappedOp); + qs = `?${params.toString()}`; + opMappedFrom = rawOp; + } + } + } catch { + /* keep rawQs if parsing fails */ + } + } + + const chooserHost = `127.0.0.1:${session.localPort}`; + const startUrl = `http://${chooserHost}/select/${qs}`; + + sshLogger.info("Proxying OPKSSH /select", { + operation: "opkssh_select_proxy", + requestId, + targetUrl: startUrl, + opMappedFrom, + }); + + const isLocalHostname = (host: string): boolean => { + const bare = host.split(":")[0]; + return ( + bare === "127.0.0.1" || bare === "localhost" || bare === "[::1]" + ); + }; + + interface UpstreamResponse { + status: number; + location?: string; + contentType: string; + body: string; + targetUrl: string; + elapsedMs: number; + } + + const fetchUpstream = async ( + url: string, + ): Promise => { + const started = Date.now(); + let hostHeader = chooserHost; + try { + hostHeader = new URL(url).host; + } catch { + /* fall back to chooser host */ + } + const r = await selectaxios({ + method: "GET", + url, + maxRedirects: 0, + validateStatus: () => true, + timeout: 10000, + responseType: "text", + transformResponse: (v) => v, + headers: { host: hostHeader }, + }); + const locHeader = r.headers["location"]; + const location = Array.isArray(locHeader) ? locHeader[0] : locHeader; + const ctHeader = r.headers["content-type"]; + const ctRaw = Array.isArray(ctHeader) ? ctHeader[0] : ctHeader; + const contentType = typeof ctRaw === "string" ? ctRaw : ""; + const body = + typeof r.data === "string" ? r.data : String(r.data ?? ""); + return { + status: r.status, + location: typeof location === "string" ? location : undefined, + contentType, + body, + targetUrl: url, + elapsedMs: Date.now() - started, + }; + }; + + const logResponse = (response: UpstreamResponse): void => { + sshLogger.info("OPKSSH /select upstream response", { + operation: "opkssh_select_upstream_response", + requestId, + targetUrl: response.targetUrl, + status: response.status, + location: response.location, + contentType: response.contentType, + elapsedMs: response.elapsedMs, + bodyPreview: response.body.slice(0, 256), + }); + }; + + const MAX_HOPS = 4; + + try { + let response = await fetchUpstream(startUrl); + logResponse(response); + + for (let hop = 0; hop < MAX_HOPS; hop++) { + if ( + response.status < 300 || + response.status >= 400 || + !response.location + ) { + break; + } + const loc = response.location; + + // Relative path: resolve against the current upstream. + if (loc.startsWith("/")) { + let currentHost = chooserHost; + try { + currentHost = new URL(response.targetUrl).host; + } catch { + /* keep default */ + } + response = await fetchUpstream(`http://${currentHost}${loc}`); + logResponse(response); + continue; + } + + // Absolute URL: if it points to a localhost OPKSSH endpoint, capture + // the port. Then redirect the BROWSER to the proxied path so that + // Set-Cookie headers from OPKSSH's /login handler reach the browser + // directly — following them server-side would swallow the cookie. + if (/^https?:\/\//i.test(loc)) { + try { + const parsed = new URL(loc); + if (isLocalHostname(parsed.host)) { + // Capture callback listener port if not yet known. + if (!session.callbackPort) { + const port = parseInt(parsed.port, 10); + if (!Number.isNaN(port)) { + session.callbackPort = port; + sshLogger.info( + "Captured OPKSSH callback listener port from /select redirect", + { + operation: "opkssh_select_callback_port_detected", + requestId, + callbackPort: port, + }, + ); + } + } + // Redirect browser through the chooser proxy so it can receive + // the state cookie that OPKSSH sets on /login. + const browserPath = `/host/opkssh-chooser/${requestId}${parsed.pathname}${parsed.search}`; + sshLogger.info( + "Redirecting browser to OPKSSH callback listener via proxy", + { + operation: "opkssh_select_browser_redirect_to_login", + requestId, + browserPath, + callbackPort: session.callbackPort, + }, + ); + res.redirect(302, browserPath); + return; + } + // External OAuth provider URL — done, handled below. + break; + } catch { + break; + } + } + + break; + } + + const isExternalRedirect = + response.status >= 300 && + response.status < 400 && + !!response.location && + /^https?:\/\//i.test(response.location) && + (() => { + try { + return !isLocalHostname( + new URL(response.location as string).host, + ); + } catch { + return false; + } + })(); + + if (isExternalRedirect) { + const oauthUrl = response.location as string; + try { + const parsed = new URL(oauthUrl); + const oauthState = parsed.searchParams.get("state"); + if (oauthState) registerOAuthState(oauthState, requestId); + } catch { + /* already validated above */ + } + sshLogger.info( + "OPKSSH /select redirecting browser to OAuth provider", + { + operation: "opkssh_select_redirect", + requestId, + oauthUrl, + }, + ); + res.redirect(302, oauthUrl); + return; + } + + const bodyPreview = response.body.slice(0, 512); + const detailLines = [ + `Upstream: ${response.targetUrl}`, + `Status: ${response.status}`, + response.location ? `Location: ${response.location}` : undefined, + `Content-Type: ${response.contentType || "(none)"}`, + `Elapsed: ${response.elapsedMs}ms`, + "", + bodyPreview + ? `Body (first 512 chars):\n${bodyPreview}` + : "Body: (empty)", + ].filter(Boolean) as string[]; + + sshLogger.error("OPKSSH /select did not produce an OAuth redirect", { + operation: "opkssh_select_no_oauth_redirect", + requestId, + status: response.status, + location: response.location, + contentType: response.contentType, + bodyPreview, + }); + + res.status(502).send( + renderOpksshErrorPage({ + title: "OPKSSH error", + heading: "Failed to get OAuth redirect", + message: + "OPKSSH did not return an external OAuth provider URL. " + + "This typically indicates a configuration mismatch between the provider's redirect_uris " + + "and the Termix callback path. Check the server log for the OPKSSH response body.", + details: detailLines.join("\n"), + requestId, + }), + ); + } catch (err) { + sshLogger.error("Error proxying OPKSSH /select", err, { + operation: "opkssh_select_proxy_error", + requestId, + targetUrl: startUrl, + }); + const errMsg = err instanceof Error ? err.message : String(err); + res.status(502).send( + renderOpksshErrorPage({ + title: "OPKSSH error", + heading: "Failed to reach OPKSSH service", + message: + "Termix could not connect to the local OPKSSH authentication service. " + + "The OPKSSH process may have exited or is not listening yet.", + details: `Upstream: ${startUrl}\nError: ${errMsg}`, + requestId, + }), + ); + } + return; + } + + // Paths served by the callback listener, not the chooser. + // The browser is redirected here so it receives Set-Cookie from OPKSSH. + const isCallbackListenerPath = + targetPath === "/login" || + targetPath.startsWith("/login?") || + targetPath === "/login-callback" || + targetPath.startsWith("/login-callback?"); + + const upstreamPort = + isCallbackListenerPath && session.callbackPort + ? session.callbackPort + : session.localPort; + + const targetUrl = `http://127.0.0.1:${upstreamPort}${targetPath}`; sshLogger.info("Proxying to OPKSSH chooser", { operation: "opkssh_chooser_proxy_request_to_opkssh", requestId, targetUrl, - localPort: session.localPort, + upstreamPort, targetPath, }); @@ -4170,7 +4847,7 @@ router.use( url: targetUrl, headers: { ...req.headers, - host: `localhost:${session.localPort}`, + host: `127.0.0.1:${upstreamPort}`, }, data: req.body, timeout: 10000, @@ -4195,36 +4872,73 @@ router.use( if (key.toLowerCase() === "location") { const location = value as string; if (location.startsWith("/")) { - res.setHeader(key, `/ssh/opkssh-chooser/${requestId}${location}`); + res.setHeader(key, `/host/opkssh-chooser/${requestId}${location}`); } else { const localhostMatch = location.match( - /^http:\/\/localhost:(\d+)(\/.*)?$/, + /^http:\/\/(?:localhost|127\.0\.0\.1):(\d+)(\/.*)?$/, ); if (localhostMatch) { const port = parseInt(localhostMatch[1], 10); const path = localhostMatch[2] || "/"; if (session.callbackPort && port === session.callbackPort) { - res.setHeader(key, `/ssh/opkssh-callback/${requestId}${path}`); + res.setHeader(key, `/host/opkssh-callback/${requestId}${path}`); } else if (port === session.localPort) { - res.setHeader(key, `/ssh/opkssh-chooser/${requestId}${path}`); + res.setHeader(key, `/host/opkssh-chooser/${requestId}${path}`); } else { const isCallback = path.includes("login") || path.includes("callback"); const prefix = isCallback ? "opkssh-callback" : "opkssh-chooser"; - res.setHeader(key, `/ssh/${prefix}/${requestId}${path}`); + res.setHeader(key, `/host/${prefix}/${requestId}${path}`); } } else { + // External redirect (e.g. to OIDC provider) — capture OAuth state for session binding + try { + const redirectUrl = new URL(location); + const oauthState = redirectUrl.searchParams.get("state"); + if (oauthState) { + registerOAuthState(oauthState, requestId); + } + } catch { + // Not a valid URL, skip state capture + } res.setHeader(key, value as string); } } + } else if (key.toLowerCase() === "set-cookie") { + // Rewrite cookies from OPKSSH's internal listener so they are scoped + // to the Termix proxy path instead of OPKSSH's internal path. + // The state cookie set by /login must survive to /login-callback. + const cookies = Array.isArray(value) ? value : [value as string]; + const rewritten = cookies.map((cookie) => { + return cookie + .replace(/;\s*domain=[^;]*/gi, "") + .replace(/;\s*path=[^;]*/gi, "; Path=/host/opkssh-callback/") + .concat( + cookie.match(/;\s*path=/i) + ? "" + : "; Path=/host/opkssh-callback/", + ); + }); + res.setHeader(key, rewritten); } else { res.setHeader(key, value as string); } }); - const contentType = response.headers["content-type"] || ""; + // Set a cookie to correlate this browser with the requestId. + // OAuth state capture from Location headers only works for 3xx redirects; + // if OPKSSH redirects via JavaScript, the state is never registered. + // This cookie survives the OIDC round-trip and identifies the session on callback. + res.cookie("opkssh_request_id", requestId, { + path: "/host/", + httpOnly: true, + sameSite: "lax", + maxAge: 5 * 60 * 1000, + }); + + const contentType = String(response.headers["content-type"] || ""); if (contentType.includes("text/html")) { const html = rewriteOPKSSHHtml( response.data.toString("utf-8"), @@ -4240,66 +4954,21 @@ router.use( operation: "opkssh_chooser_proxy_error", requestId, }); - res.status(500).send(` - - - - Error - - - - -
-

Error

-

Failed to load authentication page. Please try again.

-
- - - `); + res.status(500).send( + renderOpksshErrorPage({ + title: "Error", + heading: "Error", + message: "Failed to load authentication page. Please try again.", + requestId, + }), + ); } }, ); /** * @openapi - * /opkssh-callback: + * /host/opkssh-callback: * get: * summary: Static OAuth callback from OIDC provider for OPKSSH authentication * tags: [SSH] @@ -4315,19 +4984,16 @@ router.get("/opkssh-callback", async (req: Request, res: Response) => { try { sshLogger.info("OAuth callback received", { operation: "opkssh_static_callback_received", - url: req.url, - originalUrl: req.originalUrl, - query: req.query, - headers: { - host: req.headers.host, - "x-forwarded-proto": req.headers["x-forwarded-proto"], - "x-forwarded-host": req.headers["x-forwarded-host"], - "x-forwarded-port": req.headers["x-forwarded-port"], - }, + host: req.headers.host, }); - const { getUserIdFromRequest, getActiveSessionsForUser } = - await import("../../ssh/opkssh-auth.js"); + const { + getUserIdFromRequest, + getActiveSessionsForUser, + getActiveAuthSession, + getRequestIdByOAuthState, + clearOAuthState, + } = await import("../../ssh/opkssh-auth.js"); const userId = await getUserIdFromRequest({ cookies: req.cookies, @@ -4341,17 +5007,63 @@ router.get("/opkssh-callback", async (req: Request, res: Response) => { cookieKeys: Object.keys(req.cookies || {}), }); - if (!userId) { - sshLogger.error("No userId from callback request", { - operation: "opkssh_callback_unauthorized", - cookies: Object.keys(req.cookies || {}), - headers: Object.keys(req.headers), - }); - res.status(401).send("Unauthorized - no valid session"); - return; - } + let userSessions: Awaited> = []; - const userSessions = getActiveSessionsForUser(userId); + if (userId) { + userSessions = getActiveSessionsForUser(userId); + } else { + // No JWT cookie (e.g. OAuth redirect landed in external browser). + // Try to find the correct session via the OAuth state parameter. + const oauthState = req.query.state as string | undefined; + + if (oauthState) { + const mappedRequestId = getRequestIdByOAuthState(oauthState); + if (mappedRequestId) { + const mappedSession = getActiveAuthSession(mappedRequestId); + if (mappedSession) { + userSessions = [mappedSession]; + clearOAuthState(oauthState); + sshLogger.info("Resolved session via OAuth state parameter", { + operation: "opkssh_callback_state_lookup", + requestId: mappedRequestId, + }); + } + } + } + + // Fallback: use the opkssh_request_id cookie set by the chooser proxy. + // State capture only works for 3xx redirects; if OPKSSH redirects via + // JavaScript in the HTML, the state is never registered in the map. + if (userSessions.length === 0) { + const cookieRequestId = req.cookies?.opkssh_request_id; + if (cookieRequestId) { + const cookieSession = getActiveAuthSession(cookieRequestId); + if (cookieSession) { + userSessions = [cookieSession]; + res.clearCookie("opkssh_request_id", { path: "/host/" }); + sshLogger.info("Resolved session via opkssh_request_id cookie", { + operation: "opkssh_callback_cookie_lookup", + requestId: cookieRequestId, + }); + } + } + } + + if (userSessions.length === 0) { + sshLogger.warn( + "OAuth callback with no JWT, no matching state, and no session cookie", + { + operation: "opkssh_callback_no_session_match", + hasState: !!oauthState, + hasCookie: !!req.cookies?.opkssh_request_id, + }, + ); + res + .status(401) + .send("Authentication callback failed: unable to identify session"); + return; + } + } sshLogger.info("Active sessions for user", { operation: "opkssh_callback_session_lookup", @@ -4393,7 +5105,9 @@ router.get("/opkssh-callback", async (req: Request, res: Response) => { const queryString = req.url.includes("?") ? req.url.substring(req.url.indexOf("?")) : ""; - const redirectUrl = `/ssh/opkssh-callback/${session.requestId}/login-callback${queryString}`; + // OPKSSH's internal callback listener handles `/login-callback` regardless of the + // path used in --remote-redirect-uri. The dynamic route below defaults to that path. + const redirectUrl = `/host/opkssh-callback/${session.requestId}${queryString}`; sshLogger.info("Redirecting OAuth callback to dynamic route", { operation: "opkssh_static_callback_redirect", @@ -4417,7 +5131,7 @@ router.get("/opkssh-callback", async (req: Request, res: Response) => { /** * @openapi - * /opkssh-callback/{requestId}: + * /host/opkssh-callback/{requestId}: * get: * summary: OAuth callback from OIDC provider for OPKSSH authentication (handles all sub-paths) * tags: [SSH] @@ -4448,71 +5162,29 @@ router.use( const session = getActiveAuthSession(requestId); if (!session) { - res.status(404).send(` - - - - Session Not Found - - - - -
-

Session Not Found

-

Authentication session expired or invalid.

-

Please close this window and try again.

-
- - - `); + res.status(404).send( + renderOpksshErrorPage({ + title: "Session Not Found", + heading: "Session Not Found", + message: + "Authentication session expired or invalid. Please close this window and try again.", + requestId, + }), + ); return; } const axios = (await import("axios")).default; const fullPath = req.originalUrl || req.url; const pathAfterRequestId = - fullPath.split(`/ssh/opkssh-callback/${requestId}`)[1] || ""; - const targetPath = pathAfterRequestId || "/login-callback"; + fullPath.split(`/host/opkssh-callback/${requestId}`)[1] || ""; + // pathAfterRequestId may be "", "?query=...", "/subpath", or "/subpath?query=..." + // OPKSSH's internal listener serves /login-callback, so when no sub-path is present + // (query-only or empty), prepend it. + const targetPath = + pathAfterRequestId === "" || pathAfterRequestId.startsWith("?") + ? `/login-callback${pathAfterRequestId}` + : pathAfterRequestId; if (!session.callbackPort || session.callbackPort === 0) { sshLogger.error("OPKSSH callback session has no callback port", { @@ -4520,65 +5192,26 @@ router.use( requestId, sessionStatus: session.status, }); - res.status(500).send(` - - - - Error - - - - -
-

Callback Error

-

OPKSSH callback listener not ready. Please try authenticating again.

-
- - - `); + res.status(500).send( + renderOpksshErrorPage({ + title: "Error", + heading: "Callback Error", + message: + "OPKSSH callback listener not ready. Please try authenticating again.", + requestId, + }), + ); return; } - const targetUrl = `http://localhost:${session.callbackPort}${targetPath}`; + const targetUrl = `http://127.0.0.1:${session.callbackPort}${targetPath}`; const response = await axios({ method: req.method, url: targetUrl, headers: { ...req.headers, - host: `localhost:${session.callbackPort}`, + host: `127.0.0.1:${session.callbackPort}`, }, data: req.body, timeout: 10000, @@ -4594,7 +5227,7 @@ router.use( if (key.toLowerCase() === "location") { const location = value as string; if (location.startsWith("/")) { - res.setHeader(key, `/ssh/opkssh-callback/${requestId}${location}`); + res.setHeader(key, `/host/opkssh-callback/${requestId}${location}`); } else { res.setHeader(key, value as string); } @@ -4603,7 +5236,7 @@ router.use( } }); - const contentType = response.headers["content-type"] || ""; + const contentType = String(response.headers["content-type"] || ""); if (contentType.includes("text/html")) { const html = rewriteOPKSSHHtml( response.data.toString("utf-8"), @@ -4620,66 +5253,21 @@ router.use( requestId, }); - res.status(500).send(` - - - - Error - - - - -
-

Error

-

An unexpected error occurred. Please try again.

-
- - - `); + res.status(500).send( + renderOpksshErrorPage({ + title: "Error", + heading: "Error", + message: "An unexpected error occurred. Please try again.", + requestId, + }), + ); } }, ); /** * @openapi - * /db/proxy/test: + * /host/db/proxy/test: * post: * summary: Test proxy connectivity * description: Tests connectivity through a proxy configuration to a target host. @@ -4753,4 +5341,52 @@ router.post( }, ); +router.post( + "/db/host/:id/wake", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const hostId = parseInt(req.params.id); + const userId = (req as AuthenticatedRequest).userId; + + try { + const host = await db + .select({ macAddress: hosts.macAddress }) + .from(hosts) + .where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))) + .then((rows) => rows[0]); + + if (!host) { + return res.status(404).json({ error: "Host not found" }); + } + + if (!host.macAddress || !isValidMac(host.macAddress)) { + return res + .status(400) + .json({ error: "No valid MAC address configured" }); + } + + await sendWakeOnLan(host.macAddress); + + sshLogger.info("Wake-on-LAN packet sent", { + operation: "wake_on_lan", + userId, + hostId, + }); + + res.json({ success: true }); + } catch (error) { + sshLogger.error("Wake-on-LAN failed", error, { + operation: "wake_on_lan", + userId, + hostId, + }); + res.status(500).json({ + error: + error instanceof Error ? error.message : "Failed to send WoL packet", + }); + } + }, +); + export default router; diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts index 62f41aa6..4d4eda7b 100644 --- a/src/backend/database/routes/rbac.ts +++ b/src/backend/database/routes/rbac.ts @@ -8,6 +8,8 @@ import { roles, userRoles, sharedCredentials, + snippets, + snippetAccess, } from "../db/schema.js"; import { eq, and, desc, sql, or, isNull, gte } from "drizzle-orm"; import type { Response } from "express"; @@ -516,46 +518,6 @@ router.get( }, ); -/** - * @openapi - * /rbac/roles: - * get: - * summary: Get all roles - * description: Retrieves a list of all roles. - * tags: - * - RBAC - * responses: - * 200: - * description: A list of roles. - * 500: - * description: Failed to get roles. - */ -router.get( - "/roles", - authenticateJWT, - permissionManager.requireAdmin(), - async (req: AuthenticatedRequest, res: Response) => { - try { - const allRoles = await db - .select() - .from(roles) - .orderBy(roles.isSystem, roles.name); - - const rolesWithParsedPermissions = allRoles.map((role) => ({ - ...role, - permissions: JSON.parse(role.permissions), - })); - - res.json({ roles: rolesWithParsedPermissions }); - } catch (error) { - databaseLogger.error("Failed to get roles", error, { - operation: "get_roles", - }); - res.status(500).json({ error: "Failed to get roles" }); - } - }, -); - /** * @openapi * /rbac/roles: @@ -1193,4 +1155,369 @@ router.get( }, ); +// ============================================================================ +// SNIPPET SHARING +// ============================================================================ + +/** + * @openapi + * /rbac/snippet/{id}/share: + * post: + * summary: Share a snippet + * description: Shares a snippet with a user or role. + * tags: + * - RBAC + */ +router.post( + "/snippet/:id/share", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const snippetId = parseInt(id, 10); + const userId = req.userId!; + + if (isNaN(snippetId)) { + return res.status(400).json({ error: "Invalid snippet ID" }); + } + + try { + const { + targetType = "user", + targetUserId, + targetRoleId, + durationHours, + } = req.body; + + if (!["user", "role"].includes(targetType)) { + return res + .status(400) + .json({ error: "Invalid target type. Must be 'user' or 'role'" }); + } + + if (targetType === "user" && !isNonEmptyString(targetUserId)) { + return res + .status(400) + .json({ error: "Target user ID is required when sharing with user" }); + } + if (targetType === "role" && !targetRoleId) { + return res + .status(400) + .json({ error: "Target role ID is required when sharing with role" }); + } + + const snippet = await db + .select() + .from(snippets) + .where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId))) + .limit(1); + + if (snippet.length === 0) { + return res.status(403).json({ error: "Not snippet owner" }); + } + + if (targetType === "user") { + const targetUser = await db + .select({ id: users.id }) + .from(users) + .where(eq(users.id, targetUserId)) + .limit(1); + if (targetUser.length === 0) { + return res.status(404).json({ error: "Target user not found" }); + } + } else { + const targetRole = await db + .select({ id: roles.id }) + .from(roles) + .where(eq(roles.id, targetRoleId)) + .limit(1); + if (targetRole.length === 0) { + return res.status(404).json({ error: "Target role not found" }); + } + } + + let expiresAt: string | null = null; + if ( + durationHours && + typeof durationHours === "number" && + durationHours > 0 + ) { + const expiryDate = new Date(); + expiryDate.setHours(expiryDate.getHours() + durationHours); + expiresAt = expiryDate.toISOString(); + } + + const whereConditions = [eq(snippetAccess.snippetId, snippetId)]; + if (targetType === "user") { + whereConditions.push(eq(snippetAccess.userId, targetUserId)); + } else { + whereConditions.push(eq(snippetAccess.roleId, targetRoleId)); + } + + const existing = await db + .select() + .from(snippetAccess) + .where(and(...whereConditions)) + .limit(1); + + if (existing.length > 0) { + await db + .update(snippetAccess) + .set({ expiresAt }) + .where(eq(snippetAccess.id, existing[0].id)); + + return res.json({ + success: true, + message: "Snippet access updated", + expiresAt, + }); + } + + await db.insert(snippetAccess).values({ + snippetId, + userId: targetType === "user" ? targetUserId : null, + roleId: targetType === "role" ? targetRoleId : null, + grantedBy: userId, + permissionLevel: "view", + expiresAt, + }); + + databaseLogger.success("Snippet shared successfully", { + operation: "rbac_snippet_share", + userId, + }); + + res.json({ + success: true, + message: `Snippet shared successfully with ${targetType}`, + expiresAt, + }); + } catch (error) { + databaseLogger.error("Failed to share snippet", error, { + operation: "share_snippet", + userId, + }); + res.status(500).json({ error: "Failed to share snippet" }); + } + }, +); + +/** + * @openapi + * /rbac/snippet/{id}/access/{accessId}: + * delete: + * summary: Revoke snippet access + * description: Revokes a user's or role's access to a snippet. + * tags: + * - RBAC + */ +router.delete( + "/snippet/:id/access/:accessId", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const accessIdParam = Array.isArray(req.params.accessId) + ? req.params.accessId[0] + : req.params.accessId; + const snippetId = parseInt(id, 10); + const accessId = parseInt(accessIdParam, 10); + const userId = req.userId!; + + if (isNaN(snippetId) || isNaN(accessId)) { + return res.status(400).json({ error: "Invalid ID" }); + } + + try { + const snippet = await db + .select() + .from(snippets) + .where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId))) + .limit(1); + + if (snippet.length === 0) { + return res.status(403).json({ error: "Not snippet owner" }); + } + + await db.delete(snippetAccess).where(eq(snippetAccess.id, accessId)); + + res.json({ success: true, message: "Snippet access revoked" }); + } catch (error) { + databaseLogger.error("Failed to revoke snippet access", error, { + operation: "revoke_snippet_access", + userId, + }); + res.status(500).json({ error: "Failed to revoke access" }); + } + }, +); + +/** + * @openapi + * /rbac/snippet/{id}/access: + * get: + * summary: Get snippet access list + * description: Retrieves the list of users and roles with access to a snippet. + * tags: + * - RBAC + */ +router.get( + "/snippet/:id/access", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const snippetId = parseInt(id, 10); + const userId = req.userId!; + + if (isNaN(snippetId)) { + return res.status(400).json({ error: "Invalid snippet ID" }); + } + + try { + const snippet = await db + .select() + .from(snippets) + .where(and(eq(snippets.id, snippetId), eq(snippets.userId, userId))) + .limit(1); + + if (snippet.length === 0) { + return res.status(403).json({ error: "Not snippet owner" }); + } + + const rawAccessList = await db + .select({ + id: snippetAccess.id, + userId: snippetAccess.userId, + roleId: snippetAccess.roleId, + username: users.username, + roleName: roles.name, + roleDisplayName: roles.displayName, + grantedBy: snippetAccess.grantedBy, + grantedByUsername: sql`(SELECT username FROM users WHERE id = ${snippetAccess.grantedBy})`, + permissionLevel: snippetAccess.permissionLevel, + expiresAt: snippetAccess.expiresAt, + createdAt: snippetAccess.createdAt, + }) + .from(snippetAccess) + .leftJoin(users, eq(snippetAccess.userId, users.id)) + .leftJoin(roles, eq(snippetAccess.roleId, roles.id)) + .where(eq(snippetAccess.snippetId, snippetId)) + .orderBy(desc(snippetAccess.createdAt)); + + const accessList = rawAccessList.map((access) => ({ + id: access.id, + targetType: access.userId ? "user" : "role", + userId: access.userId, + roleId: access.roleId, + username: access.username, + roleName: access.roleName, + roleDisplayName: access.roleDisplayName, + grantedBy: access.grantedBy, + grantedByUsername: access.grantedByUsername, + permissionLevel: access.permissionLevel, + expiresAt: access.expiresAt, + createdAt: access.createdAt, + })); + + res.json({ accessList }); + } catch (error) { + databaseLogger.error("Failed to get snippet access list", error, { + operation: "get_snippet_access_list", + userId, + }); + res.status(500).json({ error: "Failed to get access list" }); + } + }, +); + +/** + * @openapi + * /rbac/shared-snippets: + * get: + * summary: Get shared snippets + * description: Retrieves snippets shared with the current user. + * tags: + * - RBAC + */ +router.get( + "/shared-snippets", + authenticateJWT, + async (req: AuthenticatedRequest, res: Response) => { + const userId = req.userId!; + + try { + const now = new Date().toISOString(); + + const directShared = await db + .select({ + id: snippets.id, + name: snippets.name, + content: snippets.content, + description: snippets.description, + folder: snippets.folder, + ownerUsername: users.username, + permissionLevel: snippetAccess.permissionLevel, + expiresAt: snippetAccess.expiresAt, + }) + .from(snippetAccess) + .innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id)) + .innerJoin(users, eq(snippets.userId, users.id)) + .where( + and( + eq(snippetAccess.userId, userId), + or( + isNull(snippetAccess.expiresAt), + gte(snippetAccess.expiresAt, now), + ), + ), + ); + + const userRoleRows = await db + .select({ roleId: userRoles.roleId }) + .from(userRoles) + .where(eq(userRoles.userId, userId)); + const roleIds = userRoleRows.map((r) => r.roleId); + + let roleShared: typeof directShared = []; + if (roleIds.length > 0) { + const directIds = directShared.map((s) => s.id); + const roleResults = await db + .select({ + id: snippets.id, + name: snippets.name, + content: snippets.content, + description: snippets.description, + folder: snippets.folder, + ownerUsername: users.username, + permissionLevel: snippetAccess.permissionLevel, + expiresAt: snippetAccess.expiresAt, + }) + .from(snippetAccess) + .innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id)) + .innerJoin(users, eq(snippets.userId, users.id)) + .where( + and( + or( + isNull(snippetAccess.expiresAt), + gte(snippetAccess.expiresAt, now), + ), + sql`${snippetAccess.roleId} IN (${sql.join( + roleIds.map((id) => sql`${id}`), + sql`, `, + )})`, + ), + ); + + roleShared = roleResults.filter((s) => !directIds.includes(s.id)); + } + + res.json({ sharedSnippets: [...directShared, ...roleShared] }); + } catch (error) { + databaseLogger.error("Failed to get shared snippets", error, { + operation: "get_shared_snippets", + userId, + }); + res.status(500).json({ error: "Failed to get shared snippets" }); + } + }, +); + export default router; diff --git a/src/backend/database/routes/snippets-reorder.ts b/src/backend/database/routes/snippets-reorder.ts new file mode 100644 index 00000000..6b408a49 --- /dev/null +++ b/src/backend/database/routes/snippets-reorder.ts @@ -0,0 +1,33 @@ +export interface SnippetReorderUpdate { + id: number; + order: number; + folder?: string; +} + +type SnippetReorderRequestBody = { + snippets?: unknown; + updates?: unknown; +}; + +export function extractSnippetReorderUpdates( + body: unknown, +): SnippetReorderUpdate[] | null { + if (!body || typeof body !== "object") { + return null; + } + + const payload = body as SnippetReorderRequestBody; + // Keep accepting the legacy `updates` key so older clients do not break + // while the web and desktop helpers converge on `snippets`. + const snippetsUpdates = Array.isArray(payload.snippets) + ? payload.snippets + : Array.isArray(payload.updates) + ? payload.updates + : null; + + if (!snippetsUpdates) { + return null; + } + + return snippetsUpdates as SnippetReorderUpdate[]; +} diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts index 97d2bf64..a3c2644e 100644 --- a/src/backend/database/routes/snippets.ts +++ b/src/backend/database/routes/snippets.ts @@ -6,6 +6,8 @@ import { eq, and, desc, asc, sql } from "drizzle-orm"; import type { Request, Response } from "express"; import { authLogger, databaseLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; +import { SSH_ALGORITHMS } from "../../utils/ssh-algorithms.js"; +import { extractSnippetReorderUpdates } from "./snippets-reorder.js"; const router = express.Router(); @@ -472,7 +474,8 @@ router.delete( * /snippets/reorder: * put: * summary: Reorder snippets - * description: Bulk updates the order and folder of snippets. + * description: Bulk updates the order and folder of snippets. Accepts + * `snippets` and the legacy `updates` payload key. * tags: * - Snippets * requestBody: @@ -507,14 +510,14 @@ router.put( requireDataAccess, async (req: Request, res: Response) => { const userId = (req as AuthenticatedRequest).userId; - const { snippets: snippetUpdates } = req.body; + const snippetUpdates = extractSnippetReorderUpdates(req.body); if (!isNonEmptyString(userId)) { authLogger.warn("Invalid userId for snippet reorder"); return res.status(400).json({ error: "Invalid userId" }); } - if (!Array.isArray(snippetUpdates) || snippetUpdates.length === 0) { + if (!snippetUpdates || snippetUpdates.length === 0) { authLogger.warn("Invalid snippet reorder data", { operation: "snippet_reorder", userId, @@ -775,18 +778,7 @@ router.post( "ssh-rsa", "ssh-dss", ], - cipher: [ - "chacha20-poly1305@openssh.com", - "aes256-gcm@openssh.com", - "aes128-gcm@openssh.com", - "aes256-ctr", - "aes192-ctr", - "aes128-ctr", - "aes256-cbc", - "aes192-cbc", - "aes128-cbc", - "3des-cbc", - ], + cipher: SSH_ALGORITHMS.cipher, hmac: [ "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", diff --git a/src/backend/database/routes/terminal.ts b/src/backend/database/routes/terminal.ts index 6b35107e..44088336 100644 --- a/src/backend/database/routes/terminal.ts +++ b/src/backend/database/routes/terminal.ts @@ -62,11 +62,37 @@ router.post( return res.status(400).json({ error: "Missing required parameters" }); } + const sensitivePatterns = [ + /passw(or)?d/i, + /\bsecret\b/i, + /\btoken\b/i, + /\bapi.?key\b/i, + /PASS(WORD)?=/i, + /AWS_SECRET/i, + /mysql\b.*-p/i, + /sudo\s+-S\b/, + /htpasswd/i, + /sshpass/i, + /curl\b.*-u\s/i, + /export\b.*(?:PASSWORD|SECRET|TOKEN|KEY)=/i, + ]; + + const trimmedCommand = command.trim(); + if (sensitivePatterns.some((p: RegExp) => p.test(trimmedCommand))) { + return res.status(201).json({ + id: 0, + userId, + hostId: parseInt(hostId, 10), + command: trimmedCommand, + executedAt: new Date().toISOString(), + }); + } + try { const insertData = { userId, hostId: parseInt(hostId, 10), - command: command.trim(), + command: trimmedCommand, }; const result = await db diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index f1282b0f..7d81cb0b 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1,11 +1,13 @@ import type { AuthenticatedRequest } from "../../../types/index.js"; import express from "express"; import { restartGuacServer } from "../../guacamole/guacamole-server.js"; +import { setGlobalLogLevel, getGlobalLogLevel } from "../../utils/logger.js"; import crypto from "crypto"; import { db } from "../db/index.js"; import { users, sessions, + trustedDevices, hosts, sshCredentials, fileManagerRecent, @@ -734,7 +736,8 @@ router.get("/oidc-config/admin", requireAdmin, async (req, res) => { .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") .get(); if (!row) { - return res.json(null); + const envConfig = getOIDCConfigFromEnv(); + return res.json(envConfig); } let config = JSON.parse((row as Record).value as string); @@ -1226,7 +1229,7 @@ router.get("/oidc/callback", async (req, res) => { const sessionDurationMs = deviceInfo.type === "desktop" || deviceInfo.type === "mobile" ? 30 * 24 * 60 * 60 * 1000 - : 2 * 60 * 60 * 1000; + : 24 * 60 * 60 * 1000; await authManager.registerOIDCUser(id, sessionDurationMs); } catch (encryptionError) { await db.delete(users).where(eq(users.id, id)); @@ -1318,12 +1321,16 @@ router.get("/oidc/callback", async (req, res) => { const redirectUrl = new URL(frontendOrigin); redirectUrl.searchParams.set("success", "true"); + if (deviceInfo.type === "desktop" || deviceInfo.type === "mobile") { + redirectUrl.searchParams.set("token", token); + } + const maxAge = deviceInfo.type === "desktop" || deviceInfo.type === "mobile" ? 30 * 24 * 60 * 60 * 1000 : storedRememberMe ? 30 * 24 * 60 * 60 * 1000 - : 2 * 60 * 60 * 1000; + : 24 * 60 * 60 * 1000; res.clearCookie("jwt", authManager.getClearCookieOptions(req)); @@ -1577,7 +1584,13 @@ router.post("/login", async (req, res) => { response.token = token; } - const maxAge = rememberMe ? 30 * 24 * 60 * 60 * 1000 : 2 * 60 * 60 * 1000; + const timeoutRow = db.$client + .prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'") + .get() as { value: string } | undefined; + const timeoutHours = timeoutRow ? parseInt(timeoutRow.value, 10) || 24 : 24; + const maxAge = rememberMe + ? 30 * 24 * 60 * 60 * 1000 + : timeoutHours * 60 * 60 * 1000; return res .cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge)) @@ -2155,12 +2168,16 @@ router.post("/initiate-reset", async (req, res) => { authLogger.warn( `Password reset attempted for non-existent user: ${username}`, ); - return res.status(404).json({ error: "User not found" }); + return res.json({ + message: + "If the user exists, a password reset code has been generated. Check docker logs for the code.", + }); } if (user[0].isOidc) { - return res.status(403).json({ - error: "Password reset not available for external authentication users", + return res.json({ + message: + "If the user exists, a password reset code has been generated. Check docker logs for the code.", }); } @@ -2175,7 +2192,7 @@ router.post("/initiate-reset", async (req, res) => { ); authLogger.info( - `Password reset code for user ${username}: ${resetCode} (expires at ${expiresAt.toLocaleString()})`, + `Password reset code generated for user ${username} (expires at ${expiresAt.toLocaleString()}). Check admin panel or database settings table for code.`, ); res.json({ @@ -2640,13 +2657,7 @@ router.post("/change-password", authenticateJWT, async (req, res) => { * description: Failed to list users. */ router.get("/list", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0 || !user[0].isAdmin) { - return res.status(403).json({ error: "Not authorized" }); - } - const allUsers = await db .select({ id: users.id, @@ -2679,13 +2690,17 @@ router.get("/list", authenticateJWT, async (req, res) => { * schema: * type: object * properties: + * userId: + * type: string + * description: Preferred unique user identifier. * username: * type: string + * description: Legacy fallback identifier. * responses: * 200: * description: User is now an admin. * 400: - * description: Username is required or user is already an admin. + * description: User ID or username is required, or the user is already an admin. * 403: * description: Not authorized. * 404: @@ -2695,10 +2710,14 @@ router.get("/list", authenticateJWT, async (req, res) => { */ router.post("/make-admin", authenticateJWT, async (req, res) => { const userId = (req as AuthenticatedRequest).userId; - const { username } = req.body; + const { userId: targetUserId, username } = req.body; + const resolvedUserId = isNonEmptyString(targetUserId) + ? targetUserId.trim() + : null; + const resolvedUsername = isNonEmptyString(username) ? username.trim() : null; - if (!isNonEmptyString(username)) { - return res.status(400).json({ error: "Username is required" }); + if (!resolvedUserId && !resolvedUsername) { + return res.status(400).json({ error: "User ID or username is required" }); } try { @@ -2710,7 +2729,12 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { const targetUser = await db .select() .from(users) - .where(eq(users.username, username)); + .where( + resolvedUserId + ? eq(users.id, resolvedUserId) + : eq(users.username, resolvedUsername!), + ) + .limit(1); if (!targetUser || targetUser.length === 0) { return res.status(404).json({ error: "User not found" }); } @@ -2722,7 +2746,11 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { await db .update(users) .set({ isAdmin: true }) - .where(eq(users.username, username)); + .where( + resolvedUserId + ? eq(users.id, resolvedUserId) + : eq(users.username, resolvedUsername!), + ); try { const { saveMemoryDatabaseToFile } = await import("../db/index.js"); @@ -2730,7 +2758,8 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { } catch (saveError) { authLogger.error("Failed to persist admin promotion to disk", saveError, { operation: "make_admin_save_failed", - username, + userId: targetUser[0].id, + username: targetUser[0].username, }); } @@ -2738,9 +2767,9 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { operation: "admin_grant", adminId: userId, targetUserId: targetUser[0].id, - targetUsername: username, + targetUsername: targetUser[0].username, }); - res.json({ message: `User ${username} is now an admin` }); + res.json({ message: `User ${targetUser[0].username} is now an admin` }); } catch (err) { authLogger.error("Failed to make user admin", err); res.status(500).json({ error: "Failed to make user admin" }); @@ -2762,13 +2791,17 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { * schema: * type: object * properties: + * userId: + * type: string + * description: Preferred unique user identifier. * username: * type: string + * description: Legacy fallback identifier. * responses: * 200: * description: Admin status removed from user. * 400: - * description: Username is required or cannot remove your own admin status. + * description: User ID or username is required, or cannot remove your own admin status. * 403: * description: Not authorized. * 404: @@ -2778,10 +2811,14 @@ router.post("/make-admin", authenticateJWT, async (req, res) => { */ router.post("/remove-admin", authenticateJWT, async (req, res) => { const userId = (req as AuthenticatedRequest).userId; - const { username } = req.body; + const { userId: targetUserId, username } = req.body; + const resolvedUserId = isNonEmptyString(targetUserId) + ? targetUserId.trim() + : null; + const resolvedUsername = isNonEmptyString(username) ? username.trim() : null; - if (!isNonEmptyString(username)) { - return res.status(400).json({ error: "Username is required" }); + if (!resolvedUserId && !resolvedUsername) { + return res.status(400).json({ error: "User ID or username is required" }); } try { @@ -2790,7 +2827,10 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => { return res.status(403).json({ error: "Not authorized" }); } - if (adminUser[0].username === username) { + if ( + (resolvedUserId && adminUser[0].id === resolvedUserId) || + (resolvedUsername && adminUser[0].username === resolvedUsername) + ) { return res .status(400) .json({ error: "Cannot remove your own admin status" }); @@ -2799,7 +2839,12 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => { const targetUser = await db .select() .from(users) - .where(eq(users.username, username)); + .where( + resolvedUserId + ? eq(users.id, resolvedUserId) + : eq(users.username, resolvedUsername!), + ) + .limit(1); if (!targetUser || targetUser.length === 0) { return res.status(404).json({ error: "User not found" }); } @@ -2811,7 +2856,11 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => { await db .update(users) .set({ isAdmin: false }) - .where(eq(users.username, username)); + .where( + resolvedUserId + ? eq(users.id, resolvedUserId) + : eq(users.username, resolvedUsername!), + ); try { const { saveMemoryDatabaseToFile } = await import("../db/index.js"); @@ -2819,7 +2868,8 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => { } catch (saveError) { authLogger.error("Failed to persist admin removal to disk", saveError, { operation: "remove_admin_save_failed", - username, + userId: targetUser[0].id, + username: targetUser[0].username, }); } @@ -2827,9 +2877,11 @@ router.post("/remove-admin", authenticateJWT, async (req, res) => { operation: "admin_revoke", adminId: userId, targetUserId: targetUser[0].id, - targetUsername: username, + targetUsername: targetUser[0].username, + }); + res.json({ + message: `Admin status removed from ${targetUser[0].username}`, }); - res.json({ message: `Admin status removed from ${username}` }); } catch (err) { authLogger.error("Failed to remove admin status", err); res.status(500).json({ error: "Failed to remove admin status" }); @@ -2966,10 +3018,19 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => { totpBackupCodes: JSON.stringify(backupCodes), }) .where(eq(users.id, userId)); - authLogger.info("Two-factor authentication enabled", { - operation: "totp_enable", - userId, - }); + + await db.delete(sessions).where(eq(sessions.userId, userId)); + await db.delete(trustedDevices).where(eq(trustedDevices.userId, userId)); + + try { + const { saveMemoryDatabaseToFile } = await import("../db/index.js"); + await saveMemoryDatabaseToFile(); + } catch (saveError) { + authLogger.error("Failed to persist TOTP enablement to disk", saveError, { + operation: "totp_enable_db_save_failed", + userId, + }); + } res.json({ message: "TOTP enabled successfully", @@ -3359,7 +3420,13 @@ router.post("/totp/verify-login", async (req, res) => { response.token = token; } - const maxAge = rememberMe ? 30 * 24 * 60 * 60 * 1000 : 2 * 60 * 60 * 1000; + const timeoutRow = db.$client + .prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'") + .get() as { value: string } | undefined; + const timeoutHours = timeoutRow ? parseInt(timeoutRow.value, 10) || 24 : 24; + const maxAge = rememberMe + ? 30 * 24 * 60 * 60 * 1000 + : timeoutHours * 60 * 60 * 1000; return res .cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge)) @@ -4226,4 +4293,144 @@ router.patch("/guacamole-settings", authenticateJWT, async (req, res) => { } }); +/** + * @openapi + * /users/log-level: + * get: + * summary: Get log level setting + * description: Returns the configured log verbosity level. + * tags: + * - Users + * responses: + * 200: + * description: Current log level. + */ +router.get("/log-level", async (_req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'log_level'") + .get() as { value: string } | undefined; + res.json({ + level: row ? row.value : getGlobalLogLevel(), + }); + } catch (err) { + authLogger.error("Failed to get log level", err); + res.status(500).json({ error: "Failed to get log level" }); + } +}); + +/** + * @openapi + * /users/log-level: + * patch: + * summary: Update log level setting (admin only) + * description: Sets the log verbosity level. + * tags: + * - Users + * responses: + * 200: + * description: Log level updated. + * 400: + * description: Invalid log level. + * 403: + * description: Not authorized. + */ +router.patch("/log-level", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].isAdmin) { + return res.status(403).json({ error: "Not authorized" }); + } + const { level } = req.body; + const validLevels = ["debug", "info", "warn", "error"]; + if (typeof level !== "string" || !validLevels.includes(level)) { + return res + .status(400) + .json({ error: "level must be one of: debug, info, warn, error" }); + } + db.$client + .prepare( + "INSERT OR REPLACE INTO settings (key, value) VALUES ('log_level', ?)", + ) + .run(level); + setGlobalLogLevel(level); + res.json({ level }); + } catch (err) { + authLogger.error("Failed to set log level", err); + res.status(500).json({ error: "Failed to set log level" }); + } +}); + +/** + * @openapi + * /users/session-timeout: + * get: + * summary: Get session timeout setting + * description: Returns the configured session timeout in hours. + * tags: + * - Users + * responses: + * 200: + * description: Current session timeout hours. + */ +router.get("/session-timeout", async (_req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'") + .get() as { value: string } | undefined; + res.json({ + timeoutHours: row ? parseInt(row.value, 10) : 24, + }); + } catch (err) { + authLogger.error("Failed to get session timeout", err); + res.status(500).json({ error: "Failed to get session timeout" }); + } +}); + +/** + * @openapi + * /users/session-timeout: + * patch: + * summary: Update session timeout setting (admin only) + * description: Sets the session timeout in hours. + * tags: + * - Users + * responses: + * 200: + * description: Session timeout updated. + * 400: + * description: Invalid value. + * 403: + * description: Not authorized. + */ +router.patch("/session-timeout", authenticateJWT, async (req, res) => { + const userId = (req as AuthenticatedRequest).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].isAdmin) { + return res.status(403).json({ error: "Not authorized" }); + } + const { timeoutHours } = req.body; + if ( + typeof timeoutHours !== "number" || + timeoutHours < 1 || + timeoutHours > 720 + ) { + return res + .status(400) + .json({ error: "timeoutHours must be between 1 and 720" }); + } + db.$client + .prepare( + "INSERT OR REPLACE INTO settings (key, value) VALUES ('session_timeout_hours', ?)", + ) + .run(String(timeoutHours)); + res.json({ timeoutHours }); + } catch (err) { + authLogger.error("Failed to set session timeout", err); + res.status(500).json({ error: "Failed to set session timeout" }); + } +}); + export default router; diff --git a/src/backend/guacamole/guacamole-server.ts b/src/backend/guacamole/guacamole-server.ts index 6dae1a72..786aa377 100644 --- a/src/backend/guacamole/guacamole-server.ts +++ b/src/backend/guacamole/guacamole-server.ts @@ -58,7 +58,7 @@ const clientOptions = { }, allowedUnencryptedConnectionSettings: { rdp: ["width", "height", "dpi"], - vnc: ["width", "height", "dpi"], + vnc: ["width", "height"], telnet: ["width", "height"], }, connectionDefaultSettings: { @@ -74,6 +74,7 @@ const clientOptions = { width: 1280, height: 720, dpi: 96, + audio: ["audio/L16"], }, vnc: { "swap-red-blue": false, diff --git a/src/backend/ssh/docker-console.ts b/src/backend/ssh/docker-console.ts index c7cc82b1..1f8fb882 100644 --- a/src/backend/ssh/docker-console.ts +++ b/src/backend/ssh/docker-console.ts @@ -28,7 +28,15 @@ const wss = new WebSocketServer({ verifyClient: async (info) => { try { const url = parseUrl(info.req.url || "", true); - const token = url.query.token as string; + let token = url.query.token as string; + + if (!token) { + const cookieHeader = info.req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } + } if (!token) { return false; @@ -75,8 +83,10 @@ async function detectShell( } }); - stream.stderr.on("data", () => { - // Ignore stderr + stream.stderr.on("data", () => {}); + stream.stderr.on("error", () => {}); + stream.on("error", (streamErr) => { + reject(streamErr); }); }, ); @@ -229,7 +239,30 @@ async function createJumpHostChain( } wss.on("connection", async (ws: WebSocket, req) => { - const userId = (req as unknown as { userId: string }).userId; + const url = parseUrl(req.url || "", true); + let token = url.query.token as string; + + if (!token) { + const cookieHeader = req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } + } + + if (!token) { + ws.close(1008, "Authentication required"); + return; + } + + const authManagerInstance = AuthManager.getInstance(); + const payload = await authManagerInstance.verifyJWTToken(token); + if (!payload || !payload.userId) { + ws.close(1008, "Authentication required"); + return; + } + + const userId = payload.userId; const sessionId = `docker-console-${Date.now()}-${Math.random()}`; sshLogger.info("Docker console WebSocket connected", { operation: "docker_console_connect", @@ -260,21 +293,9 @@ wss.on("connection", async (ws: WebSocket, req) => { rows?: number; }; - if ( - typeof hostConfig.jumpHosts === "string" && - hostConfig.jumpHosts - ) { - try { - hostConfig.jumpHosts = JSON.parse(hostConfig.jumpHosts); - } catch (e) { - sshLogger.error("Failed to parse jump hosts", e, { - hostId: hostConfig.id, - }); - hostConfig.jumpHosts = []; - } - } + const hostId = hostConfig?.id; - if (!hostConfig || !containerId) { + if (!hostId || !containerId) { ws.send( JSON.stringify({ type: "error", @@ -284,62 +305,69 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } + if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(containerId)) { + ws.send( + JSON.stringify({ + type: "error", + message: "Invalid container ID", + }), + ); + return; + } + + const allowedShells = ["bash", "sh", "ash", "zsh"]; + if (shell && !allowedShells.includes(shell)) { + ws.send( + JSON.stringify({ + type: "error", + message: "Invalid shell", + }), + ); + return; + } + if (!hostConfig.enableDocker) { ws.send( JSON.stringify({ type: "error", - message: - "Docker is not enabled for this host. Enable it in Host Settings.", + message: "Docker is not enabled on this host", }), ); return; } try { - let resolvedCredentials: { - password?: string; - sshKey?: string; - keyPassword?: string; - authType?: string; - } = { - password: hostConfig.password, - sshKey: hostConfig.key, - keyPassword: hostConfig.keyPassword, - authType: hostConfig.authType, - }; + // Resolve host with credentials server-side + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById(hostId, userId); - if (hostConfig.credentialId) { - const credentials = await SimpleDBOps.select( - getDb() - .select() - .from(sshCredentials) - .where( - and( - eq(sshCredentials.id, hostConfig.credentialId as number), - eq(sshCredentials.userId, userId), - ), - ), - "ssh_credentials", - userId, + if (!resolvedHost) { + ws.send( + JSON.stringify({ + type: "error", + message: "Host not found", + }), ); + return; + } - if (credentials.length > 0) { - const credential = credentials[0]; - resolvedCredentials = { - password: credential.password as string | undefined, - sshKey: credential.privateKey as string | undefined, - keyPassword: credential.keyPassword as string | undefined, - authType: credential.authType as string | undefined, - }; - } + if (!resolvedHost.enableDocker) { + ws.send( + JSON.stringify({ + type: "error", + message: + "Docker is not enabled for this host. Enable it in Host Settings.", + }), + ); + return; } const client = new SSHClient(); const config: Record = { - host: hostConfig.ip?.replace(/^\[|\]$/g, "") || hostConfig.ip, - port: hostConfig.port || 22, - username: hostConfig.username, + host: resolvedHost.ip?.replace(/^\[|\]$/g, "") || resolvedHost.ip, + port: resolvedHost.port || 22, + username: resolvedHost.username, tryKeyboard: true, readyTimeout: 60000, keepaliveInterval: 30000, @@ -348,28 +376,22 @@ wss.on("connection", async (ws: WebSocket, req) => { tcpKeepAliveInitialDelay: 30000, }; - if ( - resolvedCredentials.authType === "password" && - resolvedCredentials.password - ) { - config.password = resolvedCredentials.password; - } else if ( - resolvedCredentials.authType === "key" && - resolvedCredentials.sshKey - ) { - const cleanKey = resolvedCredentials.sshKey + if (resolvedHost.authType === "password" && resolvedHost.password) { + config.password = resolvedHost.password; + } else if (resolvedHost.authType === "key" && resolvedHost.key) { + const cleanKey = resolvedHost.key .trim() .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); config.privateKey = Buffer.from(cleanKey, "utf8"); - if (resolvedCredentials.keyPassword) { - config.passphrase = resolvedCredentials.keyPassword; + if (resolvedHost.keyPassword) { + config.passphrase = resolvedHost.keyPassword; } } - if (hostConfig.jumpHosts && hostConfig.jumpHosts.length > 0) { + if (resolvedHost.jumpHosts && resolvedHost.jumpHosts.length > 0) { const jumpClient = await createJumpHostChain( - hostConfig.jumpHosts, + resolvedHost.jumpHosts, userId, ); if (jumpClient) { @@ -378,8 +400,8 @@ wss.on("connection", async (ws: WebSocket, req) => { jumpClient.forwardOut( "127.0.0.1", 0, - hostConfig.ip, - hostConfig.port || 22, + resolvedHost.ip, + resolvedHost.port || 22, (err, stream) => { if (err) return reject(err); resolve(stream); @@ -402,7 +424,7 @@ wss.on("connection", async (ws: WebSocket, req) => { stream: null, isConnected: true, containerId, - hostId: hostConfig.id, + hostId: resolvedHost.id, }; activeSessions.set(sessionId, sshSession); @@ -430,8 +452,10 @@ wss.on("connection", async (ws: WebSocket, req) => { } }); - stream.stderr.on("data", () => { - // Ignore stderr + stream.stderr.on("data", () => {}); + stream.stderr.on("error", () => {}); + stream.on("error", (streamErr) => { + reject(streamErr); }); }, ); @@ -459,7 +483,7 @@ wss.on("connection", async (ws: WebSocket, req) => { operation: "docker_attach", sessionId, userId, - hostId: hostConfig.id, + hostId: resolvedHost.id, containerId, }); @@ -494,7 +518,7 @@ wss.on("connection", async (ws: WebSocket, req) => { operation: "docker_attach_success", sessionId, userId, - hostId: hostConfig.id, + hostId: resolvedHost.id, containerId, }); @@ -509,8 +533,23 @@ wss.on("connection", async (ws: WebSocket, req) => { } }); - stream.stderr.on("data", () => { - // stderr output ignored + stream.stderr.on("data", () => {}); + stream.stderr.on("error", () => {}); + + stream.on("error", (streamErr) => { + sshLogger.error("Docker console stream error", streamErr, { + operation: "docker_console_stream_error", + sessionId, + containerId, + }); + if (ws.readyState === WebSocket.OPEN) { + ws.send( + JSON.stringify({ + type: "error", + message: `Console error: ${streamErr.message}`, + }), + ); + } }); stream.on("close", () => { diff --git a/src/backend/ssh/docker.ts b/src/backend/ssh/docker.ts index 9c3e08f3..79bbf0ba 100644 --- a/src/backend/ssh/docker.ts +++ b/src/backend/ssh/docker.ts @@ -1,5 +1,5 @@ import express from "express"; -import cors from "cors"; +import { createCorsMiddleware } from "../utils/cors-config.js"; import cookieParser from "cookie-parser"; import axios from "axios"; import { Client as SSHClient } from "ssh2"; @@ -9,6 +9,7 @@ import { eq, and } from "drizzle-orm"; import { logger } from "../utils/logger.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { AuthManager } from "../utils/auth-manager.js"; +import type { AuthenticatedRequest } from "../../types/index.js"; import { createSocks5Connection, type SOCKS5Config, @@ -40,6 +41,7 @@ interface SSHSession { timeout?: NodeJS.Timeout; activeOperations: number; hostId?: number; + userId?: string; } interface PendingTOTPSession { @@ -424,39 +426,7 @@ async function executeDockerCommand( const app = express(); -app.use( - cors({ - origin: (origin, callback) => { - if (!origin) { - return callback(null, true); - } - - if (origin.startsWith("https://")) { - return callback(null, true); - } - - if (origin.startsWith("http://")) { - return callback(null, true); - } - - const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - return callback(new Error("Not allowed by CORS")); - }, - credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: [ - "Content-Type", - "Authorization", - "User-Agent", - "X-Electron-App", - ], - }), -); +app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"])); app.use(cookieParser()); app.use(express.json({ limit: "100mb" })); @@ -469,6 +439,16 @@ app.use((_req, res, next) => { const authManager = AuthManager.getInstance(); app.use(authManager.createAuthMiddleware()); +const CONTAINER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/; +const DOCKER_TIMESTAMP_RE = /^[0-9T:.Z+-]+$/; + +app.param("containerId", (req, res, next, value) => { + if (!CONTAINER_ID_RE.test(value)) { + return res.status(400).json({ error: "Invalid container ID" }); + } + next(); +}); + /** * @openapi * /docker/ssh/connect: @@ -791,18 +771,13 @@ app.post("/docker/ssh/connect", async (req, res) => { }); } - const { promises: fs } = await import("fs"); - const path = await import("path"); - const os = await import("os"); - - const tempDir = os.tmpdir(); - const keyPath = path.join(tempDir, `opkssh-docker-${userId}-${hostId}`); - const certPath = `${keyPath}-cert.pub`; - - await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 }); - await fs.writeFile(certPath, token.sshCert, { mode: 0o600 }); - - config.privateKey = await fs.readFile(keyPath); + const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js"); + await setupOPKSSHCertAuth( + config as import("ssh2").ConnectConfig, + client, + token, + host.username, + ); connectionLogs.push( createConnectionLog( "info", @@ -810,32 +785,6 @@ app.post("/docker/ssh/connect", async (req, res) => { "Using OPKSSH certificate authentication", ), ); - - setTimeout(async () => { - try { - const cleanupResults = await Promise.allSettled([ - fs.unlink(keyPath), - fs.unlink(certPath), - ]); - - cleanupResults.forEach((result, index) => { - if (result.status === "rejected") { - sshLogger.warn(`Failed to cleanup OPKSSH temp file`, { - operation: "opkssh_temp_cleanup_failed", - file: index === 0 ? "keyPath" : "certPath", - sessionId, - error: result.reason, - }); - } - }); - } catch (error) { - sshLogger.error("Failed to cleanup OPKSSH temp files", { - operation: "opkssh_temp_cleanup_error", - sessionId, - error, - }); - } - }, 60000); } catch (opksshError) { sshLogger.error("OPKSSH authentication error for Docker", { operation: "docker_connect", @@ -983,6 +932,7 @@ app.post("/docker/ssh/connect", async (req, res) => { lastActive: Date.now(), activeOperations: 0, hostId, + userId, }; scheduleSessionCleanup(sessionId); @@ -1665,6 +1615,7 @@ app.post("/docker/ssh/connect-totp", async (req, res) => { lastActive: Date.now(), activeOperations: 0, hostId: session.hostId, + userId, }; scheduleSessionCleanup(sessionId); @@ -1850,6 +1801,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { lastActive: Date.now(), activeOperations: 0, hostId: session.hostId, + userId, }; scheduleSessionCleanup(sessionId); @@ -1953,6 +1905,7 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => { */ app.post("/docker/ssh/keepalive", async (req, res) => { const { sessionId } = req.body; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -1967,6 +1920,10 @@ app.post("/docker/ssh/keepalive", async (req, res) => { }); } + if (session.userId && session.userId !== userId) { + return res.status(403).json({ error: "Session access denied" }); + } + session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); @@ -3016,18 +2973,18 @@ app.get("/docker/containers/:sessionId/:containerId/logs", async (req, res) => { let command = `docker logs ${containerId}`; if (tail && tail > 0) { - command += ` --tail ${tail}`; + command += ` --tail ${Math.floor(tail)}`; } if (timestamps) { command += " --timestamps"; } - if (since) { + if (since && DOCKER_TIMESTAMP_RE.test(since)) { command += ` --since ${since}`; } - if (until) { + if (until && DOCKER_TIMESTAMP_RE.test(until)) { command += ` --until ${until}`; } diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index df4f779b..a3f85323 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -1,8 +1,9 @@ import express from "express"; -import cors from "cors"; +import { createCorsMiddleware } from "../utils/cors-config.js"; import cookieParser from "cookie-parser"; import axios from "axios"; import { Client as SSHClient } from "ssh2"; +import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { getDb } from "../database/db/index.js"; import { sshCredentials, hosts } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; @@ -117,37 +118,7 @@ function formatMtime(mtime: number): string { const app = express(); -app.use( - cors({ - origin: (origin, callback) => { - if (!origin) return callback(null, true); - - const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; - - if (origin.startsWith("https://")) { - return callback(null, true); - } - - if (origin.startsWith("http://")) { - return callback(null, true); - } - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - callback(new Error("Not allowed by CORS")); - }, - credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: [ - "Content-Type", - "Authorization", - "User-Agent", - "X-Electron-App", - ], - }), -); +app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"])); app.use(cookieParser()); app.use(express.json({ limit: "1gb" })); app.use(express.urlencoded({ limit: "1gb", extended: true })); @@ -399,6 +370,7 @@ interface SSHSession { sudoPassword?: string; sftp?: import("ssh2").SFTPWrapper; poolKey?: string; + userId?: string; } interface PendingTOTPSession { @@ -531,6 +503,10 @@ function scheduleSessionCleanup(sessionId: string) { } } +function verifySessionOwnership(session: SSHSession, userId: string): boolean { + return !session.userId || session.userId === userId; +} + function getMimeType(fileName: string): string { const ext = fileName.split(".").pop()?.toLowerCase(); const mimeTypes: Record = { @@ -801,151 +777,62 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { ), ); + // Resolve credentials server-side when frontend doesn't provide them let resolvedCredentials = { password, sshKey, keyPassword, authType }; - if (credentialId && hostId && userId) { - const hostRow = await getDb() - .select({ userId: hosts.userId }) - .from(hosts) - .where(eq(hosts.id, hostId)) - .limit(1); - const ownerId = hostRow[0]?.userId ?? null; - - if (ownerId && userId !== ownerId) { - try { - const { SharedCredentialManager } = - await import("../utils/shared-credential-manager.js"); - const sharedCredManager = SharedCredentialManager.getInstance(); - const sharedCred = await sharedCredManager.getSharedCredentialForUser( - hostId, - userId, - ); - - if (sharedCred) { - resolvedCredentials = { - password: sharedCred.password, - sshKey: sharedCred.key, - keyPassword: sharedCred.keyPassword, - authType: sharedCred.authType, - }; - connectionLogs.push( - createConnectionLog( - "info", - "sftp_auth", - "Credentials resolved from shared credential store", - ), - ); - } else { - fileLogger.warn(`No shared credentials found for host ${hostId}`, { - operation: "ssh_credentials", - hostId, - userId, - }); - connectionLogs.push( - createConnectionLog( - "warning", - "sftp_auth", - "No shared credentials found, using provided credentials", - ), - ); - } - } catch (error) { - fileLogger.warn( - `Failed to resolve shared credential for host ${hostId}`, - { - operation: "ssh_credentials", - hostId, - error: error instanceof Error ? error.message : "Unknown error", - }, - ); + if (hostId && userId && !password && !sshKey) { + try { + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById(hostId, userId); + if (resolvedHost) { + resolvedCredentials = { + password: resolvedHost.password, + sshKey: resolvedHost.key, + keyPassword: resolvedHost.keyPassword, + authType: resolvedHost.authType, + }; connectionLogs.push( createConnectionLog( - "warning", + "info", "sftp_auth", - `Failed to resolve shared credentials: ${error instanceof Error ? error.message : "Unknown error"}`, + "Credentials resolved from server-side host data", ), ); } - } else if (ownerId) { - try { - const credentials = await SimpleDBOps.select( - getDb() - .select() - .from(sshCredentials) - .where( - and( - eq(sshCredentials.id, credentialId), - eq(sshCredentials.userId, ownerId), - ), - ), - "ssh_credentials", - ownerId, - ); - - if (credentials.length > 0) { - const credential = credentials[0]; - resolvedCredentials = { - password: credential.password, - sshKey: credential.privateKey, - keyPassword: credential.keyPassword, - authType: credential.authType, - }; - connectionLogs.push( - createConnectionLog( - "info", - "sftp_auth", - "Credentials resolved from credential store", - ), - ); - } else { - fileLogger.warn(`No credentials found for host ${hostId}`, { - operation: "ssh_credentials", - hostId, - credentialId, - userId: ownerId, - }); - connectionLogs.push( - createConnectionLog( - "warning", - "sftp_auth", - "No stored credentials found, using provided credentials", - ), - ); - } - } catch (error) { - fileLogger.warn(`Failed to resolve credentials for host ${hostId}`, { - operation: "ssh_credentials", - hostId, - credentialId, - error: error instanceof Error ? error.message : "Unknown error", - }); - connectionLogs.push( - createConnectionLog( - "warning", - "sftp_auth", - `Failed to resolve credentials: ${error instanceof Error ? error.message : "Unknown error"}`, - ), - ); - } - } else { - fileLogger.warn( - "Missing userId for credential resolution in file manager", - { - operation: "ssh_credentials", - hostId, - credentialId, - }, - ); + } catch (error) { + fileLogger.warn(`Failed to resolve host credentials for ${hostId}`, { + operation: "ssh_credentials", + hostId, + error: error instanceof Error ? error.message : "Unknown error", + }); } - } else if (credentialId && hostId) { - fileLogger.warn( - "Missing userId for credential resolution in file manager", - { + } else if (credentialId && hostId && userId) { + // Legacy: credential resolution from credentialId + try { + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById(hostId, userId); + if (resolvedHost) { + resolvedCredentials = { + password: resolvedHost.password, + sshKey: resolvedHost.key, + keyPassword: resolvedHost.keyPassword, + authType: resolvedHost.authType, + }; + connectionLogs.push( + createConnectionLog( + "info", + "sftp_auth", + "Credentials resolved from credential store", + ), + ); + } + } catch (error) { + fileLogger.warn(`Failed to resolve credentials for host ${hostId}`, { operation: "ssh_credentials", hostId, credentialId, - hasUserId: !!userId, - }, - ); + error: error instanceof Error ? error.message : "Unknown error", + }); + } } const config: Record = { @@ -1001,18 +888,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { "ssh-rsa", "ssh-dss", ], - cipher: [ - "chacha20-poly1305@openssh.com", - "aes256-gcm@openssh.com", - "aes128-gcm@openssh.com", - "aes256-ctr", - "aes192-ctr", - "aes128-ctr", - "aes256-cbc", - "aes192-cbc", - "aes128-cbc", - "3des-cbc", - ], + cipher: SSH_ALGORITHMS.cipher, hmac: [ "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", @@ -1112,18 +988,13 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { }); } - const { promises: fs } = await import("fs"); - const path = await import("path"); - const os = await import("os"); - - const tempDir = os.tmpdir(); - const keyPath = path.join(tempDir, `opkssh-fm-${userId}-${hostId}`); - const certPath = `${keyPath}-cert.pub`; - - await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 }); - await fs.writeFile(certPath, token.sshCert, { mode: 0o600 }); - - config.privateKey = await fs.readFile(keyPath); + const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js"); + await setupOPKSSHCertAuth( + config as import("ssh2").ConnectConfig, + client, + token, + username, + ); connectionLogs.push( createConnectionLog( "info", @@ -1131,32 +1002,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { "Using OPKSSH certificate authentication", ), ); - - setTimeout(async () => { - try { - const cleanupResults = await Promise.allSettled([ - fs.unlink(keyPath), - fs.unlink(certPath), - ]); - - cleanupResults.forEach((result, index) => { - if (result.status === "rejected") { - fileLogger.warn(`Failed to cleanup OPKSSH temp file`, { - operation: "opkssh_temp_cleanup_failed", - file: index === 0 ? "keyPath" : "certPath", - sessionId, - error: result.reason, - }); - } - }); - } catch (error) { - fileLogger.error("Failed to cleanup OPKSSH temp files", { - operation: "opkssh_temp_cleanup_error", - sessionId, - error, - }); - } - }, 60000); } catch (opksshError) { fileLogger.error("OPKSSH authentication error for file manager", { operation: "file_connect", @@ -1263,6 +1108,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { isConnected: true, lastActive: Date.now(), activeOperations: 0, + userId, }; scheduleSessionCleanup(sessionId); res.json({ @@ -1906,6 +1752,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => { isConnected: true, lastActive: Date.now(), activeOperations: 0, + userId, }; scheduleSessionCleanup(sessionId); @@ -2107,6 +1954,7 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => { isConnected: true, lastActive: Date.now(), activeOperations: 0, + userId, }; scheduleSessionCleanup(sessionId); @@ -2236,10 +2084,14 @@ app.post("/ssh/file_manager/ssh/disconnect", (req, res) => { */ app.post("/ssh/file_manager/sudo-password", (req, res) => { const { sessionId, password } = req.body; + const userId = (req as AuthenticatedRequest).userId; const session = sshSessions[sessionId]; if (!session || !session.isConnected) { return res.status(400).json({ error: "Invalid or disconnected session" }); } + if (!verifySessionOwnership(session, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } session.sudoPassword = password; session.lastActive = Date.now(); res.json({ status: "success", message: "Sudo password set" }); @@ -2265,7 +2117,12 @@ app.post("/ssh/file_manager/sudo-password", (req, res) => { */ app.get("/ssh/file_manager/ssh/status", (req, res) => { const sessionId = req.query.sessionId as string; - const isConnected = !!sshSessions[sessionId]?.isConnected; + const userId = (req as AuthenticatedRequest).userId; + const session = sshSessions[sessionId]; + if (session && !verifySessionOwnership(session, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + const isConnected = !!session?.isConnected; res.json({ status: "success", connected: isConnected }); }); @@ -2294,6 +2151,7 @@ app.get("/ssh/file_manager/ssh/status", (req, res) => { */ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => { const { sessionId } = req.body; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -2308,6 +2166,10 @@ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => { }); } + if (!verifySessionOwnership(session, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); @@ -2360,6 +2222,10 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + sshConn.lastActive = Date.now(); sshConn.activeOperations++; const trySFTP = () => { @@ -2472,6 +2338,10 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { }; const tryFallbackMethod = () => { + if (!sshConn?.isConnected) { + sshConn.activeOperations--; + return res.status(500).json({ error: "SSH session disconnected" }); + } try { const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); sshConn.client.exec( @@ -3099,7 +2969,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { * /ssh/file_manager/ssh/writeFile: * post: * summary: Write to a file - * description: Writes content to a file on the remote host. + * description: Writes content to a file on the remote host and preserves the existing permissions when the file already exists. * tags: * - File Manager * requestBody: @@ -3155,6 +3025,112 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { }); sshConn.lastActive = Date.now(); + let preservedMode: number | undefined; + + const restoreOriginalMode = ( + sftp: import("ssh2").SFTPWrapper | null, + onComplete: () => void, + ) => { + if (preservedMode === undefined) { + onComplete(); + return; + } + + const permissions = preservedMode.toString(8); + + if (sftp) { + sftp.chmod(filePath, preservedMode, (chmodErr) => { + if (chmodErr) { + fileLogger.warn("Failed to restore file permissions after save", { + operation: "file_write_restore_permissions", + sessionId, + userId, + path: filePath, + permissions, + error: chmodErr.message, + }); + } else { + fileLogger.info("Restored file permissions after save", { + operation: "file_write_restore_permissions", + sessionId, + userId, + path: filePath, + permissions, + }); + } + + onComplete(); + }); + return; + } + + const escapedPath = filePath.replace(/'/g, "'\"'\"'"); + const chmodCommand = `chmod ${permissions} '${escapedPath}' && echo "SUCCESS"`; + + sshConn.client.exec(chmodCommand, (err, stream) => { + if (err) { + fileLogger.warn("Failed to restore file permissions after save", { + operation: "file_write_restore_permissions", + sessionId, + userId, + path: filePath, + permissions, + error: err.message, + }); + onComplete(); + return; + } + + let outputData = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + outputData += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); + + stream.on("close", (code) => { + if (outputData.includes("SUCCESS")) { + fileLogger.info("Restored file permissions after save", { + operation: "file_write_restore_permissions", + sessionId, + userId, + path: filePath, + permissions, + }); + } else { + fileLogger.warn("Failed to restore file permissions after save", { + operation: "file_write_restore_permissions", + sessionId, + userId, + path: filePath, + permissions, + exitCode: code, + error: + errorData || "Permission restore command did not report success", + }); + } + + onComplete(); + }); + + stream.on("error", (streamErr) => { + fileLogger.warn("Failed to restore file permissions after save", { + operation: "file_write_restore_permissions", + sessionId, + userId, + path: filePath, + permissions, + error: streamErr.message, + }); + onComplete(); + }); + }); + }; + const trySFTP = () => { try { fileLogger.info("Opening SFTP channel", { @@ -3193,75 +3169,88 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { return; } - const writeStream = sftp.createWriteStream(filePath); - - let hasError = false; - let hasFinished = false; - - writeStream.on("error", (streamErr) => { - if (hasError || hasFinished) return; - hasError = true; - fileLogger.warn( - `SFTP write failed, trying fallback method: ${streamErr.message}`, - ); - tryFallbackMethod(); - }); - - writeStream.on("finish", () => { - if (hasError || hasFinished) return; - hasFinished = true; - fileLogger.success("File written successfully", { - operation: "file_write_success", - sessionId, - userId, - path: filePath, - bytes: fileBuffer.length, - }); - if (!res.headersSent) { - res.json({ - message: "File written successfully", - path: filePath, - toast: { - type: "success", - message: `File written: ${filePath}`, + sftp.stat(filePath, (statErr, stats) => { + if (statErr) { + fileLogger.warn( + "Failed to read existing file permissions before save", + { + operation: "file_write_stat", + sessionId, + userId, + path: filePath, + error: statErr.message, }, + ); + } else if (stats.isFile()) { + preservedMode = stats.mode & 0o7777; + } + + const writeStream = sftp.createWriteStream(filePath); + + let hasError = false; + let hasFinished = false; + let isFinalizing = false; + + const finalizeSuccess = () => { + if (hasError || hasFinished) return; + hasFinished = true; + isFinalizing = false; + fileLogger.success("File written successfully", { + operation: "file_write_success", + sessionId, + userId, + path: filePath, + bytes: fileBuffer.length, }); + if (!res.headersSent) { + res.json({ + message: "File written successfully", + path: filePath, + toast: { + type: "success", + message: `File written: ${filePath}`, + }, + }); + } + }; + + writeStream.on("error", (streamErr) => { + if (hasError || hasFinished || isFinalizing) return; + hasError = true; + isFinalizing = false; + fileLogger.warn( + `SFTP write failed, trying fallback method: ${streamErr.message}`, + ); + tryFallbackMethod(); + }); + + const finishWrite = () => { + if (hasError || hasFinished || isFinalizing) return; + isFinalizing = true; + restoreOriginalMode(sftp, finalizeSuccess); + }; + + writeStream.on("finish", () => { + finishWrite(); + }); + + writeStream.on("close", () => { + finishWrite(); + }); + + try { + writeStream.write(fileBuffer); + writeStream.end(); + } catch (writeErr) { + if (hasError || hasFinished) return; + hasError = true; + isFinalizing = false; + fileLogger.warn( + `SFTP write operation failed, trying fallback method: ${writeErr.message}`, + ); + tryFallbackMethod(); } }); - - writeStream.on("close", () => { - if (hasError || hasFinished) return; - hasFinished = true; - fileLogger.success("File written successfully", { - operation: "file_write_success", - sessionId, - userId, - path: filePath, - bytes: fileBuffer.length, - }); - if (!res.headersSent) { - res.json({ - message: "File written successfully", - path: filePath, - toast: { - type: "success", - message: `File written: ${filePath}`, - }, - }); - } - }); - - try { - writeStream.write(fileBuffer); - writeStream.end(); - } catch (writeErr) { - if (hasError || hasFinished) return; - hasError = true; - fileLogger.warn( - `SFTP write operation failed, trying fallback method: ${writeErr.message}`, - ); - tryFallbackMethod(); - } }) .catch((err: Error) => { fileLogger.warn( @@ -3278,6 +3267,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { }; const tryFallbackMethod = () => { + if (!sshConn?.isConnected) { + sshConn.activeOperations--; + return res.status(500).json({ error: "SSH session disconnected" }); + } try { let contentBuffer: Buffer; if (typeof content === "string") { @@ -3327,16 +3320,18 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { stream.on("close", (code) => { if (outputData.includes("SUCCESS")) { - if (!res.headersSent) { - res.json({ - message: "File written successfully", - path: filePath, - toast: { - type: "success", - message: `File written: ${filePath}`, - }, - }); - } + restoreOriginalMode(null, () => { + if (!res.headersSent) { + res.json({ + message: "File written successfully", + path: filePath, + toast: { + type: "success", + message: `File written: ${filePath}`, + }, + }); + } + }); } else { fileLogger.error( `Fallback write failed with code ${code}: ${errorData}`, @@ -3564,6 +3559,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { }; const tryFallbackMethod = () => { + if (!sshConn?.isConnected) { + sshConn.activeOperations--; + return res.status(500).json({ error: "SSH session disconnected" }); + } try { let contentBuffer: Buffer; if (typeof content === "string") { @@ -3588,6 +3587,20 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { chunks.push(base64Content.slice(i, i + chunkSize)); } + if (!sshConn?.isConnected) { + fileLogger.error("SSH connection lost before fallback upload", { + operation: "file_upload_fallback", + sessionId, + path: fullPath, + }); + if (!res.headersSent) { + return res + .status(500) + .json({ error: "SSH connection lost during upload" }); + } + return; + } + if (chunks.length === 1) { const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); @@ -3615,6 +3628,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { errorData += chunk.toString(); }); + stream.stderr.on("error", (stderrErr) => { + fileLogger.error("Fallback upload stderr error:", stderrErr); + }); + stream.on("close", (code) => { if (outputData.includes("SUCCESS")) { if (!res.headersSent) { @@ -3685,6 +3702,13 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { errorData += chunk.toString(); }); + stream.stderr.on("error", (stderrErr) => { + fileLogger.error( + "Chunked fallback upload stderr error:", + stderrErr, + ); + }); + stream.on("close", (code) => { if (outputData.includes("SUCCESS")) { if (!res.headersSent) { @@ -5228,26 +5252,32 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { const targetPath = extractPath || archivePath.substring(0, archivePath.lastIndexOf("/")); + const escapedArchive = archivePath.replace(/'/g, "'\"'\"'"); + const escapedTarget = targetPath.replace(/'/g, "'\"'\"'"); + const escapedDecompressed = archivePath + .replace(/\.gz$/, "") + .replace(/'/g, "'\"'\"'"); + if (fileExt.endsWith(".tar.gz") || fileExt.endsWith(".tgz")) { - extractCommand = `tar -xzf "${archivePath}" -C "${targetPath}"`; + extractCommand = `tar -xzf '${escapedArchive}' -C '${escapedTarget}'`; } else if (fileExt.endsWith(".tar.bz2") || fileExt.endsWith(".tbz2")) { - extractCommand = `tar -xjf "${archivePath}" -C "${targetPath}"`; + extractCommand = `tar -xjf '${escapedArchive}' -C '${escapedTarget}'`; } else if (fileExt.endsWith(".tar.xz")) { - extractCommand = `tar -xJf "${archivePath}" -C "${targetPath}"`; + extractCommand = `tar -xJf '${escapedArchive}' -C '${escapedTarget}'`; } else if (fileExt.endsWith(".tar")) { - extractCommand = `tar -xf "${archivePath}" -C "${targetPath}"`; + extractCommand = `tar -xf '${escapedArchive}' -C '${escapedTarget}'`; } else if (fileExt.endsWith(".zip")) { - extractCommand = `unzip -o "${archivePath}" -d "${targetPath}"`; + extractCommand = `unzip -o '${escapedArchive}' -d '${escapedTarget}'`; } else if (fileExt.endsWith(".gz") && !fileExt.endsWith(".tar.gz")) { - extractCommand = `gunzip -c "${archivePath}" > "${archivePath.replace(/\.gz$/, "")}"`; + extractCommand = `gunzip -c '${escapedArchive}' > '${escapedDecompressed}'`; } else if (fileExt.endsWith(".bz2") && !fileExt.endsWith(".tar.bz2")) { - extractCommand = `bunzip2 -k "${archivePath}"`; + extractCommand = `bunzip2 -k '${escapedArchive}'`; } else if (fileExt.endsWith(".xz") && !fileExt.endsWith(".tar.xz")) { - extractCommand = `unxz -k "${archivePath}"`; + extractCommand = `unxz -k '${escapedArchive}'`; } else if (fileExt.endsWith(".7z")) { - extractCommand = `7z x "${archivePath}" -o"${targetPath}"`; + extractCommand = `7z x '${escapedArchive}' -o'${escapedTarget}'`; } else if (fileExt.endsWith(".rar")) { - extractCommand = `unrar x "${archivePath}" "${targetPath}/"`; + extractCommand = `unrar x '${escapedArchive}' '${escapedTarget}/'`; } else { return res.status(400).json({ error: "Unsupported archive format" }); } @@ -5433,10 +5463,12 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { const firstPath = paths[0]; const workingDir = firstPath.substring(0, firstPath.lastIndexOf("/")) || "/"; + const escapeShell = (s: string) => s.replace(/'/g, "'\"'\"'"); + const fileNames = paths .map((p) => { const name = p.split("/").pop(); - return `"${name}"`; + return `'${escapeShell(name || "")}'`; }) .join(" "); @@ -5449,18 +5481,21 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { : `${workingDir}/${archiveName}`; } + const escapedDir = escapeShell(workingDir); + const escapedArchive = escapeShell(archivePath); + if (compressionFormat === "zip") { - compressCommand = `cd "${workingDir}" && zip -r "${archivePath}" ${fileNames}`; + compressCommand = `cd '${escapedDir}' && zip -r '${escapedArchive}' ${fileNames}`; } else if (compressionFormat === "tar.gz" || compressionFormat === "tgz") { - compressCommand = `cd "${workingDir}" && tar -czf "${archivePath}" ${fileNames}`; + compressCommand = `cd '${escapedDir}' && tar -czf '${escapedArchive}' ${fileNames}`; } else if (compressionFormat === "tar.bz2" || compressionFormat === "tbz2") { - compressCommand = `cd "${workingDir}" && tar -cjf "${archivePath}" ${fileNames}`; + compressCommand = `cd '${escapedDir}' && tar -cjf '${escapedArchive}' ${fileNames}`; } else if (compressionFormat === "tar.xz") { - compressCommand = `cd "${workingDir}" && tar -cJf "${archivePath}" ${fileNames}`; + compressCommand = `cd '${escapedDir}' && tar -cJf '${escapedArchive}' ${fileNames}`; } else if (compressionFormat === "tar") { - compressCommand = `cd "${workingDir}" && tar -cf "${archivePath}" ${fileNames}`; + compressCommand = `cd '${escapedDir}' && tar -cf '${escapedArchive}' ${fileNames}`; } else if (compressionFormat === "7z") { - compressCommand = `cd "${workingDir}" && 7z a "${archivePath}" ${fileNames}`; + compressCommand = `cd '${escapedDir}' && 7z a '${escapedArchive}' ${fileNames}`; } else { return res.status(400).json({ error: "Unsupported compression format" }); } diff --git a/src/backend/ssh/host-resolver.ts b/src/backend/ssh/host-resolver.ts new file mode 100644 index 00000000..94d2f793 --- /dev/null +++ b/src/backend/ssh/host-resolver.ts @@ -0,0 +1,173 @@ +import { getDb } from "../database/db/index.js"; +import { hosts, sshCredentials } from "../database/db/schema.js"; +import { eq, and } from "drizzle-orm"; +import { SimpleDBOps } from "../utils/simple-db-ops.js"; +import { logger } from "../utils/logger.js"; +import type { SSHHost } from "../../types/index.js"; + +const sshLogger = logger; + +/** + * Resolve a host with its credentials server-side by hostId. + * This avoids passing credentials through the frontend. + */ +export async function resolveHostById( + hostId: number, + userId: string, +): Promise { + const db = getDb(); + + const hostResults = await SimpleDBOps.select( + db.select().from(hosts).where(eq(hosts.id, hostId)), + "ssh_data", + userId, + ); + + if (hostResults.length === 0) return null; + + const host = hostResults[0] as Record; + + // Parse JSON fields + if (typeof host.jumpHosts === "string" && host.jumpHosts) { + try { + host.jumpHosts = JSON.parse(host.jumpHosts as string); + } catch { + host.jumpHosts = []; + } + } + if (typeof host.tunnelConnections === "string") { + try { + host.tunnelConnections = JSON.parse(host.tunnelConnections as string); + } catch { + host.tunnelConnections = []; + } + } + if (typeof host.statsConfig === "string" && host.statsConfig) { + try { + host.statsConfig = JSON.parse(host.statsConfig as string); + } catch { + host.statsConfig = undefined; + } + } + if (typeof host.terminalConfig === "string" && host.terminalConfig) { + try { + host.terminalConfig = JSON.parse(host.terminalConfig as string); + } catch { + host.terminalConfig = undefined; + } + } + if (typeof host.socks5ProxyChain === "string" && host.socks5ProxyChain) { + try { + host.socks5ProxyChain = JSON.parse(host.socks5ProxyChain as string); + } catch { + host.socks5ProxyChain = []; + } + } + if (typeof host.quickActions === "string" && host.quickActions) { + try { + host.quickActions = JSON.parse(host.quickActions as string); + } catch { + host.quickActions = []; + } + } + + // Resolve credential if using credential-based auth + if (host.credentialId) { + const ownerId = (host.userId || userId) as string; + try { + // Try shared credential first for non-owner users + if (userId !== ownerId) { + try { + const { SharedCredentialManager } = + await import("../utils/shared-credential-manager.js"); + const sharedCredManager = SharedCredentialManager.getInstance(); + const sharedCred = await sharedCredManager.getSharedCredentialForUser( + hostId, + userId, + ); + if (sharedCred) { + host.password = sharedCred.password; + host.key = sharedCred.key; + host.keyPassword = sharedCred.keyPassword; + host.keyType = sharedCred.keyType; + if (!host.overrideCredentialUsername) { + host.username = sharedCred.username; + } + host.authType = sharedCred.key + ? "key" + : sharedCred.password + ? "password" + : "none"; + return host as unknown as SSHHost; + } + } catch (e) { + sshLogger.warn("Failed to get shared credential, falling back", { + operation: "host_resolver_shared_credential", + hostId, + error: e instanceof Error ? e.message : "Unknown", + }); + } + } + + const credentials = await SimpleDBOps.select( + db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, host.credentialId as number), + eq(sshCredentials.userId, ownerId), + ), + ), + "ssh_credentials", + ownerId, + ); + + if (credentials.length > 0) { + const cred = credentials[0] as Record; + host.password = cred.password; + host.key = cred.key; + host.keyPassword = cred.keyPassword; + host.keyType = cred.keyType; + if (!host.overrideCredentialUsername) { + host.username = cred.username; + } + host.authType = cred.key ? "key" : cred.password ? "password" : "none"; + } + } catch (e) { + sshLogger.warn("Failed to resolve credential for host", { + operation: "host_resolver_credential", + hostId, + error: e instanceof Error ? e.message : "Unknown", + }); + } + } + + return host as unknown as SSHHost; +} + +/** + * Check if a user has access to a host (owner or shared access). + */ +export async function checkHostAccess( + hostId: number, + userId: string, + hostUserId: string, + requiredPermission: "read" | "execute" = "execute", +): Promise { + if (userId === hostUserId) return true; + + try { + const { PermissionManager } = + await import("../utils/permission-manager.js"); + const permissionManager = PermissionManager.getInstance(); + const accessInfo = await permissionManager.canAccessHost( + userId, + hostId, + requiredPermission, + ); + return accessInfo.hasAccess; + } catch { + return false; + } +} diff --git a/src/backend/ssh/opkssh-auth.ts b/src/backend/ssh/opkssh-auth.ts index d4b919fb..c4d62b41 100644 --- a/src/backend/ssh/opkssh-auth.ts +++ b/src/backend/ssh/opkssh-auth.ts @@ -12,10 +12,13 @@ import { FieldCrypto } from "../utils/field-crypto.js"; import { promises as fs } from "fs"; import path from "path"; import axios from "axios"; +import yaml from "js-yaml"; import { getRequestOrigin } from "../utils/request-origin.js"; const AUTH_TIMEOUT = 60 * 1000; +export const OPKSSH_CALLBACK_PATH = "/host/opkssh-callback"; + interface OPKSSHAuthSession { requestId: string; userId: string; @@ -25,6 +28,7 @@ interface OPKSSHAuthSession { localPort: number; callbackPort: number; remoteRedirectUri: string; + providers: Array<{ alias: string; issuer: string }>; status: | "starting" | "waiting_for_auth" @@ -47,6 +51,7 @@ interface OPKSSHAuthSession { } const activeAuthSessions = new Map(); +const oauthStateToRequestId = new Map(); const cleanupInProgress = new Set(); function getOPKConfigPath(): string { @@ -78,10 +83,17 @@ async function createTemplateConfig(): Promise { } } +interface ProviderRedirectInfo { + alias: string; + issuer: string; + redirectUris: string[]; +} + async function checkOPKConfigExists(): Promise<{ exists: boolean; error?: string; configPath?: string; + providers?: ProviderRedirectInfo[]; }> { const configPath = getOPKConfigPath(); const isDocker = @@ -119,15 +131,44 @@ async function checkOPKConfigExists(): Promise<{ }; } - if (!content.includes("redirect_uris:")) { - return { - exists: false, - configPath, - error: `OPKSSH configuration is missing 'redirect_uris' field. This field must contain the Termix callback URL that you registered with your OAuth provider (e.g., http://localhost:8080/host/opkssh-callback for Docker). The static callback route will internally redirect to the dynamic route for proper URL rewriting.`, + let providers: ProviderRedirectInfo[] = []; + try { + const parsed = yaml.load(content) as { + providers?: Array<{ + alias?: string; + issuer?: string; + redirect_uris?: string[]; + }>; }; + if (parsed?.providers && Array.isArray(parsed.providers)) { + providers = parsed.providers + .filter( + ( + p, + ): p is { + alias: string; + issuer: string; + redirect_uris?: string[]; + } => typeof p.alias === "string" && typeof p.issuer === "string", + ) + .map((p) => ({ + alias: p.alias, + issuer: p.issuer.replace(/^https?:\/\//, ""), + redirectUris: Array.isArray(p.redirect_uris) + ? p.redirect_uris.filter( + (u): u is string => typeof u === "string", + ) + : [], + })); + } + } catch (e) { + sshLogger.warn("Failed to parse OPKSSH config for providers", { + operation: "opkssh_config_parse_providers_error", + error: e, + }); } - return { exists: true, configPath }; + return { exists: true, configPath, providers }; } catch { await createTemplateConfig(); return { @@ -138,6 +179,63 @@ async function checkOPKConfigExists(): Promise<{ } } +// OPKSSH's `redirect_uris` field lists candidate LOCAL ports for the callback listener +// that OPKSSH binds on the host running the binary. The openpubkey library enforces these +// must be localhost, a non-localhost entry causes ECONNRESET on /select/ at runtime. +// The publicly registered OAuth redirect URI is what Termix passes via --remote-redirect-uri +// (derived from request origin); users do NOT put that URL in this config field. +function validateRedirectUrisAreLocalhost( + providers: ProviderRedirectInfo[], +): { ok: true } | { ok: false; message: string } { + const isLocalHost = (host: string): boolean => { + const bare = host.replace(/^\[|\]$/g, ""); + return ( + bare === "localhost" || + bare === "127.0.0.1" || + bare === "::1" || + bare === "0:0:0:0:0:0:0:1" || + bare.startsWith("localhost:") || + bare.startsWith("127.0.0.1:") + ); + }; + + const issues: string[] = []; + for (const p of providers) { + const uris = p.redirectUris || []; + if (uris.length === 0) continue; + const nonLocal = uris.filter((u) => { + try { + return !isLocalHost(new URL(u).hostname); + } catch { + return true; + } + }); + if (nonLocal.length > 0) { + issues.push( + `Provider '${p.alias}': non-localhost entries in redirect_uris: ${nonLocal.join(", ")}`, + ); + } + } + + if (issues.length > 0) { + return { + ok: false, + message: + `OPKSSH configuration error: 'redirect_uris' must only contain localhost URLs.\n\n` + + `${issues.join("\n")}\n\n` + + `This field is OPKSSH's local callback listener, it must be localhost (or omitted to use ` + + `the defaults http://localhost:3000/login-callback, :10001, :11110). ` + + `The public Termix callback URL is supplied automatically by Termix via --remote-redirect-uri; ` + + `you do not put it here. Register the PUBLIC Termix URL with your OAuth provider instead ` + + `(e.g. https://your-domain${OPKSSH_CALLBACK_PATH}).\n\n` + + `Fix: remove the non-localhost entries above, or delete the whole 'redirect_uris' block to use defaults.\n\n` + + `Docs: https://docs.termix.site/opkssh`, + }; + } + + return { ok: true }; +} + export async function startOPKSSHAuth( userId: string, hostId: number, @@ -178,8 +276,37 @@ export async function startOPKSSHAuth( return ""; } + const redirectValidation = validateRedirectUrisAreLocalhost( + configCheck.providers || [], + ); + if (redirectValidation.ok === false) { + sshLogger.warn("OPKSSH config redirect_uris validation failed", { + operation: "opkssh_config_redirect_uris_not_localhost", + configPath: configCheck.configPath, + }); + ws.send( + JSON.stringify({ + type: "opkssh_config_error", + requestId: "", + error: redirectValidation.message, + instructions: redirectValidation.message, + }), + ); + return ""; + } + const requestId = randomUUID(); - const remoteRedirectUri = `${requestOrigin}/host/opkssh-callback`; + const remoteRedirectUri = `${requestOrigin}${OPKSSH_CALLBACK_PATH}`; + + sshLogger.info("Starting OPKSSH auth session", { + operation: "opkssh_start_auth_remote_redirect_uri", + requestId, + userId, + hostId, + requestOrigin, + remoteRedirectUri, + providerAliases: (configCheck.providers || []).map((p) => p.alias), + }); const session: Partial = { requestId, @@ -189,6 +316,7 @@ export async function startOPKSSHAuth( localPort: 0, callbackPort: 0, remoteRedirectUri, + providers: configCheck.providers || [], status: "starting", ws, stdoutBuffer: "", @@ -261,7 +389,80 @@ export async function startOPKSSHAuth( handleOPKSSHOutput(requestId, stderr); } - if (stderr.includes("provider not found") || stderr.includes("config")) { + const lowerStderr = stderr.toLowerCase(); + + // OPKSSH's openpubkey library rejects non-localhost `redirect_uris` at runtime + // with the distinctive message "redirectURI must be localhost". Surface that + // directly with actionable guidance. + if (lowerStderr.includes("redirecturi must be localhost")) { + sshLogger.warn("OPKSSH rejected non-localhost entry in redirect_uris", { + operation: "opkssh_stderr_redirect_uris_not_localhost", + requestId, + remoteRedirectUri, + stderrSnippet: stderr.slice(0, 500), + }); + ws.send( + JSON.stringify({ + type: "opkssh_config_error", + requestId, + error: + `OPKSSH rejected the local callback URI: every entry in 'redirect_uris' must be localhost.\n\n` + + `OPKSSH output:\n${stderr.trim()}\n\n` + + `The 'redirect_uris' config field is OPKSSH's LOCAL listener — it is not the public Termix callback. ` + + `Remove any non-localhost entries from redirect_uris (or delete the whole block to use OPKSSH's ` + + `defaults of :3000, :10001, :11110). Register the public Termix callback URL with your OAuth ` + + `provider instead, Termix passes it to OPKSSH automatically via --remote-redirect-uri.`, + instructions: "See documentation: https://docs.termix.site/opkssh", + }), + ); + await cleanup(); + return; + } + + // Generic redirect-uri/mismatch errors (OAuth provider side, OPKSSH config side, etc.) + const genericRedirectIndicators = [ + "redirect_uri", + "redirect uri", + "invalid redirect", + "no matching redirect", + "allowed redirect", + "mismatching redirection", + ]; + const hasGenericRedirectError = genericRedirectIndicators.some((s) => + lowerStderr.includes(s), + ); + + if (hasGenericRedirectError) { + sshLogger.warn("OPKSSH stderr reported redirect_uri error", { + operation: "opkssh_stderr_redirect_uri_error", + requestId, + remoteRedirectUri, + stderrSnippet: stderr.slice(0, 500), + }); + ws.send( + JSON.stringify({ + type: "opkssh_config_error", + requestId, + error: + `OPKSSH or the OAuth provider rejected the redirect URI.\n\n` + + `Computed Termix callback URI (sent to provider): ${remoteRedirectUri}\n\n` + + `OPKSSH output:\n${stderr.trim()}\n\n` + + `Register '${remoteRedirectUri}' as an authorized redirect URI with your OAuth provider ` + + `(e.g. in Google Cloud Console → OAuth client). ` + + `Also confirm any 'redirect_uris' in your OPKSSH config contain ONLY localhost URLs.`, + instructions: "See documentation: https://docs.termix.site/opkssh", + }), + ); + await cleanup(); + return; + } + + if ( + stderr.includes("provider not found") || + stderr.includes("config error") || + stderr.includes("invalid config") || + stderr.includes("config not found") + ) { ws.send( JSON.stringify({ type: "opkssh_config_error", @@ -340,18 +541,18 @@ function handleOPKSSHOutput(requestId: string, output: string): void { session.stdoutBuffer += output; const chooserUrlMatch = session.stdoutBuffer.match( - /(?:Opening browser to|Open your browser to:)\s*http:\/\/localhost:(\d+)\/chooser/, + /(?:Opening browser to|Open your browser to:)\s*http:\/\/(?:localhost|127\.0\.0\.1):(\d+)\/chooser/, ); if (chooserUrlMatch && session.status === "starting") { const actualPort = parseInt(chooserUrlMatch[1], 10); - const localChooserUrl = `http://localhost:${actualPort}/chooser`; + const localChooserUrl = `http://127.0.0.1:${actualPort}/chooser`; session.localPort = actualPort; - const baseUrl = session.remoteRedirectUri.replace( - /\/ssh\/opkssh-callback$/, - "", - ); + const baseUrl = session.remoteRedirectUri + .replace(/\/host\/opkssh-callback$/, "") + // In direct dev mode the WS server (30002) is separate from the HTTP API (30001) + .replace(/:30002\b/, ":30001"); const proxiedChooserUrl = `${baseUrl}/host/opkssh-chooser/${requestId}`; session.status = "waiting_for_auth"; @@ -361,6 +562,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void { requestId, stage: "chooser", url: proxiedChooserUrl, + providers: session.providers, localUrl: localChooserUrl, message: "Please authenticate in your browser", }), @@ -368,7 +570,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void { } const callbackPortMatch = session.stdoutBuffer.match( - /listening on http:\/\/127\.0\.0\.1:(\d+)\//, + /listening on http:\/\/(?:127\.0\.0\.1|localhost):(\d+)\//, ); if (callbackPortMatch && !session.callbackPort) { session.callbackPort = parseInt(callbackPortMatch[1], 10); @@ -509,25 +711,6 @@ async function storeOPKSSHToken(session: OPKSSHAuthSession): Promise { }), ); - try { - await axios.post( - "http://localhost:30006/activity/log", - { - type: "opkssh_authentication", - hostId: session.hostId, - hostName: session.hostname, - status: "approved", - }, - { - headers: { - Authorization: `Bearer ${process.env.INTERNAL_AUTH_TOKEN}`, - }, - }, - ); - } catch (activityError) { - sshLogger.warn("Failed to log OPKSSH activity", activityError); - } - await session.cleanup(); } catch (error) { sshLogger.error( @@ -657,7 +840,7 @@ export async function handleOAuthCallback( } try { - const callbackUrl = `http://localhost:${session.localPort}/login-callback?${queryString}`; + const callbackUrl = `http://127.0.0.1:${session.localPort}/login-callback?${queryString}`; await axios.get(callbackUrl, { timeout: 10000, validateStatus: () => true, @@ -720,6 +903,13 @@ async function cleanupAuthSession(requestId: string): Promise { } } + // Clean up any OAuth state mappings for this session + for (const [state, reqId] of oauthStateToRequestId.entries()) { + if (reqId === requestId) { + oauthStateToRequestId.delete(state); + } + } + activeAuthSessions.delete(requestId); } finally { cleanupInProgress.delete(requestId); @@ -753,6 +943,18 @@ export function getActiveSessionsAll(): OPKSSHAuthSession[] { return Array.from(activeAuthSessions.values()); } +export function registerOAuthState(state: string, requestId: string): void { + oauthStateToRequestId.set(state, requestId); +} + +export function getRequestIdByOAuthState(state: string): string | undefined { + return oauthStateToRequestId.get(state); +} + +export function clearOAuthState(state: string): void { + oauthStateToRequestId.delete(state); +} + export async function getUserIdFromRequest(req: { cookies?: Record; headers: Record; diff --git a/src/backend/ssh/opkssh-cert-auth.ts b/src/backend/ssh/opkssh-cert-auth.ts new file mode 100644 index 00000000..bd8e4da8 --- /dev/null +++ b/src/backend/ssh/opkssh-cert-auth.ts @@ -0,0 +1,236 @@ +// OPKSSH certificate authentication workarounds for ssh2. +// ssh2 doesn't support OpenSSH cert auth natively — this module grafts +// the certificate onto the parsed key, wraps ECDSA signing to convert +// DER → SSH wire format, and patches Protocol.authPK to use the base +// algorithm in the signature wrapper (required by OpenSSH's sshkey_check_sigtype). + +import type { Client, ConnectConfig } from "ssh2"; + +interface OPKSSHToken { + privateKey: string; + sshCert: string; +} +export async function setupOPKSSHCertAuth( + config: ConnectConfig, + client: Client, + token: OPKSSHToken, + username: string, +): Promise { + const { createRequire } = await import("node:module"); + const esmRequire = createRequire(import.meta.url); + const { + utils: { parseKey }, + } = esmRequire("ssh2"); + + const parsed = parseKey(Buffer.from(token.privateKey)); + if (parsed instanceof Error || !parsed) { + throw new Error("Failed to parse OPKSSH private key"); + } + const privKey: any = Array.isArray(parsed) ? parsed[0] : parsed; + + // Extract cert type and blob from the stored certificate + const certParts = token.sshCert.trim().split(/\s+/); + const certType = certParts[0]; + privKey.type = certType; + const certBlob = Buffer.from(certParts[1], "base64"); + + // Replace the internal public SSH blob with the full certificate + const pubSSHSym = Object.getOwnPropertySymbols(privKey).find( + (s) => String(s) === "Symbol(Public key SSH)", + ); + if (!pubSSHSym) { + throw new Error( + "Cannot find public SSH symbol on parsed key, ssh2 internals may have changed", + ); + } + privKey[pubSSHSym] = certBlob; + + // Wrap sign() for ECDSA cert keys + if (privKey.type.startsWith("ecdsa-")) { + const origSign = privKey.sign.bind(privKey); + privKey.sign = (data: Buffer, algo?: string) => { + const sigAlgo = algo?.includes("-cert-") + ? algo.replace(/-cert-v\d+@openssh\.com$/, "") + : algo; + const sig = origSign(data, sigAlgo); + if (sig instanceof Error || sig[0] !== 0x30) return sig; + // Convert DER-encoded ECDSA signature to SSH wire format + try { + let pos = 2; + if (sig[1] & 0x80) pos += sig[1] & 0x7f; + pos++; + const rLen = sig[pos++]; + const r = sig.subarray(pos, pos + rLen); + pos += rLen + 1; + const sLen = sig[pos++]; + const s = sig.subarray(pos, pos + sLen); + const out = Buffer.allocUnsafe(4 + r.length + 4 + s.length); + out.writeUInt32BE(r.length, 0); + r.copy(out, 4); + out.writeUInt32BE(s.length, 4 + r.length); + s.copy(out, 4 + r.length + 4); + return out; + } catch { + return sig; + } + }; + } + + // Set up authHandler to bypass ssh2's cert type rejection + let certAuthAttempted = false; + config.authHandler = ( + methodsLeft: string[], + _partialSuccess: boolean, + callback: (authInfo: any) => void, + ) => { + if ( + !certAuthAttempted && + (!methodsLeft || methodsLeft.includes("publickey")) + ) { + certAuthAttempted = true; + callback({ type: "publickey", username, key: privKey }); + } else { + callback(false); + } + }; + + // Monkey-patch Protocol.authPK after connect() to fix the signature + // wrapper algorithm for cert types. + const baseAlgo = certType.replace(/-cert-v\d+@openssh\.com$/, ""); + const origConnect = client.connect.bind(client); + (client as any).connect = (cfg: any) => { + origConnect(cfg); + const proto = (client as any)._protocol; + if (!proto) return; + const origAuthPK = proto.authPK.bind(proto); + proto.authPK = ( + user: string, + pubKey: any, + keyAlgo: string | undefined, + cbSign?: Function, + ) => { + const isCertAuth = !!cbSign && pubKey?.type?.includes("-cert-"); + if (!isCertAuth) { + return origAuthPK(user, pubKey, keyAlgo, cbSign); + } + + // Signed auth with cert type: rebuild packet with base algo in + // the signature wrapper. keyAlgo may be undefined for ECDSA. + const certAlgo = keyAlgo || pubKey.type; + const pubSSH = pubKey.getPublicSSH(); + const sessionID = proto._kex.sessionID; + const sesLen = sessionID.length; + const userLen = Buffer.byteLength(user); + const certAlgoLen = Buffer.byteLength(certAlgo); + const baseAlgoLen = Buffer.byteLength(baseAlgo); + const pubKeyLen = pubSSH.length; + + // Build data to sign (uses cert algo — matches server verification) + const sigDataLen = + 4 + + sesLen + + 1 + + 4 + + userLen + + 4 + + 14 + + 4 + + 9 + + 1 + + 4 + + certAlgoLen + + 4 + + pubKeyLen; + const sigData = Buffer.allocUnsafe(sigDataLen); + let sp = 0; + sigData.writeUInt32BE(sesLen, sp); + sp += 4; + sessionID.copy(sigData, sp); + sp += sesLen; + sigData[sp++] = 50; // SSH_MSG_USERAUTH_REQUEST + sigData.writeUInt32BE(userLen, sp); + sp += 4; + sigData.write(user, sp, userLen, "utf8"); + sp += userLen; + sigData.writeUInt32BE(14, sp); + sp += 4; + sigData.write("ssh-connection", sp, 14, "utf8"); + sp += 14; + sigData.writeUInt32BE(9, sp); + sp += 4; + sigData.write("publickey", sp, 9, "utf8"); + sp += 9; + sigData[sp++] = 1; // TRUE + sigData.writeUInt32BE(certAlgoLen, sp); + sp += 4; + sigData.write(certAlgo, sp, certAlgoLen, "utf8"); + sp += certAlgoLen; + sigData.writeUInt32BE(pubKeyLen, sp); + sp += 4; + pubSSH.copy(sigData, sp); + + cbSign(sigData, (signature: Buffer) => { + const sigLen = signature.length; + const payloadLen = + 1 + + 4 + + userLen + + 4 + + 14 + + 4 + + 9 + + 1 + + 4 + + certAlgoLen + + 4 + + pubKeyLen + + 4 + + 4 + + baseAlgoLen + + 4 + + sigLen; + const packet = proto._packetRW.write.alloc(payloadLen); + let pp = proto._packetRW.write.allocStart; + packet[pp] = 50; // SSH_MSG_USERAUTH_REQUEST + packet.writeUInt32BE(userLen, ++pp); + pp += 4; + packet.write(user, pp, userLen, "utf8"); + pp += userLen; + packet.writeUInt32BE(14, pp); + pp += 4; + packet.write("ssh-connection", pp, 14, "utf8"); + pp += 14; + packet.writeUInt32BE(9, pp); + pp += 4; + packet.write("publickey", pp, 9, "utf8"); + pp += 9; + packet[pp++] = 1; // TRUE + // Header: cert type + packet.writeUInt32BE(certAlgoLen, pp); + pp += 4; + packet.write(certAlgo, pp, certAlgoLen, "utf8"); + pp += certAlgoLen; + // Public key blob + packet.writeUInt32BE(pubKeyLen, pp); + pp += 4; + pubSSH.copy(packet, pp); + pp += pubKeyLen; + // Signature wrapper: base algo (NOT cert type) + packet.writeUInt32BE(4 + baseAlgoLen + 4 + sigLen, pp); + pp += 4; + packet.writeUInt32BE(baseAlgoLen, pp); + pp += 4; + packet.write(baseAlgo, pp, baseAlgoLen, "utf8"); + pp += baseAlgoLen; + packet.writeUInt32BE(sigLen, pp); + pp += 4; + signature.copy(packet, pp); + + proto._authsQueue.push("publickey"); + proto._debug?.("Outbound: Sending USERAUTH_REQUEST (publickey)"); + const finalized = proto._packetRW.write.finalize(packet); + proto._cipher.encrypt(finalized); + }); + }; + }; +} diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 7562e1b6..1876c795 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -1,8 +1,9 @@ import express from "express"; import net from "net"; -import cors from "cors"; +import { createCorsMiddleware } from "../utils/cors-config.js"; import cookieParser from "cookie-parser"; import { Client, type ConnectConfig } from "ssh2"; +import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { getDb } from "../database/db/index.js"; import { hosts, sshCredentials } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; @@ -31,7 +32,9 @@ import { connectionPool, withConnection } from "./ssh-connection-pool.js"; function supportsMetrics(host: SSHHostWithCredentials): boolean { const connectionType = host.connectionType || "ssh"; - return connectionType === "ssh"; + if (connectionType !== "ssh") return false; + if (host.authType === "none" || host.authType === "opkssh") return false; + return true; } function isTcpPingEnabled(statsConfig: StatsConfig): boolean { @@ -865,7 +868,15 @@ class PollingManager { latestConfig.statsConfig.metricsEnabled && supportsMetrics(latestConfig.host) ) { - this.pollHostMetrics(latestConfig.host, latestConfig.viewerUserId); + this.pollHostMetrics( + latestConfig.host, + latestConfig.viewerUserId, + ).catch((err) => { + statsLogger.error("Metrics polling failed", err, { + operation: "metrics_poll_unhandled", + hostId: host.id, + }); + }); } }, intervalMs); } else { @@ -929,9 +940,11 @@ class PollingManager { return; } - const hasExistingMetrics = this.metricsStore.has(refreshedHost.id); + if (authFailureTracker.shouldSkip(host.id)) { + return; + } - if (hasExistingMetrics && pollingBackoff.shouldSkip(host.id)) { + if (pollingBackoff.shouldSkip(host.id)) { return; } @@ -942,16 +955,38 @@ class PollingManager { timestamp: Date.now(), }); pollingBackoff.reset(refreshedHost.id); + authFailureTracker.reset(refreshedHost.id); } catch (error) { + const isAuthError = + error instanceof Error && + (error.message.includes("authentication") || + error.message.includes("Authentication") || + error.message.includes("permission denied") || + error.message.includes("Permission denied")); + + if (isAuthError) { + // authFailureTracker already handles auth errors inside collectMetrics; + // only log on the first occurrence to avoid repeated spam + const alreadyTracked = authFailureTracker.shouldSkip(host.id); + if (!alreadyTracked) { + statsLogger.error("Stats collector connection failed", error, { + operation: "stats_connect_failed", + hostId: refreshedHost.id, + }); + } + return; + } + pollingBackoff.recordFailure(refreshedHost.id); - const latestConfig = this.pollingConfigs.get(refreshedHost.id); - if (latestConfig && latestConfig.statsConfig.metricsEnabled) { - const backoffInfo = pollingBackoff.getBackoffInfo(refreshedHost.id); + // Only log when a new retry window opens, not on every skipped poll + const backoff = pollingBackoff.getBackoffInfo(refreshedHost.id); + const isNewFailure = + backoff !== null && !backoff.includes("polling suspended"); + if (isNewFailure) { statsLogger.error("Stats collector connection failed", error, { operation: "stats_connect_failed", hostId: refreshedHost.id, - retryInfo: backoffInfo, }); } } @@ -1071,7 +1106,18 @@ class PollingManager { }); if (this.activeViewers.get(hostId)!.size === 1) { - this.startMetricsForHost(hostId, userId); + // Fire-and-forget: never let background metrics start-up failures + // propagate up to the HTTP handler that registered the viewer. + Promise.resolve() + .then(() => this.startMetricsForHost(hostId, userId)) + .catch((err) => { + statsLogger.warn("startMetricsForHost rejected (non-fatal)", { + operation: "start_metrics_unhandled", + hostId, + userId, + error: err instanceof Error ? err.message : String(err), + }); + }); } } @@ -1153,37 +1199,7 @@ function validateHostId( } const app = express(); -app.use( - cors({ - origin: (origin, callback) => { - if (!origin) return callback(null, true); - - const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - if (origin.startsWith("https://")) { - return callback(null, true); - } - - if (origin.startsWith("http://")) { - return callback(null, true); - } - - callback(new Error("Not allowed by CORS")); - }, - credentials: true, - methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allowedHeaders: [ - "Content-Type", - "Authorization", - "User-Agent", - "X-Electron-App", - ], - }), -); +app.use(createCorsMiddleware()); app.use(cookieParser()); app.use(express.json({ limit: "1mb" })); app.use((_req, res, next) => { @@ -1315,8 +1331,9 @@ async function resolveHostCredentials( const isSharedHost = userId !== ownerId; if (isSharedHost) { - const { SharedCredentialManager } = - await import("../utils/shared-credential-manager.js"); + const { SharedCredentialManager } = await import( + "../utils/shared-credential-manager.js" + ); const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCred = await sharedCredManager.getSharedCredentialForUser( host.id as number, @@ -1373,8 +1390,13 @@ async function resolveHostCredentials( if (credential.password) { baseHost.password = credential.password; } - if (credential.key) { - baseHost.key = credential.key; + if ( + credential.key || + (credential as Record).privateKey + ) { + baseHost.key = + credential.key || + ((credential as Record).privateKey as string); } if (credential.keyPassword) { baseHost.keyPassword = credential.keyPassword; @@ -1485,18 +1507,7 @@ async function buildSshConfig( "ssh-rsa", "ssh-dss", ], - cipher: [ - "chacha20-poly1305@openssh.com", - "aes256-gcm@openssh.com", - "aes128-gcm@openssh.com", - "aes256-ctr", - "aes192-ctr", - "aes128-ctr", - "aes256-cbc", - "aes192-cbc", - "aes128-cbc", - "3des-cbc", - ], + cipher: SSH_ALGORITHMS.cipher, hmac: [ "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", @@ -1546,7 +1557,7 @@ async function buildSshConfig( } else if (host.authType === "none") { // no credentials needed } else if (host.authType === "opkssh") { - // handled externally + // cert auth setup happens in createSshFactory (needs client instance) } else if (host.authType === "credential") { if (host.password) { base.password = host.password; @@ -1586,6 +1597,19 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise { const config = await buildSshConfig(host); const client = new Client(); + // Set up OPKSSH cert auth if needed (requires client instance) + if (host.authType === "opkssh" && host.userId) { + const { getOPKSSHToken } = await import("./opkssh-auth.js"); + const token = await getOPKSSHToken(host.userId, host.id); + if (!token) { + throw new Error( + "OPKSSH authentication required. Please open a Terminal connection first.", + ); + } + const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js"); + await setupOPKSSHCertAuth(config, client, token, host.username); + } + const proxyConfig: SOCKS5Config | null = host.useSocks5 && (host.socks5Host || @@ -1913,7 +1937,8 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ } else if ( error.message.includes("No password available") || error.message.includes("Unsupported authentication type") || - error.message.includes("No SSH key available") + error.message.includes("No SSH key available") || + error.message.includes("Invalid SSH key format") ) { authFailureTracker.recordFailure(host.id, "AUTH", true); } else if ( @@ -2413,6 +2438,27 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { const config = await buildSshConfig(host); const client = new Client(); + if (host.authType === "opkssh" && host.userId) { + const { getOPKSSHToken } = await import("./opkssh-auth.js"); + const token = await getOPKSSHToken(host.userId, host.id); + if (!token) { + connectionLogs.push( + createConnectionLog( + "error", + "auth", + "OPKSSH authentication required. Please open a Terminal connection first.", + ), + ); + return res.status(401).json({ + error: "OPKSSH authentication required", + requiresOPKSSHAuth: true, + connectionLogs, + }); + } + const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js"); + await setupOPKSSHCertAuth(config, client, token, host.username); + } + const connectionPromise = new Promise<{ success: boolean; requires_totp?: boolean; @@ -3028,8 +3074,70 @@ app.post("/metrics/register-viewer", async (req, res) => { } try { + // Graceful no-op if host is inaccessible, metrics disabled, or host type + // does not support metrics. The client may call this speculatively, so + // avoid returning 5xx for expected "no metrics available" scenarios. + let host: SSHHostWithCredentials | undefined; + try { + host = await fetchHostById(hostId, userId); + } catch (lookupErr) { + statsLogger.warn( + "register-viewer host lookup failed (treating as no-op)", + { + operation: "register_viewer_lookup", + hostId, + userId, + error: + lookupErr instanceof Error ? lookupErr.message : String(lookupErr), + }, + ); + } + + if (!host) { + return res.json({ + success: true, + skipped: true, + reason: "host_not_found", + }); + } + + if (!supportsMetrics(host)) { + return res.json({ + success: true, + skipped: true, + reason: "metrics_unsupported", + }); + } + + const statsConfig = pollingManager.parseStatsConfig(host.statsConfig); + if (!statsConfig.metricsEnabled) { + return res.json({ + success: true, + skipped: true, + reason: "metrics_disabled", + }); + } + const viewerSessionId = `viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - pollingManager.registerViewer(hostId, viewerSessionId, userId); + try { + pollingManager.registerViewer(hostId, viewerSessionId, userId); + } catch (regErr) { + statsLogger.warn( + "pollingManager.registerViewer threw (treating as no-op)", + { + operation: "register_viewer_internal", + hostId, + userId, + error: regErr instanceof Error ? regErr.message : String(regErr), + }, + ); + return res.json({ + success: true, + skipped: true, + reason: "register_failed_noop", + }); + } + res.json({ success: true, viewerSessionId }); } catch (error) { statsLogger.error("Failed to register viewer", { @@ -3038,7 +3146,14 @@ app.post("/metrics/register-viewer", async (req, res) => { userId, error: error instanceof Error ? error.message : String(error), }); - res.status(500).json({ error: "Failed to register viewer" }); + // Even on unexpected errors we prefer a graceful client experience: the + // viewer-registration is purely an optimization and should never break + // the UI. Report success:false but HTTP 200 so the client can decide. + res.status(200).json({ + success: false, + skipped: true, + reason: "internal_error", + }); } }); diff --git a/src/backend/ssh/terminal-session-manager.ts b/src/backend/ssh/terminal-session-manager.ts index 9a127355..44451158 100644 --- a/src/backend/ssh/terminal-session-manager.ts +++ b/src/backend/ssh/terminal-session-manager.ts @@ -19,7 +19,6 @@ export interface TerminalSession { sshConn: Client | null; sshStream: ClientChannel | null; jumpClient: Client | null; - opksshTempFiles: { keyPath: string; certPath: string } | null; cols: number; rows: number; @@ -32,6 +31,7 @@ export interface TerminalSession { outputBuffer: string[]; outputBufferBytes: number; + tmuxSessionName: string | null; } class TerminalSessionManager { @@ -99,7 +99,6 @@ class TerminalSessionManager { sshConn: null, sshStream: null, jumpClient: null, - opksshTempFiles: null, cols, rows, isConnected: false, @@ -109,6 +108,7 @@ class TerminalSessionManager { detachTimeout: null, outputBuffer: [], outputBufferBytes: 0, + tmuxSessionName: null, }; this.sessions.set(id, session); @@ -132,7 +132,6 @@ class TerminalSessionManager { conn: Client, stream: ClientChannel, jumpClient?: Client | null, - opksshTempFiles?: { keyPath: string; certPath: string } | null, ): void { const session = this.sessions.get(sessionId); if (!session) return; @@ -140,7 +139,6 @@ class TerminalSessionManager { session.sshConn = conn; session.sshStream = stream; session.jumpClient = jumpClient ?? null; - session.opksshTempFiles = opksshTempFiles ?? null; session.isConnected = true; } @@ -326,12 +324,6 @@ class TerminalSessionManager { session.jumpClient = null; } - if (session.opksshTempFiles) { - const tempFiles = session.opksshTempFiles; - session.opksshTempFiles = null; - this.cleanupOpksshFiles(tempFiles); - } - session.isConnected = false; session.outputBuffer = []; session.outputBufferBytes = 0; @@ -449,32 +441,6 @@ class TerminalSessionManager { } } - private async cleanupOpksshFiles(tempFiles: { - keyPath: string; - certPath: string; - }): Promise { - try { - const { promises: fs } = await import("fs"); - const results = await Promise.allSettled([ - fs.unlink(tempFiles.keyPath), - fs.unlink(tempFiles.certPath), - ]); - results.forEach((result, index) => { - if (result.status === "rejected") { - sshLogger.warn("Failed to cleanup OPKSSH temp file", { - operation: "opkssh_temp_cleanup_failed", - file: index === 0 ? "keyPath" : "certPath", - }); - } - }); - } catch (error) { - sshLogger.error("Failed to cleanup OPKSSH temp files", { - operation: "opkssh_temp_cleanup_error", - error, - }); - } - } - destroyAll(): void { for (const id of [...this.sessions.keys()]) { this.destroySession(id); diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 3466cd7c..982a54e2 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -1,5 +1,8 @@ import { WebSocketServer, WebSocket, type RawData } from "ws"; import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2"; +import net from "net"; +import dgram from "dgram"; +import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { parse as parseUrl } from "url"; import axios from "axios"; import { getDb } from "../database/db/index.js"; @@ -17,6 +20,46 @@ import { SSHAuthManager } from "./auth-manager.js"; import type { ProxyNode } from "../../types/index.js"; import { SSHHostKeyVerifier } from "./host-key-verifier.js"; import { sessionManager } from "./terminal-session-manager.js"; +import { + detectTmux, + attachOrCreateTmuxSession, + queryNewestTmuxSession, +} from "./tmux-helper.js"; + +async function performPortKnocking( + host: string, + sequence: Array<{ port: number; protocol?: string; delay?: number }>, +): Promise { + for (const knock of sequence) { + const protocol = knock.protocol || "tcp"; + const delay = knock.delay ?? 100; + + await new Promise((resolve) => { + if (protocol === "udp") { + const client = dgram.createSocket("udp4"); + client.send(Buffer.alloc(0), knock.port, host, () => { + client.close(); + resolve(); + }); + } else { + const socket = new net.Socket(); + socket.once("connect", () => { + socket.destroy(); + resolve(); + }); + socket.once("error", () => { + socket.destroy(); + resolve(); + }); + socket.connect(knock.port, host); + } + }); + + if (delay > 0) { + await new Promise((r) => setTimeout(r, delay)); + } + } +} interface ConnectToHostData { cols: number; @@ -42,6 +85,11 @@ interface ConnectToHostData { socks5Username?: string; socks5Password?: string; socks5ProxyChain?: unknown; + portKnockSequence?: Array<{ + port: number; + protocol?: "tcp" | "udp"; + delay?: number; + }>; terminalConfig?: { keepaliveInterval?: number; keepaliveCountMax?: number; @@ -255,7 +303,7 @@ async function createJumpHostChain( host: jumpHostConfig.ip?.replace(/^\[|\]$/g, "") || jumpHostConfig.ip, port: jumpHostConfig.port || 22, username: jumpHostConfig.username, - tryKeyboard: true, + tryKeyboard: jumpHostConfig.authType !== "none", readyTimeout: 30000, hostVerifier: jumpHostVerifier, }; @@ -320,7 +368,15 @@ const wss = new WebSocketServer({ verifyClient: async (info) => { try { const url = parseUrl(info.req.url!, true); - const token = url.query.token as string; + let token = url.query.token as string; + + if (!token) { + const cookieHeader = info.req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } + } if (!token) { return false; @@ -338,7 +394,7 @@ const wss = new WebSocketServer({ const existingConnections = userConnections.get(payload.userId); - if (existingConnections && existingConnections.size >= 3) { + if (existingConnections && existingConnections.size >= 10) { return false; } @@ -359,7 +415,15 @@ wss.on("connection", async (ws: WebSocket, req) => { try { const url = parseUrl(req.url!, true); - const token = url.query.token as string; + let token = url.query.token as string; + + if (!token) { + const cookieHeader = req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } + } if (!token) { ws.close(1008, "Authentication required"); @@ -427,10 +491,28 @@ wss.on("connection", async (ws: WebSocket, req) => { let warpgateAuthPromptSent = false; let warpgateAuthTimeout: NodeJS.Timeout | null = null; let isAwaitingAuthCredentials = false; - let opksshTempFiles: { keyPath: string; certPath: string } | null = null; + + let wsAlive = true; + + ws.on("pong", () => { + wsAlive = true; + }); const wsPingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { + if (!wsAlive) { + sshLogger.warn( + "WebSocket pong timeout - terminating zombie connection", + { + operation: "ws_pong_timeout", + userId, + sessionId: currentSessionId, + }, + ); + ws.terminate(); + return; + } + wsAlive = false; ws.ping(); } }, 30000); @@ -624,6 +706,7 @@ wss.on("connection", async (ws: WebSocket, req) => { hostName: s.hostName, createdAt: s.createdAt, lastDetachedAt: s.lastDetachedAt, + tmuxSessionName: s.tmuxSessionName, })), }), ); @@ -678,6 +761,52 @@ wss.on("connection", async (ws: WebSocket, req) => { ws.send(JSON.stringify({ type: "pong" })); break; + case "tmux_attach": { + const tmuxData = data as { sessionName: string }; + const session = currentSessionId + ? sessionManager.getSession(currentSessionId) + : null; + if (session?.sshStream) { + const existingName = tmuxData.sessionName || undefined; + attachOrCreateTmuxSession(session.sshStream, existingName); + if (existingName) { + session.tmuxSessionName = existingName; + sshLogger.info("User selected tmux session to attach", { + operation: "tmux_user_attach", + sessionName: existingName, + hostId: session.hostId, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_attached", + sessionName: existingName, + }), + ); + } else { + // New session from picker -- query name after startup + const sshConn = session.sshConn; + setTimeout(async () => { + const sessionName = sshConn + ? await queryNewestTmuxSession(sshConn) + : null; + session.tmuxSessionName = sessionName; + sshLogger.info("User requested new tmux session", { + operation: "tmux_user_create", + sessionName, + hostId: session.hostId, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_created", + sessionName, + }), + ); + }, 500); + } + } + break; + } + case "totp_response": { const totpData = data as TOTPResponseData; if (keyboardInteractiveFinish && totpData?.code) { @@ -1048,6 +1177,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } }, 120000); + // Resolve credentials server-side when frontend doesn't provide them let resolvedCredentials = { username, password, @@ -1057,94 +1187,52 @@ wss.on("connection", async (ws: WebSocket, req) => { authType, }; const authMethodNotAvailable = false; - if (credentialId && id) { - const hostRow = await getDb() - .select({ userId: hosts.userId }) - .from(hosts) - .where(eq(hosts.id, id)) - .limit(1); - const ownerId = hostRow[0]?.userId ?? null; - - if (ownerId && userId !== ownerId) { - try { - const { SharedCredentialManager } = - await import("../utils/shared-credential-manager.js"); - const sharedCredManager = SharedCredentialManager.getInstance(); - const sharedCred = await sharedCredManager.getSharedCredentialForUser( - id, - userId, + if (id && userId && !password && !key) { + try { + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById(id, userId); + if (resolvedHost) { + resolvedCredentials = { + username: resolvedHost.username || username, + password: resolvedHost.password, + key: resolvedHost.key, + keyPassword: resolvedHost.keyPassword, + keyType: resolvedHost.keyType, + authType: resolvedHost.authType, + }; + sendLog( + "auth", + "info", + "Credentials resolved from server-side host data", ); - - if (sharedCred) { - resolvedCredentials = { - username: sharedCred.username || username, - password: sharedCred.password, - key: sharedCred.key, - keyPassword: sharedCred.keyPassword, - keyType: sharedCred.keyType, - authType: sharedCred.authType, - }; - } else { - sshLogger.warn(`No shared credentials found for host ${id}`, { - operation: "ssh_credentials", - userId, - hostId: id, - }); - } - } catch (error) { - sshLogger.warn(`Failed to resolve shared credential for host ${id}`, { - operation: "ssh_credentials", - hostId: id, - error: error instanceof Error ? error.message : "Unknown error", - }); } - } else if (ownerId) { - try { - const credentials = await SimpleDBOps.select( - getDb() - .select() - .from(sshCredentials) - .where( - and( - eq(sshCredentials.id, credentialId), - eq(sshCredentials.userId, ownerId), - ), - ), - "ssh_credentials", - ownerId, - ); - - if (credentials.length > 0) { - const credential = credentials[0]; - resolvedCredentials = { - username: (credential.username as string | undefined) || username, - password: credential.password as string | undefined, - key: credential.privateKey as string | undefined, - keyPassword: credential.keyPassword as string | undefined, - keyType: credential.keyType as string | undefined, - authType: credential.authType as string | undefined, - }; - } else { - sshLogger.warn(`No credentials found for host ${id}`, { - operation: "ssh_credentials", - hostId: id, - credentialId, - userId: ownerId, - }); - } - } catch (error) { - sshLogger.warn(`Failed to resolve credentials for host ${id}`, { - operation: "ssh_credentials", - hostId: id, - credentialId, - error: error instanceof Error ? error.message : "Unknown error", - }); + } catch (error) { + sshLogger.warn(`Failed to resolve host credentials for ${id}`, { + operation: "ssh_credentials", + hostId: id, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } else if (credentialId && id && userId) { + try { + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById(id, userId); + if (resolvedHost) { + resolvedCredentials = { + username: resolvedHost.username || username, + password: resolvedHost.password, + key: resolvedHost.key, + keyPassword: resolvedHost.keyPassword, + keyType: resolvedHost.keyType, + authType: resolvedHost.authType, + }; } - } else { - sshLogger.warn("Missing userId for credential resolution in terminal", { + } catch (error) { + sshLogger.warn(`Failed to resolve credentials for host ${id}`, { operation: "ssh_credentials", hostId: id, credentialId, + error: error instanceof Error ? error.message : "Unknown error", }); } } @@ -1331,7 +1419,6 @@ wss.on("connection", async (ws: WebSocket, req) => { sshConn!, stream, lastJumpClient, - opksshTempFiles, ); sessionManager.attachWs(currentSessionId, userId, ws); @@ -1422,16 +1509,115 @@ wss.on("connection", async (ws: WebSocket, req) => { } }); - if (initialPath && initialPath.trim() !== "") { - const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}" && pwd\n`; - stream.write(cdCommand); - } + const autoTmux = hostConfig.terminalConfig?.autoTmux === true; - if (executeCommand && executeCommand.trim() !== "") { + // Helper to run initialPath/executeCommand after the shell + // (or tmux session) is ready + const runPostShellCommands = (delay: number) => { setTimeout(() => { - const command = `${executeCommand}\n`; - stream.write(command); - }, 500); + if (initialPath && initialPath.trim() !== "") { + const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}" && pwd\r`; + stream.write(cdCommand); + } + if (executeCommand && executeCommand.trim() !== "") { + setTimeout(() => { + stream.write(`${executeCommand}\r`); + }, 300); + } + }, delay); + }; + + if (autoTmux && conn) { + (async () => { + try { + const detection = await detectTmux(conn); + if (!detection.available) { + sshLogger.warn("tmux not found on remote host", { + operation: "tmux_detection", + hostId: id, + }); + ws.send( + JSON.stringify({ + type: "tmux_unavailable", + message: + "tmux is not installed on the remote host. Falling back to standard shell.", + }), + ); + // tmux unavailable, run commands in plain shell + runPostShellCommands(0); + } else if (detection.sessions.length === 0) { + attachOrCreateTmuxSession(stream); + // Query the name tmux assigned after a short delay + setTimeout(async () => { + const sessionName = await queryNewestTmuxSession(conn); + const session = sessionManager.getSession(boundSessionId); + if (session) { + session.tmuxSessionName = sessionName; + } + sshLogger.info("Created new tmux session", { + operation: "tmux_new_session", + sessionName, + hostId: id, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_created", + sessionName, + }), + ); + }, 500); + // Wait for tmux to start before running commands inside it + runPostShellCommands(500); + } else if (detection.sessions.length === 1) { + attachOrCreateTmuxSession(stream, detection.sessions[0].name); + const sessionName = detection.sessions[0].name; + const session = sessionManager.getSession(boundSessionId); + if (session) { + session.tmuxSessionName = sessionName; + } + sshLogger.info("Auto-attached to existing tmux session", { + operation: "tmux_auto_attach", + sessionName, + hostId: id, + }); + ws.send( + JSON.stringify({ + type: "tmux_session_attached", + sessionName, + }), + ); + // Reattaching to existing session -- don't re-run + // initialPath/executeCommand since the session already + // has its own state + } else { + sshLogger.info( + "Multiple tmux sessions found, sending list to frontend", + { + operation: "tmux_sessions_available", + sessions: detection.sessions, + hostId: id, + }, + ); + ws.send( + JSON.stringify({ + type: "tmux_sessions_available", + sessions: detection.sessions, + }), + ); + // Commands deferred until user picks a session + } + } catch (error) { + sshLogger.error("tmux detection failed", error, { + operation: "tmux_detection_error", + hostId: id, + }); + // Fallback: run commands in plain shell + runPostShellCommands(0); + } + })(); + } else { + // No tmux -- run commands directly as before + runPostShellCommands(0); } ws.send( @@ -1801,7 +1987,7 @@ wss.on("connection", async (ws: WebSocket, req) => { host: ip, port, username, - tryKeyboard: true, + tryKeyboard: resolvedCredentials.authType !== "none", keepaliveInterval: typeof hostKeepaliveInterval === "number" ? hostKeepaliveInterval @@ -1859,18 +2045,7 @@ wss.on("connection", async (ws: WebSocket, req) => { "ssh-rsa", "ssh-dss", ], - cipher: [ - "chacha20-poly1305@openssh.com", - "aes256-gcm@openssh.com", - "aes128-gcm@openssh.com", - "aes256-ctr", - "aes192-ctr", - "aes128-ctr", - "aes256-cbc", - "aes192-cbc", - "aes128-cbc", - "3des-cbc", - ], + cipher: SSH_ALGORITHMS.cipher, hmac: [ "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", @@ -1974,19 +2149,8 @@ wss.on("connection", async (ws: WebSocket, req) => { sendLog("auth", "info", "Using cached OPKSSH certificate"); - const { promises: fs } = await import("fs"); - const path = await import("path"); - const os = await import("os"); - - const tempDir = os.tmpdir(); - const keyPath = path.join(tempDir, `opkssh-${userId}-${id}`); - const certPath = `${keyPath}-cert.pub`; - - await fs.writeFile(keyPath, token.privateKey, { mode: 0o600 }); - await fs.writeFile(certPath, token.sshCert, { mode: 0o600 }); - - opksshTempFiles = { keyPath, certPath }; - connectConfig.privateKey = await fs.readFile(keyPath); + const { setupOPKSSHCertAuth } = await import("./opkssh-cert-auth.js"); + await setupOPKSSHCertAuth(connectConfig, sshConn, token, username); } catch (opksshError) { sshLogger.error("OPKSSH authentication error", opksshError, { operation: "opkssh_auth_error", @@ -2017,6 +2181,24 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } + if ( + hostConfig.portKnockSequence && + hostConfig.portKnockSequence.length > 0 + ) { + try { + sshLogger.info( + `Port knocking ${hostConfig.ip} (${hostConfig.portKnockSequence.length} ports)`, + { operation: "port_knock", hostId: hostConfig.id }, + ); + await performPortKnocking(hostConfig.ip, hostConfig.portKnockSequence); + } catch (err) { + sshLogger.warn("Port knocking failed, attempting connection anyway", { + operation: "port_knock", + hostId: hostConfig.id, + }); + } + } + const proxyConfig: SOCKS5Config | null = hostConfig.useSocks5 && (hostConfig.socks5Host || @@ -2171,6 +2353,7 @@ wss.on("connection", async (ws: WebSocket, req) => { } else { sendLog("handshake", "info", "Starting SSH session"); sendLog("auth", "info", `Authenticating as ${username}`); + sshLogger.info("Initiating SSH connection", { operation: "terminal_ssh_connect_attempt", sessionId, @@ -2219,7 +2402,6 @@ wss.on("connection", async (ws: WebSocket, req) => { sshStream = null; sshConn = null; lastJumpClient = null; - opksshTempFiles = null; resetConnectionState(); isCleaningUp = false; diff --git a/src/backend/ssh/tmux-helper.ts b/src/backend/ssh/tmux-helper.ts new file mode 100644 index 00000000..2f3023c4 --- /dev/null +++ b/src/backend/ssh/tmux-helper.ts @@ -0,0 +1,156 @@ +import type { Client, ClientChannel } from "ssh2"; +import { sshLogger } from "../utils/logger.js"; + +export interface TmuxSessionInfo { + name: string; + created: number; + lastActivity: number; + windows: number; + attachedClients: number; +} + +export interface TmuxDetectionResult { + available: boolean; + sessions: TmuxSessionInfo[]; +} + +/** + * Run a command on the remote host via a separate exec channel. + * Returns stdout as a string. Does not pollute the interactive shell. + */ +export function execCommand(conn: Client, command: string): Promise { + return new Promise((resolve, reject) => { + conn.exec(command, (err, stream) => { + if (err) { + reject(err); + return; + } + let stdout = ""; + let stderr = ""; + stream.on("data", (data: Buffer) => { + stdout += data.toString("utf-8"); + }); + stream.stderr.on("data", (data: Buffer) => { + stderr += data.toString("utf-8"); + }); + stream.on("error", (err: Error) => { + reject(err); + }); + stream.on("close", (code: number) => { + if (code !== 0 && stdout === "") { + reject( + new Error(stderr.trim() || `Command exited with code ${code}`), + ); + } else { + resolve(stdout.trim()); + } + }); + }); + }); +} + +/** + * Detect whether tmux is installed and list all existing sessions with details. + */ +export async function detectTmux(conn: Client): Promise { + try { + await execCommand(conn, "command -v tmux"); + } catch { + return { available: false, sessions: [] }; + } + + let sessions: TmuxSessionInfo[] = []; + try { + const output = await execCommand( + conn, + `tmux list-sessions -F "#{session_name}|#{session_created}|#{session_activity}|#{session_windows}|#{session_attached}" 2>/dev/null`, + ); + if (output) { + sessions = output + .split("\n") + .filter((line) => line.length > 0) + .map((line) => { + const [name, created, activity, windows, attached] = line.split("|"); + return { + name, + created: parseInt(created, 10) || 0, + lastActivity: parseInt(activity, 10) || 0, + windows: parseInt(windows, 10) || 0, + attachedClients: parseInt(attached, 10) || 0, + }; + }); + } + } catch { + // tmux server not running yet -- no sessions exist + } + + return { available: true, sessions }; +} + +// tmux options applied on every attach/create: +// - mouse on: enables mouse wheel / touch scrollback through tmux history +// - history-limit: deep scrollback buffer on the remote host +// - set-clipboard on: use OSC 52 to sync tmux selections to the client clipboard +// - mode-keys vi: use vi-style keys in copy mode +// - MouseDragEnd: stop the selection but keep it highlighted so the user can +// adjust and press Enter to copy (or drag again) +// - Enter: copy the (possibly adjusted) selection and exit copy mode +// - pane-mode-changed hook: on copy-mode entry, show a brief hint so users +// know to press Enter to copy the selection +// Using -q on set/set-hook to suppress errors on older tmux versions that don't support +// a particular option (e.g. set-clipboard on tmux < 2.5). Note: set-hook doesn't support -q. +const TMUX_OPTS = + `set -gq mouse on` + + ` \\; set -gq history-limit 50000` + + ` \\; set -gq set-clipboard on` + + ` \\; set -gq mode-keys vi` + + ` \\; bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X stop-selection` + + ` \\; bind-key -T copy-mode-vi Enter send-keys -X copy-selection-and-cancel` + + ` \\; set-hook -g pane-mode-changed` + + ` 'if -F "#{pane_in_mode}"` + + ` "display-message -d 2500 \\"Adjust selection and press Enter to copy\\""'`; + +/** + * Write tmux attach or new-session command to the interactive shell stream. + * Uses && exit so the shell only closes if tmux started successfully. + */ +export function attachOrCreateTmuxSession( + stream: ClientChannel, + existingSessionName?: string, +): void { + let command: string; + if (existingSessionName) { + command = `tmux ${TMUX_OPTS} \\; attach-session -t ${shellEscape(existingSessionName)} && exit\r`; + } else { + command = `tmux ${TMUX_OPTS} \\; new-session && exit\r`; + } + + sshLogger.info("Writing tmux command to shell", { + operation: "tmux_attach_or_create", + sessionName: existingSessionName || "(auto)", + isReattach: !!existingSessionName, + }); + + stream.write(command); +} + +/** + * Query the name of the most recently created tmux session via exec channel. + */ +export async function queryNewestTmuxSession( + conn: Client, +): Promise { + try { + const output = await execCommand( + conn, + `tmux list-sessions -F "#{session_created}:#{session_name}" 2>/dev/null | sort -rn | head -1 | cut -d: -f2-`, + ); + return output || null; + } catch { + return null; + } +} + +function shellEscape(s: string): string { + return "'" + s.replace(/'/g, "'\\''") + "'"; +} diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index b17a2eae..14fcddee 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -1,7 +1,8 @@ import express, { type Response } from "express"; -import cors from "cors"; +import { createCorsMiddleware } from "../utils/cors-config.js"; import cookieParser from "cookie-parser"; import { Client } from "ssh2"; +import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { ChildProcess } from "child_process"; import axios from "axios"; import { getDb } from "../database/db/index.js"; @@ -26,40 +27,7 @@ import { PermissionManager } from "../utils/permission-manager.js"; import { withConnection } from "./ssh-connection-pool.js"; const app = express(); -app.use( - cors({ - origin: (origin, callback) => { - if (!origin) return callback(null, true); - - const allowedOrigins = ["http://localhost:5173", "http://127.0.0.1:5173"]; - - if (allowedOrigins.includes(origin)) { - return callback(null, true); - } - - if (origin.startsWith("https://")) { - return callback(null, true); - } - - if (origin.startsWith("http://")) { - return callback(null, true); - } - - callback(new Error("Not allowed by CORS")); - }, - credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: [ - "Origin", - "X-Requested-With", - "Content-Type", - "Accept", - "Authorization", - "User-Agent", - "X-Electron-App", - ], - }), -); +app.use(createCorsMiddleware(["GET", "POST", "PUT", "DELETE", "OPTIONS"])); app.use(cookieParser()); app.use(express.json()); app.use((_req, res, next) => { @@ -546,7 +514,7 @@ async function connectSSHTunnel( tunnelConnecting.add(tunnelName); - cleanupTunnelResources(tunnelName, true); + await cleanupTunnelResources(tunnelName, true); if (retryAttempt === 0) { retryExhaustedTunnels.delete(tunnelName); @@ -602,70 +570,53 @@ async function connectSSHTunnel( const effectiveUserId = tunnelConfig.requestingUserId || tunnelConfig.sourceUserId; - if (tunnelConfig.sourceCredentialId && effectiveUserId) { + // Resolve source credentials server-side when not provided by frontend + if ( + tunnelConfig.sourceHostId && + effectiveUserId && + !tunnelConfig.sourcePassword && + !tunnelConfig.sourceSSHKey + ) { try { - if ( - tunnelConfig.requestingUserId && - tunnelConfig.requestingUserId !== tunnelConfig.sourceUserId - ) { - const { SharedCredentialManager } = - await import("../utils/shared-credential-manager.js"); - const sharedCredManager = SharedCredentialManager.getInstance(); - - if (tunnelConfig.sourceHostId) { - const sharedCred = await sharedCredManager.getSharedCredentialForUser( - tunnelConfig.sourceHostId, - tunnelConfig.requestingUserId, - ); - - if (sharedCred) { - resolvedSourceCredentials = { - password: sharedCred.password, - sshKey: sharedCred.key, - keyPassword: sharedCred.keyPassword, - keyType: sharedCred.keyType, - authMethod: sharedCred.authType, - }; - } else { - const errorMessage = `Cannot connect tunnel '${tunnelName}': shared credentials not available`; - tunnelLogger.error(errorMessage, undefined, { - operation: "tunnel_shared_credentials_unavailable", - tunnelName, - requestingUserId: tunnelConfig.requestingUserId, - sourceUserId: tunnelConfig.sourceUserId, - sourceHostId: tunnelConfig.sourceHostId, - }); - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: errorMessage, - }); - tunnelConnecting.delete(tunnelName); - return; - } - } - } else { - const userDataKey = DataCrypto.getUserDataKey(effectiveUserId); - if (userDataKey) { - const credentials = await SimpleDBOps.select( - getDb() - .select() - .from(sshCredentials) - .where(eq(sshCredentials.id, tunnelConfig.sourceCredentialId)), - "ssh_credentials", - effectiveUserId, - ); - - if (credentials.length > 0) { - const credential = credentials[0]; - resolvedSourceCredentials = { - password: credential.password as string | undefined, - sshKey: credential.privateKey as string | undefined, - keyPassword: credential.keyPassword as string | undefined, - keyType: credential.keyType as string | undefined, - authMethod: credential.authType as string, - }; - } + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById( + tunnelConfig.sourceHostId, + effectiveUserId, + ); + if (resolvedHost) { + resolvedSourceCredentials = { + password: resolvedHost.password, + sshKey: resolvedHost.key, + keyPassword: resolvedHost.keyPassword, + keyType: resolvedHost.keyType, + authMethod: resolvedHost.authType, + }; + } + } catch (error) { + tunnelLogger.warn("Failed to resolve source host credentials", { + operation: "tunnel_connect", + tunnelName, + sourceHostId: tunnelConfig.sourceHostId, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } else if (tunnelConfig.sourceCredentialId && effectiveUserId) { + // Legacy: credential resolution from credentialId + try { + if (tunnelConfig.sourceHostId) { + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById( + tunnelConfig.sourceHostId, + effectiveUserId, + ); + if (resolvedHost) { + resolvedSourceCredentials = { + password: resolvedHost.password, + sshKey: resolvedHost.key, + keyPassword: resolvedHost.keyPassword, + keyType: resolvedHost.keyType, + authMethod: resolvedHost.authType, + }; } } } catch (error) { @@ -1152,18 +1103,7 @@ async function connectSSHTunnel( "ssh-rsa", "ssh-dss", ], - cipher: [ - "chacha20-poly1305@openssh.com", - "aes256-gcm@openssh.com", - "aes128-gcm@openssh.com", - "aes256-ctr", - "aes192-ctr", - "aes128-ctr", - "aes256-cbc", - "aes192-cbc", - "aes128-cbc", - "3des-cbc", - ], + cipher: SSH_ALGORITHMS.cipher, hmac: [ "hmac-sha2-512-etm@openssh.com", "hmac-sha2-256-etm@openssh.com", @@ -1328,34 +1268,31 @@ async function killRemoteTunnelByMarker( authMethod: tunnelConfig.sourceAuthMethod, }; - if (tunnelConfig.sourceCredentialId && tunnelConfig.sourceUserId) { + if ( + tunnelConfig.sourceHostId && + tunnelConfig.sourceUserId && + !tunnelConfig.sourcePassword && + !tunnelConfig.sourceSSHKey + ) { try { - const userDataKey = DataCrypto.getUserDataKey(tunnelConfig.sourceUserId); - if (userDataKey) { - const credentials = await SimpleDBOps.select( - getDb() - .select() - .from(sshCredentials) - .where(eq(sshCredentials.id, tunnelConfig.sourceCredentialId)), - "ssh_credentials", - tunnelConfig.sourceUserId, - ); - - if (credentials.length > 0) { - const credential = credentials[0]; - resolvedSourceCredentials = { - password: credential.password as string | undefined, - sshKey: credential.privateKey as string | undefined, - keyPassword: credential.keyPassword as string | undefined, - keyType: credential.keyType as string | undefined, - authMethod: credential.authType as string, - }; - } + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById( + tunnelConfig.sourceHostId, + tunnelConfig.sourceUserId, + ); + if (resolvedHost) { + resolvedSourceCredentials = { + password: resolvedHost.password, + sshKey: resolvedHost.key, + keyPassword: resolvedHost.keyPassword, + keyType: resolvedHost.keyType, + authMethod: resolvedHost.authType, + }; } } catch (error) { tunnelLogger.warn("Failed to resolve source credentials for cleanup", { tunnelName, - credentialId: tunnelConfig.sourceCredentialId, + sourceHostId: tunnelConfig.sourceHostId, error: error instanceof Error ? error.message : "Unknown error", }); } @@ -1774,13 +1711,38 @@ app.post( tunnelConfig.endpointIP = endpointHost.ip; tunnelConfig.endpointSSHPort = endpointHost.port; tunnelConfig.endpointUsername = endpointHost.username; - tunnelConfig.endpointPassword = endpointHost.password; tunnelConfig.endpointAuthMethod = endpointHost.authType; - tunnelConfig.endpointSSHKey = endpointHost.key; - tunnelConfig.endpointKeyPassword = endpointHost.keyPassword; tunnelConfig.endpointKeyType = endpointHost.keyType; tunnelConfig.endpointCredentialId = endpointHost.credentialId; tunnelConfig.endpointUserId = endpointHost.userId; + + // Resolve credentials server-side instead of from HTTP response + if (endpointHost.id && endpointHost.userId) { + try { + const { resolveHostById } = await import("./host-resolver.js"); + const resolved = await resolveHostById( + endpointHost.id, + endpointHost.userId, + ); + if (resolved) { + tunnelConfig.endpointPassword = resolved.password; + tunnelConfig.endpointSSHKey = resolved.key; + tunnelConfig.endpointKeyPassword = resolved.keyPassword; + } + } catch (credError) { + tunnelLogger.warn( + "Failed to resolve endpoint credentials from DB", + { + operation: "tunnel_endpoint_credential_resolve", + endpointHostId: endpointHost.id, + error: + credError instanceof Error + ? credError.message + : "Unknown", + }, + ); + } + } } catch (resolveError) { tunnelLogger.error( "Failed to resolve endpoint host", @@ -2082,11 +2044,7 @@ async function initializeAutoStartTunnels(): Promise { sourceIP: host.ip, sourceSSHPort: host.port, sourceUsername: host.username, - sourcePassword: host.autostartPassword || host.password, sourceAuthMethod: host.authType, - sourceSSHKey: host.autostartKey || host.key, - sourceKeyPassword: - host.autostartKeyPassword || host.keyPassword, sourceKeyType: host.keyType, sourceCredentialId: host.credentialId, sourceUserId: host.userId, @@ -2094,20 +2052,8 @@ async function initializeAutoStartTunnels(): Promise { endpointSSHPort: endpointHost.port, endpointUsername: endpointHost.username, endpointHost: tunnelConnection.endpointHost, - endpointPassword: - tunnelConnection.endpointPassword || - endpointHost.autostartPassword || - endpointHost.password, endpointAuthMethod: tunnelConnection.endpointAuthType || endpointHost.authType, - endpointSSHKey: - tunnelConnection.endpointKey || - endpointHost.autostartKey || - endpointHost.key, - endpointKeyPassword: - tunnelConnection.endpointKeyPassword || - endpointHost.autostartKeyPassword || - endpointHost.keyPassword, endpointKeyType: tunnelConnection.endpointKeyType || endpointHost.keyType, endpointCredentialId: endpointHost.credentialId, diff --git a/src/backend/ssh/widgets/login-stats-collector.ts b/src/backend/ssh/widgets/login-stats-collector.ts index ccb2befe..3fd02e3a 100644 --- a/src/backend/ssh/widgets/login-stats-collector.ts +++ b/src/backend/ssh/widgets/login-stats-collector.ts @@ -50,10 +50,10 @@ export async function collectLoginStats(client: Client): Promise { try { const date = new Date(timeStr); parsedTime = isNaN(date.getTime()) - ? new Date().toISOString() + ? timeStr || "unknown" : date.toISOString(); } catch { - parsedTime = new Date().toISOString(); + parsedTime = timeStr || "unknown"; } recentLogins.push({ @@ -99,21 +99,30 @@ export async function collectLoginStats(client: Client): Promise { ip = ipMatch[1]; } - const dateMatch = line.match(/^(\w+\s+\d+\s+\d+:\d+:\d+)/); + const dateMatch = line.match(/^(\w+)\s+(\d+)\s+(\d+:\d+:\d+)/); if (dateMatch) { - const currentYear = new Date().getFullYear(); - timeStr = `${currentYear} ${dateMatch[1]}`; + const [, month, day, time] = dateMatch; + const now = new Date(); + const currentYear = now.getFullYear(); + const candidate = new Date(`${month} ${day}, ${currentYear} ${time}`); + if (!isNaN(candidate.getTime()) && candidate > now) { + // If parsed date is in the future, it's from last year + timeStr = `${month} ${day}, ${currentYear - 1} ${time}`; + } else { + timeStr = `${month} ${day}, ${currentYear} ${time}`; + } } if (user && ip) { let parsedTime: string; try { - const date = timeStr ? new Date(timeStr) : new Date(); - parsedTime = isNaN(date.getTime()) - ? new Date().toISOString() - : date.toISOString(); + const date = timeStr ? new Date(timeStr) : null; + parsedTime = + date && !isNaN(date.getTime()) + ? date.toISOString() + : timeStr || "unknown"; } catch { - parsedTime = new Date().toISOString(); + parsedTime = timeStr || "unknown"; } failedLogins.push({ diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 8d566eca..3a82dda7 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -7,7 +7,11 @@ import { AutoSSLSetup } from "./utils/auto-ssl-setup.js"; import { AuthManager } from "./utils/auth-manager.js"; import { DataCrypto } from "./utils/data-crypto.js"; import { SystemCrypto } from "./utils/system-crypto.js"; -import { systemLogger, versionLogger } from "./utils/logger.js"; +import { + systemLogger, + versionLogger, + setGlobalLogLevel, +} from "./utils/logger.js"; (async () => { const initStartTime = Date.now(); @@ -112,28 +116,27 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; await authManager.initialize(); DataCrypto.initialize(); - const { OPKSSHBinaryManager } = - await import("./utils/opkssh-binary-manager.js"); - try { - await OPKSSHBinaryManager.ensureBinary(); - } catch (error) { - const dataDir = - process.env.DATA_DIR || path.join(process.cwd(), "db", "data"); - systemLogger.warn( - "Failed to initialize OPKSSH binary - OPKSSH authentication will not be available", - { - operation: "opkssh_binary_init_failed", - error: error instanceof Error ? error.message : "Unknown error", - stack: error instanceof Error ? error.stack : undefined, - platform: process.platform, - arch: process.arch, - dataDir, - }, - ); - } + import("./utils/opkssh-binary-manager.js").then( + ({ OPKSSHBinaryManager }) => { + OPKSSHBinaryManager.ensureBinary().catch((error) => { + const dataDir = + process.env.DATA_DIR || path.join(process.cwd(), "db", "data"); + systemLogger.warn( + "Failed to initialize OPKSSH binary - OPKSSH authentication will not be available", + { + operation: "opkssh_binary_init_failed", + error: error instanceof Error ? error.message : "Unknown error", + stack: error instanceof Error ? error.stack : undefined, + platform: process.platform, + arch: process.arch, + dataDir, + }, + ); + }); + }, + ); await import("./database/database.js"); - await import("./ssh/terminal.js"); await import("./ssh/tunnel.js"); await import("./ssh/file-manager.js"); @@ -142,6 +145,19 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; await import("./ssh/docker-console.js"); await import("./dashboard.js"); + // Initialize log level from database settings + const { getDb: getDbForSettings } = await import("./database/db/index.js"); + const settingsDb = getDbForSettings(); + const logLevelRow = settingsDb.$client + .prepare("SELECT value FROM settings WHERE key = 'log_level'") + .get() as { value: string } | undefined; + if (logLevelRow) { + setGlobalLogLevel(logLevelRow.value); + systemLogger.info(`Log level set to: ${logLevelRow.value}`, { + operation: "log_level_init", + }); + } + // Initialize Guacamole server for RDP/VNC/Telnet support const { getDb: getDbForGuac } = await import("./database/db/index.js"); const guacDb = getDbForGuac(); @@ -153,20 +169,21 @@ import { systemLogger, versionLogger } from "./utils/logger.js"; : true; if (process.env.ENABLE_GUACAMOLE !== "false" && guacEnabled) { - try { - await import("./guacamole/guacamole-server.js"); - systemLogger.info("Guacamole server initialized", { - operation: "guac_init", + import("./guacamole/guacamole-server.js") + .then(() => { + systemLogger.info("Guacamole server initialized", { + operation: "guac_init", + }); + }) + .catch((error) => { + systemLogger.warn( + "Failed to initialize Guacamole server (guacd may not be available)", + { + operation: "guac_init_skip", + error: error instanceof Error ? error.message : "Unknown error", + }, + ); }); - } catch (error) { - systemLogger.warn( - "Failed to initialize Guacamole server (guacd may not be available)", - { - operation: "guac_init_skip", - error: error instanceof Error ? error.message : "Unknown error", - }, - ); - } } systemLogger.success("Termix backend started successfully", { diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 73773854..87d4669c 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -4,7 +4,11 @@ import { SystemCrypto } from "./system-crypto.js"; import { DataCrypto } from "./data-crypto.js"; import { databaseLogger, authLogger } from "./logger.js"; import type { Request, Response, NextFunction } from "express"; -import { db } from "../database/db/index.js"; +import { + db, + getSqlite, + saveMemoryDatabaseToFile, +} from "../database/db/index.js"; import { sessions, trustedDevices } from "../database/db/schema.js"; import { eq, and, sql } from "drizzle-orm"; import { nanoid } from "nanoid"; @@ -206,15 +210,20 @@ class AuthManager { ): Promise { const jwtSecret = await this.systemCrypto.getJWTSecret(); + const timeoutRow = db.$client + .prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'") + .get() as { value: string } | undefined; + const defaultExpiry = `${timeoutRow ? parseInt(timeoutRow.value, 10) || 24 : 24}h`; + let expiresIn = options.expiresIn; if (!expiresIn && !options.pendingTOTP) { if (options.rememberMe) { expiresIn = "30d"; } else { - expiresIn = "24h"; + expiresIn = defaultExpiry; } } else if (!expiresIn) { - expiresIn = "24h"; + expiresIn = defaultExpiry; } const payload: JWTPayload = { userId }; @@ -567,6 +576,13 @@ class AuthManager { return res.status(401).json({ error: "Invalid token" }); } + if (payload.pendingTOTP) { + return res.status(401).json({ + error: "TOTP verification required", + code: "TOTP_REQUIRED", + }); + } + if (payload.sessionId) { try { const sessionRecords = await db @@ -708,6 +724,13 @@ class AuthManager { return res.status(401).json({ error: "Invalid token" }); } + if (payload.pendingTOTP) { + return res.status(401).json({ + error: "TOTP verification required", + code: "TOTP_REQUIRED", + }); + } + try { const { db } = await import("../database/db/index.js"); const { users } = await import("../database/db/schema.js"); @@ -785,6 +808,22 @@ class AuthManager { }); } } else { + try { + await db.delete(sessions).where(eq(sessions.userId, userId)); + + try { + const { saveMemoryDatabaseToFile } = + await import("../database/db/index.js"); + await saveMemoryDatabaseToFile(); + } catch { + // best effort + } + } catch (error) { + databaseLogger.error("Failed to revoke all sessions on logout", error, { + operation: "session_revoke_all_failed", + userId, + }); + } this.userCrypto.logoutUser(userId); } } diff --git a/src/backend/utils/cors-config.ts b/src/backend/utils/cors-config.ts new file mode 100644 index 00000000..07416a6c --- /dev/null +++ b/src/backend/utils/cors-config.ts @@ -0,0 +1,71 @@ +import cors from "cors"; +import type { Request, Response, NextFunction } from "express"; +import { getRequestOrigin } from "./request-origin.js"; + +const DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"]; +const ELECTRON_FILE_ORIGIN = "file://"; + +function getAllowedOrigins(): string[] { + const envOrigins = process.env.CORS_ALLOWED_ORIGINS; + if (!envOrigins) return []; + return envOrigins + .split(",") + .map((o) => o.trim()) + .filter(Boolean); +} + +function isLocalRequest(req: Request): boolean { + const remoteAddr = req.socket?.remoteAddress || req.ip || ""; + return ( + remoteAddr === "127.0.0.1" || + remoteAddr === "::1" || + remoteAddr === "::ffff:127.0.0.1" + ); +} + +export function createCorsMiddleware( + methods: string[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + extraHeaders: string[] = [], +) { + const allowedHeaders = [ + "Origin", + "X-Requested-With", + "Content-Type", + "Accept", + "Authorization", + "User-Agent", + "X-Electron-App", + "Cache-Control", + ...extraHeaders, + ]; + + return (req: Request, res: Response, next: NextFunction) => { + const handler = cors({ + origin: (origin, callback) => { + // No origin = same-origin or non-browser request (curl, internal service calls) + if (!origin) return callback(null, true); + + // Requests coming from localhost (nginx proxy, internal service calls) + if (isLocalRequest(req)) return callback(null, true); + + if (DEV_ORIGINS.includes(origin)) return callback(null, true); + if (origin.startsWith(ELECTRON_FILE_ORIGIN)) + return callback(null, true); + + const configured = getAllowedOrigins(); + if (configured.length === 0) return callback(null, true); + if (configured.includes("*") || configured.includes(origin)) + return callback(null, true); + + const sameOrigin = getRequestOrigin(req); + if (origin === sameOrigin) return callback(null, true); + + callback(new Error("Not allowed by CORS")); + }, + credentials: true, + methods, + allowedHeaders, + }); + handler(req, res, next); + }; +} diff --git a/src/backend/utils/credential-system-encryption-migration.ts b/src/backend/utils/credential-system-encryption-migration.ts index 4dd3008f..5fb48e92 100644 --- a/src/backend/utils/credential-system-encryption-migration.ts +++ b/src/backend/utils/credential-system-encryption-migration.ts @@ -107,24 +107,24 @@ export class CredentialSystemEncryptionMigration { migrated++; } catch (error) { - databaseLogger.error("Failed to migrate credential", error, { - credentialId: cred.id, - userId, - }); + databaseLogger.warn( + `Skipping credential migration for credential ${cred.id}: ${error instanceof Error ? error.message : "Unknown error"}`, + { + operation: "credential_migration_skip", + credentialId: cred.id, + userId, + }, + ); failed++; } } return { migrated, failed, skipped }; } catch (error) { - databaseLogger.error( - "Credential system encryption migration failed", - error, - { - operation: "credential_migration_failed", - userId, - error: error instanceof Error ? error.message : "Unknown error", - }, - ); + databaseLogger.warn("Credential system encryption migration incomplete", { + operation: "credential_migration_incomplete", + userId, + error: error instanceof Error ? error.message : "Unknown error", + }); throw error; } } diff --git a/src/backend/utils/logger.ts b/src/backend/utils/logger.ts index b15d45d2..c287502b 100644 --- a/src/backend/utils/logger.ts +++ b/src/backend/utils/logger.ts @@ -2,6 +2,33 @@ import chalk from "chalk"; export type LogLevel = "debug" | "info" | "warn" | "error" | "success"; +const LOG_LEVEL_PRIORITY: Record = { + debug: 0, + info: 1, + success: 1, + warn: 2, + error: 3, +}; + +let globalLogLevel: LogLevel = "info"; + +export function setGlobalLogLevel(level: string): void { + const normalized = level.toLowerCase(); + if (normalized in LOG_LEVEL_PRIORITY) { + globalLogLevel = normalized as LogLevel; + } +} + +export function getGlobalLogLevel(): LogLevel { + return globalLogLevel; +} + +// Initialize from environment variable if set +const envLogLevel = process.env.LOG_LEVEL?.toLowerCase(); +if (envLogLevel && envLogLevel in LOG_LEVEL_PRIORITY) { + globalLogLevel = envLogLevel as LogLevel; +} + export interface LogContext { service?: string; operation?: string; @@ -140,7 +167,7 @@ export class Logger { } private shouldLog(level: LogLevel, message: string): boolean { - if (level === "debug" && process.env.NODE_ENV === "production") { + if (LOG_LEVEL_PRIORITY[level] < LOG_LEVEL_PRIORITY[globalLogLevel]) { return false; } diff --git a/src/backend/utils/opkssh-binary-manager.ts b/src/backend/utils/opkssh-binary-manager.ts index dd55ed0f..a3438192 100644 --- a/src/backend/utils/opkssh-binary-manager.ts +++ b/src/backend/utils/opkssh-binary-manager.ts @@ -168,7 +168,7 @@ export class OPKSSHBinaryManager { const osMap: Record = { win32: "windows", linux: "linux", - darwin: "darwin", + darwin: "osx", }; const archMap: Record = { @@ -209,7 +209,7 @@ export class OPKSSHBinaryManager { const osMap: Record = { win32: "windows", linux: "linux", - darwin: "darwin", + darwin: "osx", }; const archMap: Record = { diff --git a/src/backend/utils/request-origin.ts b/src/backend/utils/request-origin.ts index eca003fb..ea0a530f 100644 --- a/src/backend/utils/request-origin.ts +++ b/src/backend/utils/request-origin.ts @@ -6,10 +6,12 @@ export function getRequestOrigin(req: Request | IncomingMessage): string { const protoHeader = req.headers["x-forwarded-proto"]; if (protoHeader) { - protocol = + const raw = typeof protoHeader === "string" ? protoHeader.split(",")[0].trim() : protoHeader[0]; + // Normalize WebSocket protocols to their HTTP equivalents + protocol = raw === "wss" ? "https" : raw === "ws" ? "http" : raw; } else if ("protocol" in req && req.protocol) { protocol = req.protocol; } else { diff --git a/src/backend/utils/shared-credential-manager.ts b/src/backend/utils/shared-credential-manager.ts index 261ffa1a..dad95bc1 100644 --- a/src/backend/utils/shared-credential-manager.ts +++ b/src/backend/utils/shared-credential-manager.ts @@ -379,7 +379,7 @@ class SharedCredentialManager { cred.keyPassword, ownerDEK, credentialId, - "key_password", + "keyPassword", ) : undefined, keyType: cred.keyType, diff --git a/src/backend/utils/ssh-algorithms.ts b/src/backend/utils/ssh-algorithms.ts new file mode 100644 index 00000000..d2d5b273 --- /dev/null +++ b/src/backend/utils/ssh-algorithms.ts @@ -0,0 +1,71 @@ +import crypto from "crypto"; +import type { ConnectConfig, CipherAlgorithm } from "ssh2"; + +// Maps SSH cipher names to their OpenSSL equivalents (as used by ssh2 internally) +const SSH_CIPHER_SSL_NAME: Partial> = { + "chacha20-poly1305@openssh.com": "chacha20", + "aes256-gcm@openssh.com": "aes-256-gcm", + "aes128-gcm@openssh.com": "aes-128-gcm", + "aes256-ctr": "aes-256-ctr", + "aes192-ctr": "aes-192-ctr", + "aes128-ctr": "aes-128-ctr", + "aes256-cbc": "aes-256-cbc", + "aes192-cbc": "aes-192-cbc", + "aes128-cbc": "aes-128-cbc", + "3des-cbc": "des-ede3-cbc", +}; + +const availableCiphers = new Set(crypto.getCiphers()); + +function filterCiphers(list: CipherAlgorithm[]): CipherAlgorithm[] { + return list.filter((name) => { + const sslName = SSH_CIPHER_SSL_NAME[name]; + return !sslName || availableCiphers.has(sslName); + }); +} + +export const SSH_ALGORITHMS: NonNullable = { + kex: [ + "curve25519-sha256", + "curve25519-sha256@libssh.org", + "ecdh-sha2-nistp521", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp256", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group14-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group1-sha1", + ], + serverHostKey: [ + "ssh-ed25519", + "ecdsa-sha2-nistp521", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp256", + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + "ssh-dss", + ], + cipher: filterCiphers([ + "chacha20-poly1305@openssh.com", + "aes256-gcm@openssh.com", + "aes128-gcm@openssh.com", + "aes256-ctr", + "aes192-ctr", + "aes128-ctr", + "aes256-cbc", + "aes192-cbc", + "aes128-cbc", + "3des-cbc", + ]), + hmac: [ + "hmac-sha2-512-etm@openssh.com", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512", + "hmac-sha2-256", + "hmac-sha1", + "hmac-md5", + ], + compress: ["none", "zlib@openssh.com", "zlib"], +}; diff --git a/src/backend/utils/wake-on-lan.ts b/src/backend/utils/wake-on-lan.ts new file mode 100644 index 00000000..2697b35e --- /dev/null +++ b/src/backend/utils/wake-on-lan.ts @@ -0,0 +1,46 @@ +import dgram from "dgram"; + +const MAC_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/; + +function parseMac(mac: string): Buffer { + return Buffer.from(mac.replace(/[:-]/g, ""), "hex"); +} + +function buildMagicPacket(mac: string): Buffer { + const macBytes = parseMac(mac); + const packet = Buffer.alloc(102); + packet.fill(0xff, 0, 6); + for (let i = 0; i < 16; i++) { + macBytes.copy(packet, 6 + i * 6); + } + return packet; +} + +export function isValidMac(mac: string): boolean { + return MAC_REGEX.test(mac); +} + +export function sendWakeOnLan(mac: string): Promise { + return new Promise((resolve, reject) => { + if (!isValidMac(mac)) { + return reject(new Error("Invalid MAC address")); + } + + const packet = buildMagicPacket(mac); + const socket = dgram.createSocket("udp4"); + + socket.once("error", (err) => { + socket.close(); + reject(err); + }); + + socket.bind(() => { + socket.setBroadcast(true); + socket.send(packet, 0, packet.length, 9, "255.255.255.255", (err) => { + socket.close(); + if (err) reject(err); + else resolve(); + }); + }); + }); +} diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 93e2f18c..ba8b8e24 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,7 +1,14 @@ /* eslint-disable react-refresh/only-export-components */ import { createContext, useContext, useEffect, useState } from "react"; -type Theme = "dark" | "light" | "system"; +type Theme = + | "dark" + | "light" + | "system" + | "dracula" + | "gentlemansChoice" + | "midnightEspresso" + | "catppuccinMocha"; type ThemeProviderProps = { children: React.ReactNode; @@ -12,11 +19,13 @@ type ThemeProviderProps = { type ThemeProviderState = { theme: Theme; setTheme: (theme: Theme) => void; + setThemePreview: (theme: Theme | null) => void; }; const initialState: ThemeProviderState = { theme: "system", setTheme: () => null, + setThemePreview: () => null, }; const ThemeProviderContext = createContext(initialState); @@ -30,13 +39,23 @@ export function ThemeProvider({ const [theme, setTheme] = useState( () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, ); + const [previewTheme, setPreviewTheme] = useState(null); useEffect(() => { const root = window.document.documentElement; - root.classList.remove("light", "dark"); + root.classList.remove( + "light", + "dark", + "dracula", + "gentlemansChoice", + "midnightEspresso", + "catppuccinMocha", + ); - if (theme === "system") { + const activeTheme = previewTheme || theme; + + if (activeTheme === "system") { const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") .matches ? "dark" @@ -46,8 +65,18 @@ export function ThemeProvider({ return; } - root.classList.add(theme); - }, [theme]); + root.classList.add(activeTheme); + + const darkCustomThemes: Theme[] = [ + "dracula", + "gentlemansChoice", + "midnightEspresso", + "catppuccinMocha", + ]; + if (darkCustomThemes.includes(activeTheme)) { + root.classList.add("dark"); + } + }, [theme, previewTheme]); const value = { theme, @@ -55,6 +84,9 @@ export function ThemeProvider({ localStorage.setItem(storageKey, theme); setTheme(theme); }, + setThemePreview: (theme: Theme | null) => { + setPreviewTheme(theme); + }, }; return ( diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 264f0503..4c7e3c57 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -39,9 +39,19 @@ const Toaster = ({ ...props }: ToasterProps) => { message: rateLimitedToast, }); + const darkCustomThemes = [ + "dracula", + "gentlemansChoice", + "midnightEspresso", + "catppuccinMocha", + ]; + const sonnerTheme: ToasterProps["theme"] = darkCustomThemes.includes(theme) + ? "dark" + : (theme as ToasterProps["theme"]); + return ( = { brightWhite: "#a6adc8", }, }, + + gentlemansChoice: { + name: "Gentleman's Choice", + category: "dark", + colors: { + background: "#1a1c1a", + foreground: "#d1c7a3", + cursor: "#d1c7a3", + cursorAccent: "#1a1c1a", + selectionBackground: "#3e4437", + black: "#1a1c1a", + red: "#9d3a3a", + green: "#5a7a3a", + yellow: "#b39a3a", + blue: "#3a5a7a", + magenta: "#7a3a5a", + cyan: "#3a7a7a", + white: "#d1c7a3", + brightBlack: "#3e4437", + brightRed: "#bf4a4a", + brightGreen: "#7a9a4a", + brightYellow: "#d1b34a", + brightBlue: "#4a7abf", + brightMagenta: "#9a4abf", + brightCyan: "#4abfbf", + brightWhite: "#e3dbc3", + }, + }, + + midnightEspresso: { + name: "Midnight Espresso", + category: "dark", + colors: { + background: "#120f0d", + foreground: "#ceb195", + cursor: "#ceb195", + cursorAccent: "#120f0d", + selectionBackground: "#3d2b1f", + black: "#120f0d", + red: "#a05a4a", + green: "#7a8a5a", + yellow: "#b08a4a", + blue: "#5a7a9a", + magenta: "#8a5a7a", + cyan: "#5a8a8a", + white: "#ceb195", + brightBlack: "#3d2b1f", + brightRed: "#c07a6a", + brightGreen: "#9aaa7a", + brightYellow: "#d0aa6a", + brightBlue: "#7a9aba", + brightMagenta: "#aa7aba", + brightCyan: "#7ababa", + brightWhite: "#e0cbb5", + }, + }, }; export const TERMINAL_FONTS = [ @@ -764,6 +820,7 @@ export const DEFAULT_TERMINAL_CONFIG = { sudoPasswordAutoFill: false, keepaliveInterval: undefined as number | undefined, keepaliveCountMax: undefined as number | undefined, + autoTmux: false, }; export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG; diff --git a/src/index.css b/src/index.css index c5e0cf53..04a5e82b 100644 --- a/src/index.css +++ b/src/index.css @@ -233,6 +233,266 @@ --bg-overlay: rgba(0, 0, 0, 0.7); } +.dracula { + --background: #282a36; + --foreground: #ffffff; + --card: #343746; + --card-foreground: #ffffff; + --popover: #343746; + --popover-foreground: #ffffff; + --primary: #ffffff; + --primary-foreground: #282a36; + --secondary: #44475a; + --secondary-foreground: #ffffff; + --muted: #343746; + --muted-foreground: #a6accd; + --accent: #44475a; + --accent-foreground: #ffffff; + --destructive: #ff5555; + --border: #44475a; + --input: #343746; + --ring: #8be9fd; + --chart-1: #8be9fd; + --chart-2: #50fa7b; + --chart-3: #ffb86c; + --chart-4: #bd93f9; + --chart-5: #ff79c6; + --sidebar: #1e1f29; + --sidebar-foreground: #ffffff; + --sidebar-primary: #8be9fd; + --sidebar-primary-foreground: #1e1f29; + --sidebar-accent: #44475a; + --sidebar-accent-foreground: #ffffff; + --sidebar-border: #44475a; + --sidebar-ring: #8be9fd; + + --bg-base: #282a36; + --bg-elevated: #343746; + --bg-surface: #343746; + --bg-surface-hover: #44475a; + --bg-input: #343746; + --bg-deepest: #1e1f29; + --bg-header: #1e1f29; + --bg-button: #343746; + --bg-active: #44475a; + --bg-light: #343746; + --bg-subtle: #1e1f29; + --bg-interact: #44475a; + --border-base: #44475a; + --border-panel: #1e1f29; + --border-subtle: #44475a; + --border-medium: #44475a; + --bg-hover: #44475a; + --bg-hover-alt: #44475a; + --bg-pressed: #1e1f29; + --border-hover: #6272a4; + --border-active: #8be9fd; + + --foreground-secondary: #a6accd; + --foreground-subtle: #6272a4; + + --scrollbar-thumb: #44475a; + --scrollbar-thumb-hover: #6272a4; + --scrollbar-track: #282a36; + + --bg-overlay: rgba(0, 0, 0, 0.8); +} + +.gentlemansChoice { + --background: #1a1c1a; + --foreground: #d1c7a3; + --card: #2a2c2a; + --card-foreground: #d1c7a3; + --popover: #2a2c2a; + --popover-foreground: #d1c7a3; + --primary: #d1c7a3; + --primary-foreground: #1a1c1a; + --secondary: #3e4437; + --secondary-foreground: #d1c7a3; + --muted: #2a2c2a; + --muted-foreground: #8e8463; + --accent: #3e4437; + --accent-foreground: #d1c7a3; + --destructive: #9d3a3a; + --border: #3e4437; + --input: #2a2c2a; + --ring: #5a7a3a; + --chart-1: #5a7a3a; + --chart-2: #3a5a7a; + --chart-3: #b39a3a; + --chart-4: #7a3a5a; + --chart-5: #3a7a7a; + --sidebar: #151715; + --sidebar-foreground: #d1c7a3; + --sidebar-primary: #5a7a3a; + --sidebar-primary-foreground: #151715; + --sidebar-accent: #3e4437; + --sidebar-accent-foreground: #d1c7a3; + --sidebar-border: #3e4437; + --sidebar-ring: #5a7a3a; + + --bg-base: #1a1c1a; + --bg-elevated: #2a2c2a; + --bg-surface: #2a2c2a; + --bg-surface-hover: #3e4437; + --bg-input: #2a2c2a; + --bg-deepest: #151715; + --bg-header: #151715; + --bg-button: #2a2c2a; + --bg-active: #3e4437; + --bg-light: #2a2c2a; + --bg-subtle: #151715; + --bg-interact: #3e4437; + --border-base: #3e4437; + --border-panel: #151715; + --border-subtle: #3e4437; + --border-medium: #3e4437; + --bg-hover: #3e4437; + --bg-hover-alt: #3e4437; + --bg-pressed: #151715; + --border-hover: #5a7a3a; + --border-active: #d1c7a3; + + --foreground-secondary: #8e8463; + --foreground-subtle: #5e5841; + + --scrollbar-thumb: #3e4437; + --scrollbar-thumb-hover: #5e5841; + --scrollbar-track: #1a1c1a; + + --bg-overlay: rgba(0, 0, 0, 0.8); +} + +.midnightEspresso { + --background: #120f0d; + --foreground: #ceb195; + --card: #1f1a17; + --card-foreground: #ceb195; + --popover: #1f1a17; + --popover-foreground: #ceb195; + --primary: #ceb195; + --primary-foreground: #120f0d; + --secondary: #3d2b1f; + --secondary-foreground: #ceb195; + --muted: #1f1a17; + --muted-foreground: #9a8a7a; + --accent: #3d2b1f; + --accent-foreground: #ceb195; + --destructive: #a05a4a; + --border: #3d2b1f; + --input: #1f1a17; + --ring: #7a8a5a; + --chart-1: #7a8a5a; + --chart-2: #5a7a9a; + --chart-3: #b08a4a; + --chart-4: #8a5a7a; + --chart-5: #5a8a8a; + --sidebar: #0d0b0a; + --sidebar-foreground: #ceb195; + --sidebar-primary: #7a8a5a; + --sidebar-primary-foreground: #0d0b0a; + --sidebar-accent: #3d2b1f; + --sidebar-accent-foreground: #ceb195; + --sidebar-border: #3d2b1f; + --sidebar-ring: #7a8a5a; + + --bg-base: #120f0d; + --bg-elevated: #1f1a17; + --bg-surface: #1f1a17; + --bg-surface-hover: #3d2b1f; + --bg-input: #1f1a17; + --bg-deepest: #0d0b0a; + --bg-header: #0d0b0a; + --bg-button: #1f1a17; + --bg-active: #3d2b1f; + --bg-light: #1f1a17; + --bg-subtle: #0d0b0a; + --bg-interact: #3d2b1f; + --border-base: #3d2b1f; + --border-panel: #0d0b0a; + --border-subtle: #3d2b1f; + --border-medium: #3d2b1f; + --bg-hover: #3d2b1f; + --bg-hover-alt: #3d2b1f; + --bg-pressed: #0d0b0a; + --border-hover: #7a8a5a; + --border-active: #ceb195; + + --foreground-secondary: #9a8a7a; + --foreground-subtle: #6a5a4a; + + --scrollbar-thumb: #3d2b1f; + --scrollbar-thumb-hover: #6a5a4a; + --scrollbar-track: #120f0d; + + --bg-overlay: rgba(0, 0, 0, 0.8); +} + +.catppuccinMocha { + --background: #1e1e2e; + --foreground: #cdd6f4; + --card: #181825; + --card-foreground: #cdd6f4; + --popover: #181825; + --popover-foreground: #cdd6f4; + --primary: #cdd6f4; + --primary-foreground: #1e1e2e; + --secondary: #313244; + --secondary-foreground: #cdd6f4; + --muted: #181825; + --muted-foreground: #a6adc8; + --accent: #313244; + --accent-foreground: #cdd6f4; + --destructive: #f38ba8; + --border: #313244; + --input: #181825; + --ring: #89b4fa; + --chart-1: #89b4fa; + --chart-2: #a6e3a1; + --chart-3: #f9e2af; + --chart-4: #f5c2e7; + --chart-5: #94e2d5; + --sidebar: #11111b; + --sidebar-foreground: #cdd6f4; + --sidebar-primary: #89b4fa; + --sidebar-primary-foreground: #11111b; + --sidebar-accent: #313244; + --sidebar-accent-foreground: #cdd6f4; + --sidebar-border: #313244; + --sidebar-ring: #89b4fa; + + --bg-base: #1e1e2e; + --bg-elevated: #181825; + --bg-surface: #181825; + --bg-surface-hover: #313244; + --bg-input: #181825; + --bg-deepest: #11111b; + --bg-header: #11111b; + --bg-button: #181825; + --bg-active: #313244; + --bg-light: #181825; + --bg-subtle: #11111b; + --bg-interact: #313244; + --border-base: #313244; + --border-panel: #11111b; + --border-subtle: #313244; + --border-medium: #313244; + --bg-hover: #313244; + --bg-hover-alt: #313244; + --bg-pressed: #11111b; + --border-hover: #89b4fa; + --border-active: #cdd6f4; + + --foreground-secondary: #a6adc8; + --foreground-subtle: #7f849c; + + --scrollbar-thumb: #313244; + --scrollbar-thumb-hover: #45475a; + --scrollbar-track: #1e1e2e; + + --bg-overlay: rgba(0, 0, 0, 0.8); +} + @layer base { html, body { diff --git a/src/lib/db-health-monitor.ts b/src/lib/db-health-monitor.ts index 971e501e..8f6599c2 100644 --- a/src/lib/db-health-monitor.ts +++ b/src/lib/db-health-monitor.ts @@ -1,13 +1,25 @@ +/** + * DatabaseHealthMonitor + * + * Non-blocking health tracker for backend/database connectivity. The + * monitor no longer gates the whole UI: there is no full-screen overlay. + * When a transient failure is observed we emit a "degraded" event so the + * UI can surface a persistent but non-intrusive toast. A success from any + * API request clears the state. Session-expired events are also relayed + * to the UI. + * + * The previous "database-connection-lost" / "database-connection-restored" + * events have been retired along with the overlay. Listeners should use + * "database-connection-degraded" / "database-connection-degraded-cleared" + * to reflect the current UX contract: users can keep working regardless + * of backend hiccups and are simply informed via a toast. + */ type EventListener = (...args: any[]) => void; class DatabaseHealthMonitor { private static instance: DatabaseHealthMonitor; - private dbHealthy: boolean = true; - private lastCheckTime: number = 0; - private checkInProgress: boolean = false; private listeners: Map = new Map(); - private consecutiveErrorTimer: ReturnType | null = null; - private confirmedUnhealthy: boolean = false; + private degradedActive: boolean = false; private constructor() {} @@ -49,71 +61,56 @@ class DatabaseHealthMonitor { reportDatabaseError(error: any, _wasAuthenticated: boolean = false) { const errorMessage = error?.response?.data?.error || error?.message || ""; const errorCode = error?.response?.data?.code || error?.code; + const lowerMessage = errorMessage.toLowerCase(); const isDatabaseError = - errorMessage.toLowerCase().includes("database") || - errorMessage.toLowerCase().includes("sqlite") || - errorMessage.toLowerCase().includes("drizzle") || + lowerMessage.includes("database") || + lowerMessage.includes("sqlite") || + lowerMessage.includes("drizzle") || errorCode === "DATABASE_ERROR" || errorCode === "DB_CONNECTION_FAILED"; const isBackendUnreachable = errorCode === "ERR_NETWORK" || errorCode === "ECONNREFUSED" || - (errorMessage.toLowerCase().includes("network error") && - error?.response === undefined); + errorCode === "ECONNABORTED" || + errorCode === "ECONNRESET" || + errorCode === "ETIMEDOUT" || + errorCode === "ERR_CANCELED" || + (lowerMessage.includes("network error") && + error?.response === undefined) || + lowerMessage.includes("request aborted") || + lowerMessage.includes("timeout"); if (!(isDatabaseError || isBackendUnreachable)) { return; } - if (this.dbHealthy && !this.consecutiveErrorTimer) { - this.consecutiveErrorTimer = setTimeout(() => { - this.consecutiveErrorTimer = null; - if (this.dbHealthy) { - this.dbHealthy = false; - this.confirmedUnhealthy = true; - this.emit("database-connection-lost", { - error: errorMessage || "Backend server unreachable", - code: errorCode, - timestamp: Date.now(), - }); - } - }, 10000); + if (!this.degradedActive) { + this.degradedActive = true; + this.emit("database-connection-degraded", { + error: errorMessage || "Background request failed", + code: errorCode, + timestamp: Date.now(), + }); } } reportDatabaseSuccess() { - this.clearErrorTimer(); - - if (this.confirmedUnhealthy) { - this.dbHealthy = true; - this.confirmedUnhealthy = false; - this.emit("database-connection-restored", { + if (this.degradedActive) { + this.degradedActive = false; + this.emit("database-connection-degraded-cleared", { timestamp: Date.now(), }); - } else if (!this.dbHealthy) { - this.dbHealthy = true; } } - private clearErrorTimer(): void { - if (this.consecutiveErrorTimer !== null) { - clearTimeout(this.consecutiveErrorTimer); - this.consecutiveErrorTimer = null; - } - } - - isDatabaseHealthy(): boolean { - return this.dbHealthy; + isDegraded(): boolean { + return this.degradedActive; } reset() { - this.dbHealthy = true; - this.confirmedUnhealthy = false; - this.lastCheckTime = 0; - this.checkInProgress = false; - this.clearErrorTimer(); + this.degradedActive = false; } } diff --git a/src/locales/en.json b/src/locales/en.json index 0f5d6508..94f9414a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -277,6 +277,19 @@ "copyTooltip": "Copy snippet to clipboard", "editTooltip": "Edit this snippet", "deleteTooltip": "Delete this snippet", + "shareTooltip": "Share this snippet", + "sharedWithYou": "Shared", + "sharedBy": "Shared by {{username}}", + "shareSnippet": "Share Snippet", + "user": "User", + "role": "Role", + "selectTarget": "Select...", + "currentAccess": "Current Access", + "shareSuccess": "Snippet shared successfully", + "shareFailed": "Failed to share snippet", + "revokeSuccess": "Access revoked", + "revokeFailed": "Failed to revoke access", + "failedToLoadShareData": "Failed to load sharing data", "newFolder": "New Folder", "reorderSameFolder": "Can only reorder snippets within the same folder", "reorderSuccess": "Snippets reordered successfully", @@ -323,7 +336,9 @@ "authRequiredRefresh": "Authentication required. Please refresh the page.", "dataAccessLockedReauth": "Data access locked. Please re-authenticate.", "loading": "Loading command history...", - "error": "Error Loading History" + "error": "Error Loading History", + "disabledTitle": "Command History is Disabled", + "disabledDescription": "Enable Command History Tracking in your profile under Appearance settings." }, "splitScreen": { "title": "Split Screen", @@ -510,6 +525,8 @@ "checkingDatabase": "Checking database connection...", "checkingAuthentication": "Checking authentication...", "backendReconnected": "Server connection restored", + "connectionDegraded": "Server connection lost, recovering…", + "reload": "Reload", "actions": "Actions", "remove": "Remove", "revoke": "Revoke", @@ -541,7 +558,8 @@ "copySudoPassword": "Copy Sudo Password", "passwordCopied": "Password copied to clipboard", "sudoPasswordCopied": "Sudo password copied to clipboard", - "noPasswordAvailable": "No password available" + "noPasswordAvailable": "No password available", + "failedToCopyPassword": "Failed to copy password" }, "admin": { "title": "Admin Settings", @@ -810,6 +828,13 @@ "globalSettingsSaved": "Global monitoring settings saved", "failedToSaveGlobalSettings": "Failed to save global monitoring settings", "failedToLoadGlobalSettings": "Failed to load global monitoring settings", + "sessionTimeout": "Session Timeout", + "sessionTimeoutDesc": "Configure how long user sessions last before requiring re-authentication. 'Remember Me' sessions are unaffected (always 30 days).", + "sessionTimeoutHours": "Timeout Duration", + "hours": "hours", + "sessionTimeoutNote": "Valid range: 1–720 hours. Changes apply to new sessions only.", + "sessionTimeoutSaved": "Session timeout saved", + "failedToSaveSessionTimeout": "Failed to save session timeout", "guacamoleIntegration": "Remote Desktop Integration (Guacamole)", "guacamoleIntegrationDesc": "Enable RDP, VNC, and Telnet connections via guacd. Requires a guacd instance to be running.", "enableGuacamole": "Enable RDP/VNC/Telnet support", @@ -819,6 +844,12 @@ "guacamoleSettingsSaved": "Guacamole settings saved", "failedToSaveGuacamoleSettings": "Failed to save guacamole settings", "failedToLoadGuacamoleSettings": "Failed to load guacamole settings", + "logLevel": "Log Level", + "logLevelDesc": "Control the verbosity of server logs. Higher levels reduce log output. Can also be set via the LOG_LEVEL environment variable.", + "logVerbosity": "Verbosity", + "logLevelNote": "Changes take effect immediately. Debug level may impact performance.", + "logLevelSaved": "Log level saved", + "failedToSaveLogLevel": "Failed to save log level", "clampedToValidRange": "was adjusted to valid range", "sessionManagement": "Session Management", "loadingSessions": "Loading sessions...", @@ -861,6 +892,11 @@ "hostsCount": "{{count}} hosts", "importJson": "Import JSON", "importing": "Importing...", + "exportAllJson": "Export All", + "exporting": "Exporting...", + "exportedAllHosts": "Exported {{count}} hosts", + "failedToExportAllHosts": "Failed to export hosts. Please ensure you're logged in and have access to the host data.", + "exportAllSensitiveWarning": "The exported file will contain sensitive authentication data (passwords/SSH keys) in plaintext. Please keep the file secure and delete it after use.", "importJsonTitle": "Import SSH Hosts from JSON", "importJsonDesc": "Upload a JSON file to bulk import multiple SSH hosts (max 100).", "downloadSample": "Download Sample", @@ -893,6 +929,11 @@ "guacamoleSettings": "Remote Desktop Settings", "organization": "Organization", "ipAddress": "IP Address or Hostname", + "macAddress": "MAC Address", + "macAddressDesc": "For Wake-on-LAN. Format: AA:BB:CC:DD:EE:FF", + "wakeOnLan": "Wake on LAN", + "wolSent": "Wake-on-LAN packet sent", + "wolFailed": "Failed to send Wake-on-LAN packet", "ipRequired": "IP address is required", "portRequired": "Port is required", "port": "Port", @@ -1055,6 +1096,9 @@ "dragToMoveBetweenFolders": "Drag to move between folders", "exportedHostConfig": "Exported host configuration for {{name}}", "openTerminal": "Open Terminal", + "openRdp": "Open RDP", + "openVnc": "Open VNC", + "openTelnet": "Open Telnet", "openFileManager": "Open File Manager", "openTunnels": "Open Tunnels", "openServerDetails": "Open Server Details", @@ -1170,6 +1214,10 @@ "noServerFound": "No server found", "jumpHostsOrder": "Connections will be made in order: Jump Host 1 → Jump Host 2 → ... → Target Server", "socks5Proxy": "SOCKS5 Proxy", + "portKnocking": "Port Knocking", + "portKnockingDesc": "Send a sequence of connection attempts to specified ports before connecting. Used to open firewall rules dynamically.", + "addKnock": "Add Port", + "delayMs": "Delay", "socks5Description": "Configure SOCKS5 proxy for SSH connection. All traffic will be routed through the specified proxy server.", "enableSocks5": "Enable SOCKS5 Proxy", "enableSocks5Description": "Use SOCKS5 proxy for this SSH connection", @@ -1254,6 +1302,8 @@ "autoMoshDesc": "Automatically run MOSH command on connect", "moshCommand": "MOSH Command", "moshCommandDesc": "The MOSH command to execute", + "autoTmux": "Auto-tmux", + "autoTmuxDesc": "Automatically attach to an existing (or start a new) tmux session for persistent sessions and cross-device continuity", "environmentVariables": "Environment Variables", "environmentVariablesDesc": "Set custom environment variables for the terminal session", "variableName": "Variable name", @@ -1540,7 +1590,27 @@ "connecting": "Connecting...", "reconnecting": "Reconnecting... ({{attempt}}/{{max}})", "reconnected": "Reconnected successfully", + "tmuxSessionCreated": "tmux session created: {{name}}", + "tmuxSessionAttached": "tmux session attached: {{name}}", + "tmuxUnavailable": "tmux is not installed on the remote host, falling back to standard shell", + "tmuxSessionPickerTitle": "tmux Sessions", + "tmuxSessionPickerDesc": "Existing tmux sessions found on this host. Select one to reattach or create a new session.", + "tmuxWindows": "Windows", + "tmuxWindowCount": "{{count}} window", + "tmuxWindowCount_other": "{{count}} windows", + "tmuxAttached": "Attached clients", + "tmuxAttachedCount": "{{count}} attached", + "tmuxLastActivity": "Last activity", + "tmuxTimeJustNow": "just now", + "tmuxTimeMinutes": "{{count}}m ago", + "tmuxTimeHours": "{{count}}h ago", + "tmuxTimeDays": "{{count}}d ago", + "tmuxCreateNew": "Start new session", + "tmuxCopyHint": "Adjust selection and press Enter to copy to clipboard", "maxReconnectAttemptsReached": "Maximum reconnection attempts reached", + "connectionLost": "Connection lost", + "reconnect": "Reconnect", + "closeTab": "Close", "connectionTimeout": "Connection timeout", "terminalTitle": "Terminal - {{host}}", "terminalWithPath": "Terminal - {{host}}:{{path}}", @@ -1564,6 +1634,7 @@ "opksshTimeout": "Authentication timed out. Please try again.", "opksshAuthFailed": "Authentication failed. Please check your credentials and try again.", "opksshConfigMissing": "OPKSSH configuration not found. Please create ~/.opk/config.yml with your OIDC provider settings. See documentation: https://github.com/openpubkey/opkssh#configuration", + "opksshSignInWith": "Sign in with {{provider}}", "sudoPasswordPopupTitle": "Insert Password?", "sudoPasswordPopupHint": "Press Enter to insert, Esc to dismiss", "sudoPasswordPopupConfirm": "Insert", @@ -1588,7 +1659,8 @@ "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.", "hostKeyRejected": "SSH host key verification rejected. Connection cancelled.", "sessionTakenOver": "Session was opened in another tab. Reconnecting...", - "sessionAttachTimeout": "Session attachment timed out. Creating new connection..." + "sessionAttachTimeout": "Session attachment timed out. Creating new connection...", + "openFileManagerHere": "Open File Manager Here" }, "fileManager": { "title": "File Manager", @@ -2292,6 +2364,8 @@ "fileColorCodingDesc": "Color-code files by type: folders (red), files (blue), symlinks (green)", "commandAutocomplete": "Command Autocomplete", "commandAutocompleteDesc": "Enable Tab key autocomplete suggestions for terminal commands based on your command history", + "commandHistoryTracking": "Save Command History", + "commandHistoryTrackingDesc": "Store terminal commands in history for autocomplete and sidebar. Disabled by default for privacy.", "defaultSnippetFoldersCollapsed": "Collapse Snippet Folders by Default", "defaultSnippetFoldersCollapsedDesc": "When enabled, all snippet folders will be collapsed when you open the snippets tab", "terminalSyntaxHighlighting": "Terminal Syntax Highlighting", @@ -2530,7 +2604,7 @@ "database": "Database", "healthy": "Healthy", "error": "Error", - "totalServers": "Total Servers", + "totalHosts": "Total Hosts", "totalTunnels": "Total Tunnels", "totalCredentials": "Total Credentials", "recentActivity": "Recent Activity", diff --git a/src/types/guacamole-common-js.d.ts b/src/types/guacamole-common-js.d.ts index 8c3b03d6..5c494e91 100644 --- a/src/types/guacamole-common-js.d.ts +++ b/src/types/guacamole-common-js.d.ts @@ -7,11 +7,21 @@ declare module "guacamole-common-js" { getDisplay(): Display; sendKeyEvent(pressed: number, keysym: number): void; sendMouseState(state: Mouse.State): void; + sendSize(width: number, height: number): void; setClipboard(stream: OutputStream, mimetype: string): void; createClipboardStream(mimetype: string): OutputStream; onstatechange: ((state: number) => void) | null; onerror: ((error: Status) => void) | null; onclipboard: ((stream: InputStream, mimetype: string) => void) | null; + onaudio: ((stream: InputStream, mimetype: string) => void) | null; + } + + class AudioPlayer { + static getInstance( + stream: InputStream, + mimetype: string, + ): AudioPlayer | null; + sync(): void; } class Display { diff --git a/src/types/index.ts b/src/types/index.ts index 0ff039ec..2a59696e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -74,6 +74,13 @@ export interface Host { socks5Password?: string; socks5ProxyChain?: ProxyNode[]; + macAddress?: string; + portKnockSequence?: Array<{ + port: number; + protocol?: "tcp" | "udp"; + delay?: number; + }>; + connectionType?: "ssh" | "rdp" | "vnc" | "telnet"; domain?: string; security?: string; @@ -83,6 +90,10 @@ export interface Host { createdAt: string; updatedAt: string; + hasPassword?: boolean; + hasKey?: boolean; + hasSudoPassword?: boolean; + isShared?: boolean; permissionLevel?: "view"; sharedExpiresAt?: string; @@ -146,6 +157,13 @@ export interface HostData { socks5Password?: string; socks5ProxyChain?: ProxyNode[]; + macAddress?: string; + portKnockSequence?: Array<{ + port: number; + protocol?: "tcp" | "udp"; + delay?: number; + }>; + connectionType?: "ssh" | "rdp" | "vnc" | "telnet"; domain?: string; security?: string; @@ -425,6 +443,7 @@ export interface TerminalConfig { sudoPasswordAutoFill: boolean; keepaliveInterval?: number; keepaliveCountMax?: number; + autoTmux: boolean; } // ============================================================================ diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index 333ae776..afc9fa25 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -52,10 +52,13 @@ function AppContent({ const { theme, setTheme } = useTheme(); const [rightSidebarOpen, setRightSidebarOpen] = useState(false); const [rightSidebarWidth, setRightSidebarWidth] = useState(400); - const [dbConnectionFailed, setDbConnectionFailed] = useState(false); const isDarkMode = theme === "dark" || + theme === "dracula" || + theme === "gentlemansChoice" || + theme === "midnightEspresso" || + theme === "catppuccinMocha" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); const lineColor = isDarkMode ? "#151517" : "#f9f9f9"; @@ -65,12 +68,29 @@ function AppContent({ const lastAltPressTime = useRef(0); useEffect(() => { - const handleDatabaseConnectionLost = () => { - setDbConnectionFailed(true); + const DEGRADED_TOAST_ID = "db-connection-degraded"; + + const handleDatabaseConnectionDegraded = () => { + // Non-blocking, non-dismissible status toast that stays visible until + // connectivity is recovered. A Reload action lets users force-refresh + // the page if they want to, but the app itself remains fully usable. + toast.loading( + t("common.connectionDegraded", "Server connection lost, recovering…"), + { + id: DEGRADED_TOAST_ID, + duration: Infinity, + dismissible: false, + closeButton: false, + action: { + label: t("common.reload", "Reload"), + onClick: () => window.location.reload(), + }, + }, + ); }; - const handleDatabaseConnectionRestored = () => { - setDbConnectionFailed(false); + const handleDatabaseConnectionDegradedCleared = () => { + toast.dismiss(DEGRADED_TOAST_ID); toast.success(t("common.backendReconnected")); }; @@ -79,27 +99,28 @@ function AppContent({ }; dbHealthMonitor.on( - "database-connection-lost", - handleDatabaseConnectionLost, + "database-connection-degraded", + handleDatabaseConnectionDegraded, ); dbHealthMonitor.on( - "database-connection-restored", - handleDatabaseConnectionRestored, + "database-connection-degraded-cleared", + handleDatabaseConnectionDegradedCleared, ); dbHealthMonitor.on("session-expired", handleSessionExpired); return () => { dbHealthMonitor.off( - "database-connection-lost", - handleDatabaseConnectionLost, + "database-connection-degraded", + handleDatabaseConnectionDegraded, ); dbHealthMonitor.off( - "database-connection-restored", - handleDatabaseConnectionRestored, + "database-connection-degraded-cleared", + handleDatabaseConnectionDegradedCleared, ); dbHealthMonitor.off("session-expired", handleSessionExpired); + toast.dismiss(DEGRADED_TOAST_ID); }; - }, []); + }, [t]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -156,8 +177,9 @@ function AppContent({ if (hostIdentifier) { const openTerminal = async () => { try { - const { getSSHHostById, getSSHHosts } = - await import("@/ui/main-axios.ts"); + const { getSSHHostById, getSSHHosts } = await import( + "@/ui/main-axios.ts" + ); let host = null; if (/^\d+$/.test(hostIdentifier)) { @@ -188,8 +210,12 @@ function AppContent({ } }, [addTab]); + const isCheckingAuth = useRef(false); + useEffect(() => { const checkAuth = () => { + if (isCheckingAuth.current) return; + isCheckingAuth.current = true; setAuthLoading(true); getUserInfo() .then((meRes) => { @@ -197,7 +223,6 @@ function AppContent({ setIsAuthenticated(false); setIsAdmin(false); setUsername(null); - localStorage.removeItem("jwt"); } else { setIsAuthenticated(true); setIsAdmin(!!meRes.is_admin); @@ -209,8 +234,6 @@ function AppContent({ setIsAdmin(false); setUsername(null); - localStorage.removeItem("jwt"); - const errorCode = err?.response?.data?.code; if (errorCode === "SESSION_EXPIRED") { console.warn("Session expired - please log in again"); @@ -218,6 +241,7 @@ function AppContent({ }) .finally(() => { setAuthLoading(false); + isCheckingAuth.current = false; }); }; @@ -292,7 +316,7 @@ function AppContent({ const showAdmin = currentTabData?.type === "admin"; const showProfile = currentTabData?.type === "user_profile"; - if (authLoading && !dbConnectionFailed) { + if (authLoading) { return (
-
- {}} - initialDbError="Database connection failed" - /> -
- -
- ); - } - return (
>([]); const [usersLoading, setUsersLoading] = React.useState(false); @@ -80,9 +80,9 @@ export function AdminSettings({ const [selectedUserForEdit, setSelectedUserForEdit] = React.useState<{ id: string; username: string; - is_admin: boolean; - is_oidc: boolean; - password_hash?: string; + isAdmin: boolean; + isOidc: boolean; + passwordHash?: string; } | null>(null); const [currentUser, setCurrentUser] = React.useState<{ @@ -148,7 +148,6 @@ export function AdminSettings({ console.warn("Failed to fetch current user info", err); } }); - fetchUsers(); fetchSessions(); }, []); @@ -340,7 +339,15 @@ export function AdminSettings({
- + { + if (value === "users") { + fetchUsers(); + } + }} + className="w-full" + > { if (open && user) { - setIsAdmin(user.is_admin); + setIsAdmin(user.isAdmin); loadRoles(); } }, [open, user]); @@ -135,19 +135,18 @@ export function UserEditDialog({ setAdminLoading(true); try { if (checked) { - await makeUserAdmin(userToUpdate.username); + await makeUserAdmin(userToUpdate.id); toast.success( t("admin.userIsNowAdmin", { username: userToUpdate.username }), ); } else { - await removeAdminStatus(userToUpdate.username); + await removeAdminStatus(userToUpdate.id); toast.success( t("admin.adminStatusRemoved", { username: userToUpdate.username }), ); } setIsAdmin(checked); onSuccess(); - onOpenChange(true); } catch (error) { console.error("Failed to toggle admin status:", error); toast.error( @@ -332,9 +331,9 @@ export function UserEditDialog({ const getAuthTypeDisplay = (): string => { if (!user) return ""; - if (user.is_oidc && user.password_hash) { + if (user.isOidc && user.passwordHash) { return t("admin.dualAuth"); - } else if (user.is_oidc) { + } else if (user.isOidc) { return t("admin.externalOIDC"); } else { return t("admin.localPassword"); @@ -344,7 +343,7 @@ export function UserEditDialog({ if (!user) return null; const showPasswordReset = - allowPasswordLogin && (user.password_hash || !user.is_oidc); + allowPasswordLogin && (user.passwordHash || !user.isOidc); return ( diff --git a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx b/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx index 5c38955f..8736c5a7 100644 --- a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx +++ b/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx @@ -1,7 +1,5 @@ import React from "react"; import { Button } from "@/components/ui/button.tsx"; -import { Label } from "@/components/ui/label.tsx"; -import { PasswordInput } from "@/components/ui/password-input.tsx"; import { Download, Upload } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -22,33 +20,8 @@ export function DatabaseSecurityTab({ const [exportLoading, setExportLoading] = React.useState(false); const [importLoading, setImportLoading] = React.useState(false); const [importFile, setImportFile] = React.useState(null); - const [exportPassword, setExportPassword] = React.useState(""); - const [showPasswordInput, setShowPasswordInput] = React.useState(false); - const [importPassword, setImportPassword] = React.useState(""); - - const requiresImportPassword = React.useMemo( - () => !currentUser?.is_oidc, - [currentUser?.is_oidc], - ); - - const requiresExportPassword = React.useMemo( - () => !currentUser?.is_oidc, - [currentUser?.is_oidc], - ); const handleExportDatabase = async () => { - if (requiresExportPassword) { - if (!showPasswordInput) { - setShowPasswordInput(true); - return; - } - - if (!exportPassword.trim()) { - toast.error(t("admin.passwordRequired")); - return; - } - } - setExportLoading(true); try { const isDev = @@ -66,15 +39,21 @@ export function DatabaseSecurityTab({ ? `http://localhost:30001/database/export` : `${window.location.protocol}//${window.location.host}${getBasePath()}/database/export`; + const headers: Record = { + "Content-Type": "application/json", + }; + if (isElectron()) { + const token = localStorage.getItem("jwt"); + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + } + const response = await fetch(apiUrl, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers, credentials: "include", - body: JSON.stringify( - requiresExportPassword ? { password: exportPassword } : {}, - ), + body: JSON.stringify({}), }); if (response.ok) { @@ -94,15 +73,9 @@ export function DatabaseSecurityTab({ document.body.removeChild(a); toast.success(t("admin.databaseExportedSuccessfully")); - setExportPassword(""); - setShowPasswordInput(false); } else { const error = await response.json(); - if (error.code === "PASSWORD_REQUIRED") { - toast.error(t("admin.passwordRequired")); - } else { - toast.error(error.error || t("admin.databaseExportFailed")); - } + toast.error(error.error || t("admin.databaseExportFailed")); } } catch { toast.error(t("admin.databaseExportFailed")); @@ -117,11 +90,6 @@ export function DatabaseSecurityTab({ return; } - if (requiresImportPassword && !importPassword.trim()) { - toast.error(t("admin.passwordRequired")); - return; - } - setImportLoading(true); try { const isDev = @@ -141,12 +109,18 @@ export function DatabaseSecurityTab({ const formData = new FormData(); formData.append("file", importFile); - if (requiresImportPassword) { - formData.append("password", importPassword); + + const importHeaders: Record = {}; + if (isElectron()) { + const token = localStorage.getItem("jwt"); + if (token) { + importHeaders["Authorization"] = `Bearer ${token}`; + } } const response = await fetch(apiUrl, { method: "POST", + headers: importHeaders, credentials: "include", body: formData, }); @@ -181,7 +155,6 @@ export function DatabaseSecurityTab({ `Import completed: ${imported} items imported${details.length > 0 ? ` (${details.join(", ")})` : ""}, ${skipped} items skipped`, ); setImportFile(null); - setImportPassword(""); setTimeout(() => { window.location.reload(); @@ -193,11 +166,7 @@ export function DatabaseSecurityTab({ } } else { const error = await response.json(); - if (error.code === "PASSWORD_REQUIRED") { - toast.error(t("admin.passwordRequired")); - } else { - toast.error(error.error || t("admin.databaseImportFailed")); - } + toast.error(error.error || t("admin.databaseImportFailed")); } } catch { toast.error(t("admin.databaseImportFailed")); @@ -220,45 +189,13 @@ export function DatabaseSecurityTab({

{t("admin.exportDescription")}

- {showPasswordInput && requiresExportPassword && ( -
- - setExportPassword(e.target.value)} - placeholder="Enter your password" - onKeyDown={(e) => { - if (e.key === "Enter") { - handleExportDatabase(); - } - }} - /> -
- )} - {showPasswordInput && requiresExportPassword && ( - - )}
@@ -294,29 +231,9 @@ export function DatabaseSecurityTab({ - {importFile && requiresImportPassword && ( -
- - setImportPassword(e.target.value)} - placeholder="Enter your password" - onKeyDown={(e) => { - if (e.key === "Enter") { - handleImportDatabase(); - } - }} - /> -
- )} - {user.is_oidc && !user.password_hash && ( + {user.isOidc && !user.passwordHash && ( )} - {user.is_oidc && user.password_hash && ( + {user.isOidc && user.passwordHash && ( diff --git a/src/ui/desktop/apps/command-palette/CommandPalette.tsx b/src/ui/desktop/apps/command-palette/CommandPalette.tsx index 84c419a8..43344022 100644 --- a/src/ui/desktop/apps/command-palette/CommandPalette.tsx +++ b/src/ui/desktop/apps/command-palette/CommandPalette.tsx @@ -33,7 +33,8 @@ import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { getRecentActivity, getSSHHosts, - getGuacamoleToken, + getGuacamoleDpi, + getGuacamoleTokenFromHost, logActivity, } from "@/ui/main-axios.ts"; import type { RecentActivityItem } from "@/ui/main-axios.ts"; @@ -235,17 +236,7 @@ export function CommandPalette({ ) { try { const protocol = host.connectionType as "rdp" | "vnc" | "telnet"; - const result = await getGuacamoleToken({ - protocol, - hostname: host.ip, - port: host.port, - username: host.username, - password: host.password, - domain: host.domain, - security: host.security, - ignoreCert: host.ignoreCert, - guacamoleConfig: host.guacamoleConfig as any, - }); + const result = await getGuacamoleTokenFromHost(host.id); addTab({ type: protocol, @@ -262,6 +253,7 @@ export function CommandPalette({ domain: host.domain, security: host.security, "ignore-cert": host.ignoreCert, + dpi: getGuacamoleDpi(host), }, }); diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/desktop/apps/dashboard/Dashboard.tsx index 55ab28b1..44a19254 100644 --- a/src/ui/desktop/apps/dashboard/Dashboard.tsx +++ b/src/ui/desktop/apps/dashboard/Dashboard.tsx @@ -12,10 +12,12 @@ import { getCredentials, getRecentActivity, resetRecentActivity, + getAllServerStatuses, getServerMetricsById, registerMetricsViewer, sendMetricsHeartbeat, - getGuacamoleToken, + getGuacamoleDpi, + getGuacamoleTokenFromHost, type RecentActivityItem, } from "@/ui/main-axios.ts"; import { useSidebar } from "@/components/ui/sidebar.tsx"; @@ -68,7 +70,7 @@ export function Dashboard({ const [versionStatus, setVersionStatus] = useState< "up_to_date" | "requires_update" >("up_to_date"); - const [versionText, setVersionText] = useState("v1.8.0"); + const [versionText, setVersionText] = useState(""); const [dbHealth, setDbHealth] = useState<"healthy" | "error">("healthy"); const [totalServers, setTotalServers] = useState(0); const [totalTunnels, setTotalTunnels] = useState(0); @@ -223,72 +225,108 @@ export function Dashboard({ setRecentActivityLoading(false); setServerStatsLoading(true); + + // Fetch current host statuses once so we can skip offline hosts + // before issuing per-host register-viewer / metrics requests. + let hostStatuses: Record = {}; + try { + hostStatuses = (await getAllServerStatuses()) as Record< + number, + { status?: string } + >; + } catch { + // Best-effort: if the status endpoint is unavailable, fall back + // to the previous behavior and still attempt each host. + hostStatuses = {}; + } + const newViewerSessions = new Map(); const serversWithStats = await Promise.all( - hosts - .slice(0, 50) - .map( - async (host: { - id: number; - name: string; - authType?: string; - statsConfig?: string | { metricsEnabled?: boolean }; - }) => { - try { - let statsConfig: { metricsEnabled?: boolean } = { - metricsEnabled: true, + hosts.slice(0, 50).map( + async (host: { + id: number; + name: string; + authType?: string; + statsConfig?: + | string + | { + metricsEnabled?: boolean; + statusCheckEnabled?: boolean; }; - if (host.statsConfig) { - if (typeof host.statsConfig === "string") { - statsConfig = JSON.parse(host.statsConfig); - } else { - statsConfig = host.statsConfig; - } - } - - if (statsConfig.metricsEnabled === false) { - return null; - } - - if (host.authType === "none") { - return null; - } - - if (host.authType === "opkssh") { - return null; - } - - const existingSession = viewerSessions.get(host.id); - let sessionId = existingSession; - - if (!existingSession) { - try { - const viewerResult = await registerMetricsViewer(host.id); - if ( - viewerResult.success && - viewerResult.viewerSessionId - ) { - sessionId = viewerResult.viewerSessionId; - newViewerSessions.set(host.id, sessionId); - } - } catch (error) { - console.error( - `Failed to register viewer for host ${host.id}:`, - error, - ); - } + }) => { + try { + let statsConfig: { + metricsEnabled?: boolean; + statusCheckEnabled?: boolean; + } = { + metricsEnabled: true, + statusCheckEnabled: true, + }; + if (host.statsConfig) { + if (typeof host.statsConfig === "string") { + statsConfig = JSON.parse(host.statsConfig); } else { - newViewerSessions.set(host.id, existingSession); + statsConfig = host.statsConfig; } + } - const metrics = await getServerMetricsById(host.id); - return { - id: host.id, - name: host.name || `Host ${host.id}`, - cpu: metrics.cpu.percent, - ram: metrics.memory.percent, - }; - } catch { + if (statsConfig.metricsEnabled === false) { + return null; + } + + if (host.authType === "none") { + return null; + } + + if (host.authType === "opkssh") { + return null; + } + + // Skip hosts that are known to be offline: no metrics can + // possibly exist for them, and hitting /metrics/:id would + // just 404. If the status is unknown (e.g. no entry yet + // or statusCheckEnabled === false) we still attempt. + if (statsConfig.statusCheckEnabled !== false) { + const knownStatus = hostStatuses?.[host.id]?.status; + if (knownStatus === "offline") { + return null; + } + } + + const existingSession = viewerSessions.get(host.id); + let sessionId = existingSession; + let registrationSkipped = false; + + if (!existingSession) { + try { + const viewerResult = await registerMetricsViewer(host.id); + if (viewerResult.skipped) { + // Metrics disabled/unsupported on this host; don't + // poll and don't surface this as an error. + registrationSkipped = true; + } else if ( + viewerResult.success && + viewerResult.viewerSessionId + ) { + sessionId = viewerResult.viewerSessionId; + newViewerSessions.set(host.id, sessionId); + } + } catch (error) { + console.error( + `Failed to register viewer for host ${host.id}:`, + error, + ); + } + } else { + newViewerSessions.set(host.id, existingSession); + } + + if (registrationSkipped) { + return null; + } + + const metrics = await getServerMetricsById(host.id); + if (!metrics) { return { id: host.id, name: host.name || `Host ${host.id}`, @@ -296,8 +334,22 @@ export function Dashboard({ ram: null, }; } - }, - ), + return { + id: host.id, + name: host.name || `Host ${host.id}`, + cpu: metrics.cpu?.percent ?? null, + ram: metrics.memory?.percent ?? null, + }; + } catch { + return { + id: host.id, + name: host.name || `Host ${host.id}`, + cpu: null, + ram: null, + }; + } + }, + ), ); setViewerSessions(newViewerSessions); const validServerStats = serversWithStats.filter( @@ -388,17 +440,7 @@ export function Dashboard({ hostConfig: host, }); } else if (item.type === "telnet") { - getGuacamoleToken({ - protocol: "telnet", - hostname: host.ip, - port: host.port, - username: host.username, - password: host.password, - domain: host.domain, - security: host.security, - ignoreCert: host.ignoreCert, - guacamoleConfig: host.guacamoleConfig as any, - }) + getGuacamoleTokenFromHost(host.id) .then((result) => { addTab({ type: "telnet", @@ -415,6 +457,7 @@ export function Dashboard({ domain: host.domain, security: host.security, "ignore-cert": host.ignoreCert, + dpi: getGuacamoleDpi(host), }, }); }) @@ -422,17 +465,7 @@ export function Dashboard({ console.error("Failed to get telnet token:", error); }); } else if (item.type === "vnc") { - getGuacamoleToken({ - protocol: "vnc", - hostname: host.ip, - port: host.port, - username: host.username, - password: host.password, - domain: host.domain, - security: host.security, - ignoreCert: host.ignoreCert, - guacamoleConfig: host.guacamoleConfig as any, - }) + getGuacamoleTokenFromHost(host.id) .then((result) => { addTab({ type: "vnc", @@ -449,6 +482,7 @@ export function Dashboard({ domain: host.domain, security: host.security, "ignore-cert": host.ignoreCert, + dpi: getGuacamoleDpi(host), }, }); }) @@ -456,17 +490,7 @@ export function Dashboard({ console.error("Failed to get vnc token:", error); }); } else if (item.type === "rdp") { - getGuacamoleToken({ - protocol: "rdp", - hostname: host.ip, - port: host.port, - username: host.username, - password: host.password, - domain: host.domain, - security: host.security, - ignoreCert: host.ignoreCert, - guacamoleConfig: host.guacamoleConfig as any, - }) + getGuacamoleTokenFromHost(host.id) .then((result) => { addTab({ type: "rdp", @@ -483,6 +507,7 @@ export function Dashboard({ domain: host.domain, security: host.security, "ignore-cert": host.ignoreCert, + dpi: getGuacamoleDpi(host), }, }); }) @@ -650,15 +675,6 @@ export function Dashboard({ > {t("dashboard.discord")} - + ))} + + + ); } const TerminalInner = forwardRef( @@ -94,6 +204,8 @@ const TerminalInner = forwardRef( onTitleChange, initialPath, executeCommand, + onOpenFileManager, + previewTheme, }, ref, ) { @@ -114,21 +226,37 @@ const TerminalInner = forwardRef( const { theme: appTheme } = useTheme(); const { addLog, isExpanded: isConnectionLogExpanded } = useConnectionLog(); - const config = { ...DEFAULT_TERMINAL_CONFIG, ...hostConfig.terminalConfig }; + const savedTheme = localStorage.getItem( + `terminal_theme_host_${hostConfig.id}`, + ); + const config = { + ...DEFAULT_TERMINAL_CONFIG, + ...hostConfig.terminalConfig, + theme: + savedTheme || + hostConfig.terminalConfig?.theme || + DEFAULT_TERMINAL_CONFIG.theme, + }; const isDarkMode = appTheme === "dark" || + appTheme === "dracula" || + appTheme === "gentlemansChoice" || + appTheme === "midnightEspresso" || + appTheme === "catppuccinMocha" || (appTheme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); let themeColors; - if (config.theme === "termix") { + const activeTheme = previewTheme || config.theme; + + if (activeTheme === "termix") { themeColors = isDarkMode ? TERMINAL_THEMES.termixDark.colors : TERMINAL_THEMES.termixLight.colors; } else { themeColors = - TERMINAL_THEMES[config.theme]?.colors || + TERMINAL_THEMES[activeTheme]?.colors || TERMINAL_THEMES.termixDark.colors; } const backgroundColor = themeColors.background; @@ -137,11 +265,15 @@ const TerminalInner = forwardRef( const resizeTimeout = useRef(null); const wasDisconnectedBySSH = useRef(false); const pingIntervalRef = useRef(null); + const pongReceivedRef = useRef(true); + const pongTimeoutRef = useRef(null); const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [isFitted, setIsFitted] = useState(false); const [connectionError, setConnectionError] = useState(null); const connectionErrorRef = useRef(null); + const [showDisconnectedOverlay, setShowDisconnectedOverlay] = + useState(false); const updateConnectionError = useCallback((error: string | null) => { connectionErrorRef.current = error; @@ -169,8 +301,15 @@ const TerminalInner = forwardRef( requestId: string; stage: "chooser" | "waiting" | "authenticating" | "completed" | "error"; error?: string; + providers?: Array<{ alias: string; issuer: string }>; } | null>(null); const opksshTimeoutRef = useRef(null); + + const [contextMenu, setContextMenu] = useState<{ + x: number; + y: number; + hasSelection: boolean; + } | null>(null); const opksshFailedRef = useRef(false); const currentHostIdRef = useRef(null); const currentHostConfigRef = useRef(null); @@ -183,12 +322,23 @@ const TerminalInner = forwardRef( const sessionIdRef = useRef(null); const isAttachingSessionRef = useRef(false); + const [tmuxSessionPicker, setTmuxSessionPicker] = useState<{ + sessions: Array<{ + name: string; + created: number; + lastActivity: number; + windows: number; + attachedClients: number; + }>; + } | null>(null); + const tmuxSessionNameRef = useRef(null); + const tmuxCopyModeHintShownRef = useRef(false); const isVisibleRef = useRef(false); const isFittingRef = useRef(false); const reconnectTimeoutRef = useRef(null); const reconnectAttempts = useRef(0); - const maxReconnectAttempts = 3; + const maxReconnectAttempts = 8; const isUnmountingRef = useRef(false); const shouldNotReconnectRef = useRef(false); const isReconnectingRef = useRef(false); @@ -211,11 +361,35 @@ const TerminalInner = forwardRef( const connectionTimeoutRef = useRef(null); const activityLoggedRef = useRef(false); const keyHandlerAttachedRef = useRef(false); + const [commandHistoryTrackingEnabled, setCommandHistoryTrackingEnabled] = + useState( + () => localStorage.getItem("commandHistoryTracking") === "true", + ); + + useEffect(() => { + const handleCommandHistoryTrackingChanged = () => { + setCommandHistoryTrackingEnabled( + localStorage.getItem("commandHistoryTracking") === "true", + ); + }; + + window.addEventListener( + "commandHistoryTrackingChanged", + handleCommandHistoryTrackingChanged, + ); + + return () => { + window.removeEventListener( + "commandHistoryTrackingChanged", + handleCommandHistoryTrackingChanged, + ); + }; + }, []); const { trackInput, getCurrentCommand, updateCurrentCommand } = useCommandTracker({ hostId: hostConfig.id, - enabled: true, + enabled: commandHistoryTrackingEnabled, onCommandExecuted: (command) => { if (!autocompleteHistory.current.includes(command)) { autocompleteHistory.current = [ @@ -558,6 +732,10 @@ const TerminalInner = forwardRef( clearInterval(pingIntervalRef.current); pingIntervalRef.current = null; } + if (pongTimeoutRef.current) { + clearTimeout(pongTimeoutRef.current); + pongTimeoutRef.current = null; + } if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; @@ -584,6 +762,23 @@ const TerminalInner = forwardRef( setIsConnected(false); setIsConnecting(false); }, + reconnect: () => { + isUnmountingRef.current = false; + shouldNotReconnectRef.current = false; + isReconnectingRef.current = false; + isConnectingRef.current = false; + reconnectAttempts.current = 0; + wasDisconnectedBySSH.current = false; + wasConnectedRef.current = false; + updateConnectionError(null); + setShowDisconnectedOverlay(false); + if (terminal) { + terminal.clear(); + const cols = terminal.cols; + const rows = terminal.rows; + connectToHost(cols, rows); + } + }, fit: () => { fitAddonRef.current?.fit(); if (terminal) scheduleNotify(terminal.cols, terminal.rows); @@ -628,9 +823,9 @@ const TerminalInner = forwardRef( } if (reconnectAttempts.current >= maxReconnectAttempts) { - updateConnectionError(t("terminal.maxReconnectAttemptsReached")); setIsConnecting(false); shouldNotReconnectRef.current = true; + setShowDisconnectedOverlay(true); addLog({ type: "error", stage: "connection", @@ -706,7 +901,7 @@ const TerminalInner = forwardRef( }, delay); } - function connectToHost(cols: number, rows: number) { + async function connectToHost(cols: number, rows: number) { if (isConnectingRef.current) { return; } @@ -738,20 +933,52 @@ const TerminalInner = forwardRef( return; } - const baseWsUrl = isDev - ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002` - : isElectron() - ? (() => { - const baseUrl = - (window as { configuredServerUrl?: string }) - .configuredServerUrl || "http://127.0.0.1:30001"; - const wsProtocol = baseUrl.startsWith("https://") - ? "wss://" - : "ws://"; - const wsHost = baseUrl.replace(/^https?:\/\//, ""); - return `${wsProtocol}${wsHost}/ssh/websocket/`; - })() - : `${getBasePath()}/ssh/websocket/`; + let baseWsUrl: string; + + if (isDev) { + baseWsUrl = `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002`; + } else if (isElectron()) { + let configuredUrl = (window as { configuredServerUrl?: string | null }) + .configuredServerUrl; + + if (!configuredUrl && !isEmbeddedMode()) { + try { + const serverConfig = await getServerConfig(); + configuredUrl = serverConfig?.serverUrl || null; + if (configuredUrl) { + ( + window as Window & + typeof globalThis & { + configuredServerUrl?: string | null; + } + ).configuredServerUrl = configuredUrl; + } + } catch (error) { + console.error("Failed to resolve Electron server URL:", error); + } + } + + if (isEmbeddedMode()) { + baseWsUrl = "ws://127.0.0.1:30002"; + } else if (!configuredUrl) { + console.error("No configured server URL available for Electron SSH"); + setIsConnected(false); + setIsConnecting(false); + updateConnectionError(t("errors.failedToLoadServer")); + isConnectingRef.current = false; + return; + } else { + const wsProtocol = configuredUrl.startsWith("https://") + ? "wss://" + : "ws://"; + const wsHost = configuredUrl + .replace(/^https?:\/\//, "") + .replace(/\/$/, ""); + baseWsUrl = `${wsProtocol}${wsHost}/ssh/websocket/`; + } + } else { + baseWsUrl = `${getBasePath()}/ssh/websocket/`; + } if ( webSocketRef.current && @@ -769,9 +996,7 @@ const TerminalInner = forwardRef( connectionTimeoutRef.current = null; } - const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`; - - const ws = new WebSocket(wsUrl); + const ws = new WebSocket(baseWsUrl); webSocketRef.current = ws; wasDisconnectedBySSH.current = false; updateConnectionError(null); @@ -857,8 +1082,17 @@ const TerminalInner = forwardRef( ws.send(JSON.stringify({ type: "input", data })); }); + pongReceivedRef.current = true; pingIntervalRef.current = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { + if (!pongReceivedRef.current) { + console.warn( + "[WebSocket] Pong timeout - connection appears dead, closing", + ); + ws.close(); + return; + } + pongReceivedRef.current = false; ws.send(JSON.stringify({ type: "ping" })); } }, 30000); @@ -867,6 +1101,10 @@ const TerminalInner = forwardRef( ws.addEventListener("message", (event) => { try { const msg = JSON.parse(event.data); + if (msg.type === "pong") { + pongReceivedRef.current = true; + return; + } if (msg.type === "data") { if (typeof msg.data === "string") { const syntaxHighlightingEnabled = @@ -879,19 +1117,36 @@ const TerminalInner = forwardRef( terminal.write(outputData); const sudoPasswordPattern = /(?:\[sudo\][^\n]*:\s*$|sudo:[^\n]*password[^\n]*required)/i; - const passwordToFill = - hostConfig.terminalConfig?.sudoPassword || hostConfig.password; + const hasSudoPw = + hostConfig.terminalConfig?.sudoPassword || + hostConfig.password || + hostConfig.hasSudoPassword || + hostConfig.hasPassword; if ( config.sudoPasswordAutoFill && sudoPasswordPattern.test(msg.data) && - passwordToFill && + hasSudoPw && !sudoPromptShownRef.current ) { sudoPromptShownRef.current = true; confirmWithToast( t("terminal.sudoPasswordPopupTitle"), async () => { + // Fetch password on-demand from server + let passwordToFill = + hostConfig.terminalConfig?.sudoPassword || + hostConfig.password; + if (!passwordToFill && hostConfig.id) { + passwordToFill = + (await getHostPassword( + hostConfig.id, + "sudoPassword", + )) || + (await getHostPassword(hostConfig.id, "password")) || + undefined; + } if ( + passwordToFill && webSocketRef.current && webSocketRef.current.readyState === WebSocket.OPEN ) { @@ -1052,19 +1307,10 @@ const TerminalInner = forwardRef( } else if (msg.type === "disconnected") { wasDisconnectedBySSH.current = true; setIsConnected(false); - if (terminal) { - terminal.clear(); - } setIsConnecting(false); if (wasConnectedRef.current) { wasConnectedRef.current = false; - if ( - onClose && - !connectionErrorRef.current && - !opksshFailedRef.current - ) { - onClose(); - } + setShowDisconnectedOverlay(true); } else if (!connectionErrorRef.current) { updateConnectionError( msg.message || t("terminal.connectionRejected"), @@ -1158,6 +1404,7 @@ const TerminalInner = forwardRef( authUrl: msg.url || "", requestId: msg.requestId || "", stage: "chooser", + providers: msg.providers, }); if (opksshTimeoutRef.current) { clearTimeout(opksshTimeoutRef.current); @@ -1345,6 +1592,40 @@ const TerminalInner = forwardRef( const cols = terminal?.cols || 80; const rows = terminal?.rows || 24; connectToHost(cols, rows); + } else if (msg.type === "tmux_sessions_available") { + setTmuxSessionPicker({ + sessions: msg.sessions, + }); + } else if ( + msg.type === "tmux_session_created" || + msg.type === "tmux_session_attached" + ) { + const sessionName = + typeof msg.sessionName === "string" ? msg.sessionName : ""; + tmuxSessionNameRef.current = sessionName || "(active)"; + addLog({ + type: "info", + stage: "connection", + message: + msg.type === "tmux_session_created" + ? t("terminal.tmuxSessionCreated", { + name: sessionName || "new", + }) + : t("terminal.tmuxSessionAttached", { + name: sessionName, + }), + }); + } else if (msg.type === "tmux_unavailable") { + setTimeout(() => { + toast.warning(t("terminal.tmuxUnavailable"), { + duration: 8000, + }); + }, 500); + addLog({ + type: "warning", + stage: "connection", + message: t("terminal.tmuxUnavailable"), + }); } else if (msg.type === "connection_log") { if (msg.data) { addLog({ @@ -1369,8 +1650,10 @@ const TerminalInner = forwardRef( setIsConnected(false); isConnectingRef.current = false; - if (terminal) { - terminal.clear(); + + if (pongTimeoutRef.current) { + clearTimeout(pongTimeoutRef.current); + pongTimeoutRef.current = null; } if (totpTimeoutRef.current) { @@ -1379,17 +1662,21 @@ const TerminalInner = forwardRef( } if (event.code === 1006) { - console.error( - "[WebSocket] Abnormal closure detected - possible HTTPS/proxy issue", + console.warn( + "[WebSocket] Abnormal closure detected - attempting reconnection", ); addLog({ - type: "error", + type: "warning", stage: "connection", message: t("terminal.websocketAbnormalClose"), }); - updateConnectionError(t("terminal.websocketAbnormalClose")); - setIsConnecting(false); - shouldNotReconnectRef.current = true; + + if (wasConnectedRef.current) { + attemptReconnection(); + } else { + updateConnectionError(t("terminal.websocketAbnormalClose")); + setIsConnecting(false); + } return; } @@ -1406,10 +1693,6 @@ const TerminalInner = forwardRef( localStorage.removeItem("jwt"); - setTimeout(() => { - window.location.reload(); - }, 1000); - return; } @@ -1581,22 +1864,25 @@ const TerminalInner = forwardRef( commandHistoryContext.setOnDeleteCommand(handleDeleteCommand); }, [handleDeleteCommand]); + // Separate theme and options updates to avoid terminal re-initialization flashes useEffect(() => { - if (!terminal || !xtermRef.current) return; + if (!terminal) return; const config = { ...DEFAULT_TERMINAL_CONFIG, - ...hostConfig.terminalConfig, + ...(hostConfig.terminalConfig as any), }; let themeColors; - if (config.theme === "termix") { + const activeTheme = previewTheme || config.theme; + + if (activeTheme === "termix") { themeColors = isDarkMode ? TERMINAL_THEMES.termixDark.colors : TERMINAL_THEMES.termixLight.colors; } else { themeColors = - TERMINAL_THEMES[config.theme]?.colors || + TERMINAL_THEMES[activeTheme]?.colors || TERMINAL_THEMES.termixDark.colors; } @@ -1605,13 +1891,98 @@ const TerminalInner = forwardRef( ); const fontFamily = fontConfig?.fallback || TERMINAL_FONTS[0].fallback; + // Update terminal options individually to avoid re-initialization flashes + terminal.options.cursorBlink = config.cursorBlink; + terminal.options.cursorStyle = config.cursorStyle; + terminal.options.scrollback = config.scrollback; + terminal.options.fontSize = config.fontSize; + terminal.options.fontFamily = fontFamily; + terminal.options.rightClickSelectsWord = config.rightClickSelectsWord; + terminal.options.fastScrollModifier = config.fastScrollModifier; + terminal.options.fastScrollSensitivity = config.fastScrollSensitivity; + terminal.options.minimumContrastRatio = config.minimumContrastRatio; + terminal.options.letterSpacing = config.letterSpacing; + terminal.options.lineHeight = config.lineHeight; + terminal.options.bellStyle = config.bellStyle as + | "none" + | "sound" + | "visual" + | "both"; + + terminal.options.theme = { + background: themeColors.background, + foreground: themeColors.foreground, + cursor: themeColors.cursor, + cursorAccent: themeColors.cursorAccent, + selectionBackground: themeColors.selectionBackground, + selectionForeground: themeColors.selectionForeground, + black: themeColors.black, + red: themeColors.red, + green: themeColors.green, + yellow: themeColors.yellow, + blue: themeColors.blue, + magenta: themeColors.magenta, + cyan: themeColors.cyan, + white: themeColors.white, + brightBlack: themeColors.brightBlack, + brightRed: themeColors.brightRed, + brightGreen: themeColors.brightGreen, + brightYellow: themeColors.brightYellow, + brightBlue: themeColors.brightBlue, + brightMagenta: themeColors.brightMagenta, + brightCyan: themeColors.brightCyan, + brightWhite: themeColors.brightWhite, + }; + + // Ensure terminal is correctly fitted if font-related options change + if (fitAddonRef.current && isFitted) { + performFit(); + } + + // Refresh terminal to apply new theme colors to existing buffer content + hardRefresh(); + }, [ + terminal, + hostConfig.terminalConfig, + previewTheme, + isDarkMode, + isFitted, + ]); + + useEffect(() => { + if (!terminal || !xtermRef.current) return; + + const config = { + ...DEFAULT_TERMINAL_CONFIG, + ...(hostConfig.terminalConfig as any), + }; + + const fontConfig = TERMINAL_FONTS.find( + (f) => f.value === config.fontFamily, + ); + const fontFamily = fontConfig?.fallback || TERMINAL_FONTS[0].fallback; + + let themeColors; + const activeTheme = previewTheme || config.theme; + + if (activeTheme === "termix") { + themeColors = isDarkMode + ? TERMINAL_THEMES.termixDark.colors + : TERMINAL_THEMES.termixLight.colors; + } else { + themeColors = + TERMINAL_THEMES[activeTheme]?.colors || + TERMINAL_THEMES.termixDark.colors; + } + + // Set initial options before opening the terminal terminal.options = { cursorBlink: config.cursorBlink, cursorStyle: config.cursorStyle, scrollback: config.scrollback, fontSize: config.fontSize, fontFamily, - allowTransparency: true, + allowTransparency: true, // MUST be set before open() convertEol: false, windowsMode: false, macOptionIsMeta: false, @@ -1624,7 +1995,6 @@ const TerminalInner = forwardRef( letterSpacing: config.letterSpacing, lineHeight: config.lineHeight, bellStyle: config.bellStyle as "none" | "sound" | "visual" | "both", - theme: { background: themeColors.background, foreground: themeColors.foreground, @@ -1678,26 +2048,71 @@ const TerminalInner = forwardRef( } const element = xtermRef.current; - const handleContextMenu = async (e: MouseEvent) => { - if (!getUseRightClickCopyPaste()) return; + const handleContextMenu = (e: MouseEvent) => { + if (getUseRightClickCopyPaste()) { + e.preventDefault(); + e.stopPropagation(); + if (terminal.hasSelection()) { + const text = terminal.getSelection(); + navigator.clipboard + .writeText(text) + .then(() => terminal.clearSelection()); + } else { + navigator.clipboard.readText().then((text) => { + if (text) terminal.paste(text); + }); + } + return; + } + if (!onOpenFileManager) return; e.preventDefault(); e.stopPropagation(); - if (terminal.hasSelection()) { - const selection = terminal.getSelection(); - if (selection) { - await writeTextToClipboard(selection); - terminal.clearSelection(); - } - } else { - const text = await readTextFromClipboard(); - if (text) terminal.paste(text); - } + setContextMenu({ + x: e.clientX, + y: e.clientY, + hasSelection: terminal.hasSelection(), + }); }; element?.addEventListener("contextmenu", handleContextMenu); + const handlePaste = (e: ClipboardEvent) => { + const text = e.clipboardData?.getData("text"); + if (text) { + e.preventDefault(); + e.stopPropagation(); + terminal.paste(text); + } + }; + element?.addEventListener("paste", handlePaste); + + let tmuxDragTracking = false; + const handleTmuxDragStart = (e: MouseEvent) => { + if (e.button !== 0) return; + if (!tmuxSessionNameRef.current) return; + tmuxDragTracking = true; + }; + const handleTmuxDragMove = () => { + if (!tmuxDragTracking) return; + tmuxDragTracking = false; + if (tmuxCopyModeHintShownRef.current) return; + tmuxCopyModeHintShownRef.current = true; + toast.info(t("terminal.tmuxCopyHint"), { duration: 5000 }); + }; + const handleTmuxDragEnd = () => { + tmuxDragTracking = false; + }; + element?.addEventListener("mousedown", handleTmuxDragStart); + element?.addEventListener("mousemove", handleTmuxDragMove); + element?.addEventListener("mouseup", handleTmuxDragEnd); + const handleBackspaceMode = (e: KeyboardEvent) => { if (e.key !== "Backspace") return; if (e.ctrlKey || e.metaKey || e.altKey) return; + + const config = { + ...DEFAULT_TERMINAL_CONFIG, + ...(hostConfig.terminalConfig as any), + }; if (config.backspaceMode !== "control-h") return; e.preventDefault(); @@ -1729,11 +2144,15 @@ const TerminalInner = forwardRef( resizeObserver.disconnect(); clipboardProvider.dispose(); element?.removeEventListener("contextmenu", handleContextMenu); + element?.removeEventListener("paste", handlePaste); + element?.removeEventListener("mousedown", handleTmuxDragStart); + element?.removeEventListener("mousemove", handleTmuxDragMove); + element?.removeEventListener("mouseup", handleTmuxDragEnd); element?.removeEventListener("keydown", handleBackspaceMode, true); if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current); if (resizeTimeout.current) clearTimeout(resizeTimeout.current); }; - }, [xtermRef, terminal, hostConfig, isDarkMode]); + }, [xtermRef, terminal]); const isMountedRef = useRef(false); @@ -1765,6 +2184,10 @@ const TerminalInner = forwardRef( clearInterval(pingIntervalRef.current); pingIntervalRef.current = null; } + if (pongTimeoutRef.current) { + clearTimeout(pongTimeoutRef.current); + pongTimeoutRef.current = null; + } const persistenceEnabled = localStorage.getItem("enableTerminalSessionPersistence") === "true"; @@ -1838,11 +2261,9 @@ const TerminalInner = forwardRef( !e.metaKey && e.key.toLowerCase() === "v" ) { - e.preventDefault(); - e.stopPropagation(); - readTextFromClipboard().then((text) => { - if (text) terminal.paste(text); - }); + // Let the browser handle Ctrl+V natively, the paste event + // listener will intercept the result without triggering the + // clipboard permission popup return false; } @@ -2145,6 +2566,40 @@ const TerminalInner = forwardRef( backgroundColor={backgroundColor} /> + {showDisconnectedOverlay && !isConnecting && ( +
+
+ + {onClose && ( + + )} +
+
+ )} + ( requestId={opksshDialog.requestId} stage={opksshDialog.stage} error={opksshDialog.error} + providers={opksshDialog.providers} onCancel={() => { if (webSocketRef.current) { webSocketRef.current.send( @@ -2217,6 +2673,22 @@ const TerminalInner = forwardRef( ); } }} + onSelectProvider={(alias) => { + if (!opksshDialog.authUrl) return; + const selectUrl = `${opksshDialog.authUrl}/select?op=${encodeURIComponent(alias)}`; + window.open(selectUrl, "_blank"); + if (webSocketRef.current) { + webSocketRef.current.send( + JSON.stringify({ + type: "opkssh_browser_opened", + data: { requestId: opksshDialog.requestId }, + }), + ); + } + setOpksshDialog((prev) => + prev ? { ...prev, stage: "waiting" } : null, + ); + }} backgroundColor={backgroundColor} /> )} @@ -2254,6 +2726,37 @@ const TerminalInner = forwardRef( /> )} + {tmuxSessionPicker && ( + { + setTmuxSessionPicker(null); + if (webSocketRef.current?.readyState === WebSocket.OPEN) { + webSocketRef.current.send( + JSON.stringify({ + type: "tmux_attach", + data: { sessionName }, + }), + ); + } + }} + onCreateNew={() => { + setTmuxSessionPicker(null); + if (webSocketRef.current?.readyState === WebSocket.OPEN) { + webSocketRef.current.send( + JSON.stringify({ + type: "tmux_attach", + data: { sessionName: "" }, + }), + ); + } + }} + onCancel={() => setTmuxSessionPicker(null)} + backgroundColor={backgroundColor} + /> + )} + ( position={autocompletePosition} onSelect={handleAutocompleteSelect} /> + + {contextMenu && ( + { + const selection = terminal?.getSelection(); + if (selection) { + await writeTextToClipboard(selection); + terminal?.clearSelection(); + } + }} + onPaste={async () => { + const text = await readTextFromClipboard(); + if (text) terminal?.paste(text); + }} + onOpenFileManager={() => onOpenFileManager?.()} + onClose={() => setContextMenu(null)} + /> + )} ); }, diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx index 08783405..03dfe06a 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx @@ -12,6 +12,7 @@ import { CredentialsManager } from "@/ui/desktop/apps/host-manager/credentials/C import { CredentialEditor } from "@/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx"; import { useSidebar } from "@/components/ui/sidebar.tsx"; import { useTranslation } from "react-i18next"; +import { exportSSHHostWithCredentials } from "@/ui/main-axios.ts"; import type { SSHHost, HostManagerProps } from "../../../types/index"; export function HostManager({ @@ -93,9 +94,13 @@ export function HostManager({ } if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) { - setEditingHost(hostConfig); - setIsAddingHost(false); lastProcessedHostIdRef.current = hostConfig.id; + setIsAddingHost(false); + exportSSHHostWithCredentials(hostConfig.id) + .then((fullHost) => + setEditingHost({ ...hostConfig, ...fullHost } as SSHHost), + ) + .catch(() => setEditingHost(hostConfig)); } else if (!hostConfig && editingHost) { setEditingHost(null); setIsAddingHost(false); @@ -111,17 +116,26 @@ export function HostManager({ setActiveTab(normalizedTab); } if (hostConfig && hostConfig.id !== lastProcessedHostIdRef.current) { - setEditingHost(hostConfig); - setIsAddingHost(false); lastProcessedHostIdRef.current = hostConfig.id; + setIsAddingHost(false); + exportSSHHostWithCredentials(hostConfig.id) + .then((fullHost) => + setEditingHost({ ...hostConfig, ...fullHost } as SSHHost), + ) + .catch(() => setEditingHost(hostConfig)); } } }, [_updateTimestamp, initialTab, hostConfig]); - const handleEditHost = (host: SSHHost) => { - setEditingHost(host); + const handleEditHost = async (host: SSHHost) => { setIsAddingHost(false); lastProcessedHostIdRef.current = host.id; + try { + const fullHost = await exportSSHHostWithCredentials(host.id); + setEditingHost({ ...host, ...fullHost } as SSHHost); + } catch { + setEditingHost(host); + } }; const handleAddHost = () => { diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx index f960231f..333a9582 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx @@ -413,6 +413,7 @@ export function HostManagerEditor({ sudoPassword: z.string().optional(), keepaliveInterval: z.number().min(0).max(300000).optional(), keepaliveCountMax: z.number().min(0).max(100).optional(), + autoTmux: z.boolean(), }) .optional(), forceKeyboardInteractive: z.boolean().optional(), @@ -448,6 +449,16 @@ export function HostManagerEditor({ }), ) .optional(), + macAddress: z.string().optional(), + portKnockSequence: z + .array( + z.object({ + port: z.coerce.number().min(1).max(65535), + protocol: z.enum(["tcp", "udp"]).default("tcp"), + delay: z.coerce.number().min(0).max(60000).default(100), + }), + ) + .optional(), enableDocker: z.boolean().default(false), domain: z.string().optional(), security: z.string().optional(), @@ -507,11 +518,7 @@ export function HostManagerEditor({ }); } if (!data.keyType) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("hosts.keyTypeRequired", "Key type is required"), - path: ["keyType"], - }); + data.keyType = "auto"; } } else if (data.authType === "credential") { if (!data.credentialId) { @@ -522,19 +529,6 @@ export function HostManagerEditor({ }); } } - - data.tunnelConnections.forEach((connection, index) => { - if ( - connection.endpointHost && - !sshConfigurations.includes(connection.endpointHost) - ) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: t("hosts.mustSelectValidSshConfig"), - path: ["tunnelConnections", index, "endpointHost"], - }); - } - }); }); type FormData = z.infer; @@ -580,6 +574,8 @@ export function HostManagerEditor({ socks5Username: "", socks5Password: "", socks5ProxyChain: [], + macAddress: "", + portKnockSequence: [], enableDocker: false, domain: "", security: "any", @@ -821,6 +817,10 @@ export function HostManagerEditor({ socks5ProxyChain: Array.isArray(cleanedHost.socks5ProxyChain) ? cleanedHost.socks5ProxyChain : [], + macAddress: cleanedHost.macAddress || "", + portKnockSequence: Array.isArray(cleanedHost.portKnockSequence) + ? cleanedHost.portKnockSequence + : [], enableDocker: Boolean(cleanedHost.enableDocker), domain: (cleanedHost as any).domain || "", security: (cleanedHost as any).security || "any", @@ -1086,6 +1086,7 @@ export function HostManagerEditor({ socks5Username: "general", socks5Password: "general", socks5ProxyChain: "general", + portKnockSequence: "general", quickActions: "general", enableTerminal: "terminal", terminalConfig: "terminal", @@ -1248,7 +1249,10 @@ export function HostManagerEditor({ }; const handleSshConfigClick = (config: string, index: number) => { - form.setValue(`tunnelConnections.${index}.endpointHost`, config); + form.setValue(`tunnelConnections.${index}.endpointHost`, config, { + shouldValidate: true, + shouldDirty: true, + }); setSshConfigDropdownOpen((prev) => ({ ...prev, [index]: false })); }; diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx index f5eccb73..071a77fd 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx @@ -35,14 +35,15 @@ import { updateSSHHost, renameFolder, exportSSHHostWithCredentials, + exportAllSSHHosts, getSSHFolders, updateFolderMetadata, deleteAllHostsInFolder, refreshServerPolling, isElectron, getConfiguredServerUrl, + getGuacamoleDpi, getGuacamoleTokenFromHost, - getGuacamoleToken, logActivity, } from "@/ui/main-axios.ts"; import { useServerStatus } from "@/ui/contexts/ServerStatusContext"; @@ -89,6 +90,8 @@ import { Monitor, MessagesSquare, Eye, + ChevronsDownUp, + ChevronsUpDown, } from "lucide-react"; import type { SSHHost, @@ -843,6 +846,39 @@ export function HostManagerViewer({ URL.revokeObjectURL(url); }; + const [exporting, setExporting] = useState(false); + + const handleExportAll = () => { + confirmWithToast( + t("hosts.exportAllSensitiveWarning"), + async () => { + setExporting(true); + try { + const data = await exportAllSSHHosts(); + const blob = new Blob([JSON.stringify(data, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `termix-hosts-export-${new Date().toISOString().slice(0, 10)}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success( + t("hosts.exportedAllHosts", { count: data.hosts.length }), + ); + } catch { + toast.error(t("hosts.failedToExportAllHosts")); + } finally { + setExporting(false); + } + }, + "destructive", + ); + }; + const handleJsonImport = async ( event: React.ChangeEvent, ) => { @@ -1160,6 +1196,15 @@ export function HostManagerViewer({ + + @@ -1215,6 +1260,28 @@ export function HostManagerViewer({ {selectionMode ? t("hosts.exitSelectMode") : t("hosts.selectMode")} + @@ -1921,19 +1988,9 @@ export function HostManagerViewer({ | "vnc" | "telnet"; const result = - await getGuacamoleToken({ - protocol, - hostname: host.ip, - port: host.port, - username: host.username, - password: host.password, - domain: host.domain, - security: host.security, - ignoreCert: - host.ignoreCert, - guacamoleConfig: - host.guacamoleConfig as any, - }); + await getGuacamoleTokenFromHost( + host.id, + ); addTab({ type: protocol, @@ -1951,6 +2008,9 @@ export function HostManagerViewer({ security: host.security, "ignore-cert": host.ignoreCert, + dpi: getGuacamoleDpi( + host, + ), }, }); diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx index 01e6017a..1f555792 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx @@ -224,49 +224,73 @@ export function HostGeneralTab({ )} /> + {connectionType !== "vnc" && ( + { + const isCredentialAuth = authTab === "credential"; + const credentialId = form.watch("credentialId"); + const overrideEnabled = form.watch("overrideCredentialUsername"); + const selectedCredential = credentials.find( + (c) => c.id === credentialId, + ); + const shouldDisable = + isCredentialAuth && + selectedCredential?.username && + !overrideEnabled; + + return ( + + {t("hosts.username")} + + { + field.onChange(e.target.value); + if ( + isCredentialAuth && + selectedCredential && + !selectedCredential.username && + e.target.value.trim() !== "" + ) { + form.setValue("overrideCredentialUsername", true); + } + }} + onBlur={(e) => { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + + ); + }} + /> + )} + +
{ - const isCredentialAuth = authTab === "credential"; - const credentialId = form.watch("credentialId"); - const overrideEnabled = form.watch("overrideCredentialUsername"); - const selectedCredential = credentials.find( - (c) => c.id === credentialId, - ); - const shouldDisable = - isCredentialAuth && - selectedCredential?.username && - !overrideEnabled; - - return ( - - {t("hosts.username")} - - { - field.onChange(e.target.value); - if ( - isCredentialAuth && - selectedCredential && - !selectedCredential.username && - e.target.value.trim() !== "" - ) { - form.setValue("overrideCredentialUsername", true); - } - }} - onBlur={(e) => { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - - ); - }} + name="macAddress" + render={({ field }) => ( + + {t("hosts.macAddress")} + + { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + {t("hosts.macAddressDesc")} + + )} />
@@ -1438,6 +1462,120 @@ export function HostGeneralTab({ )} + + + {t("hosts.portKnocking")} + +

+ {t("hosts.portKnockingDesc")} +

+ { + const sequence = field.value || []; + return ( +
+ {sequence.map( + ( + knock: { + port: number; + protocol?: string; + delay?: number; + }, + index: number, + ) => ( +
+ { + const updated = [...sequence]; + updated[index] = { + ...updated[index], + port: parseInt(e.target.value) || 0, + }; + field.onChange(updated); + }} + className="w-24" + /> + + { + const updated = [...sequence]; + updated[index] = { + ...updated[index], + delay: parseInt(e.target.value) || 0, + }; + field.onChange(updated); + }} + className="w-20" + /> + + ms + + +
+ ), + )} + +
+ ); + }} + /> +
+
)} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx index ceea0209..66b55a7a 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx @@ -47,9 +47,11 @@ import { import { TerminalPreview } from "@/ui/desktop/apps/features/terminal/TerminalPreview.tsx"; import type { HostTerminalTabProps } from "./shared/tab-types"; import React from "react"; +import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; export function HostTerminalTab({ form, snippets, t }: HostTerminalTabProps) { const [snippetPopoverOpen, setSnippetPopoverOpen] = React.useState(false); + const { setPreviewTerminalTheme } = useTabs() as any; return (
- + setPreviewTerminalTheme(null)} + > {Object.entries(TERMINAL_THEMES).map(([key, theme]) => ( - + setPreviewTerminalTheme(key)} + > {theme.name} ))} @@ -637,6 +645,25 @@ export function HostTerminalTab({ form, snippets, t }: HostTerminalTabProps) { /> )} + ( + +
+ {t("hosts.autoTmux")} + {t("hosts.autoTmuxDesc")} +
+ + + +
+ )} + /> + ([]); const [snippetFolders, setSnippetFolders] = useState([]); + const [sharedSnippetsList, setSharedSnippetsList] = useState< + Array<{ + id: number; + name: string; + content: string; + description: string | null; + folder: string | null; + ownerUsername: string; + }> + >([]); + const [shareDialogSnippet, setShareDialogSnippet] = useState( + null, + ); + const [shareTargetType, setShareTargetType] = useState<"user" | "role">( + "user", + ); + const [shareUsers, setShareUsers] = useState< + Array<{ id: string; username: string }> + >([]); + const [shareRoles, setShareRoles] = useState< + Array<{ id: number; name: string; displayName?: string }> + >([]); + const [shareTargetId, setShareTargetId] = useState(""); + const [shareAccessList, setShareAccessList] = useState([]); const [loading, setLoading] = useState(true); const [showDialog, setShowDialog] = useState(false); const [editingSnippet, setEditingSnippet] = useState(null); @@ -222,6 +255,9 @@ export function SSHToolsSidebar({ const [searchQuery, setSearchQuery] = useState(""); const [historyRefreshCounter, setHistoryRefreshCounter] = useState(0); const commandHistoryScrollRef = React.useRef(null); + const [commandHistoryEnabled, setCommandHistoryEnabled] = useState( + () => localStorage.getItem("commandHistoryTracking") === "true", + ); const [splitMode, setSplitMode] = useState< "none" | "2" | "3" | "4" | "5" | "6" @@ -240,6 +276,14 @@ export function SSHToolsSidebar({ const startWidthRef = React.useRef(sidebarWidth); const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal"); + + useEffect(() => { + const terminalIds = new Set(terminalTabs.map((t) => t.id)); + setSelectedSnippetTabIds((prev) => + prev.filter((id) => terminalIds.has(id)), + ); + }, [terminalTabs.length]); + const activeUiTab = tabs.find((tab) => tab.id === currentTab); const activeTerminal = activeUiTab?.type === "terminal" ? activeUiTab : undefined; @@ -327,6 +371,17 @@ export function SSHToolsSidebar({ } }, [isOpen, activeTab, activeTerminalHostId]); + useEffect(() => { + const handleChange = () => { + setCommandHistoryEnabled( + localStorage.getItem("commandHistoryTracking") === "true", + ); + }; + window.addEventListener("commandHistoryTrackingChanged", handleChange); + return () => + window.removeEventListener("commandHistoryTrackingChanged", handleChange); + }, []); + const filteredCommands = searchQuery ? commandHistory.filter((cmd) => cmd.toLowerCase().includes(searchQuery.toLowerCase()), @@ -625,16 +680,19 @@ export function SSHToolsSidebar({ const fetchSnippets = async () => { try { setLoading(true); - const [snippetsData, foldersData] = await Promise.all([ + const [snippetsData, foldersData, sharedData] = await Promise.all([ getSnippets(), getSnippetFolders(), + getSharedSnippets().catch(() => ({ sharedSnippets: [] })), ]); setSnippets(Array.isArray(snippetsData) ? snippetsData : []); setSnippetFolders(Array.isArray(foldersData) ? foldersData : []); + setSharedSnippetsList(sharedData.sharedSnippets || []); } catch { toast.error(t("snippets.failedToFetch")); setSnippets([]); setSnippetFolders([]); + setSharedSnippetsList([]); } finally { setLoading(false); } @@ -675,6 +733,63 @@ export function SSHToolsSidebar({ ); }; + const handleOpenShareDialog = async (snippet: Snippet) => { + setShareDialogSnippet(snippet); + setShareTargetId(""); + setShareTargetType("user"); + try { + const [usersData, rolesData, accessData] = await Promise.all([ + getUserList(), + getRoles(), + getSnippetAccess(snippet.id), + ]); + setShareUsers( + (usersData?.users || []).map((u: Record) => ({ + id: u.id as string, + username: u.username as string, + })), + ); + setShareRoles( + (rolesData?.roles || []).map( + (r: { id: number; name: string; displayName?: string }) => r, + ), + ); + setShareAccessList(accessData.accessList || []); + } catch { + toast.error(t("snippets.failedToLoadShareData")); + } + }; + + const handleShare = async () => { + if (!shareDialogSnippet || !shareTargetId) return; + try { + await shareSnippet(shareDialogSnippet.id, { + targetType: shareTargetType, + targetUserId: shareTargetType === "user" ? shareTargetId : undefined, + targetRoleId: + shareTargetType === "role" ? parseInt(shareTargetId) : undefined, + }); + toast.success(t("snippets.shareSuccess")); + const accessData = await getSnippetAccess(shareDialogSnippet.id); + setShareAccessList(accessData.accessList || []); + setShareTargetId(""); + } catch { + toast.error(t("snippets.shareFailed")); + } + }; + + const handleRevokeSnippetAccess = async (accessId: number) => { + if (!shareDialogSnippet) return; + try { + await revokeSnippetAccess(shareDialogSnippet.id, accessId); + toast.success(t("snippets.revokeSuccess")); + const accessData = await getSnippetAccess(shareDialogSnippet.id); + setShareAccessList(accessData.accessList || []); + } catch { + toast.error(t("snippets.revokeFailed")); + } + }; + const handleSubmit = async () => { const errors = { name: !formData.name.trim(), @@ -736,7 +851,7 @@ export function SSHToolsSidebar({ selectedSnippetTabIds.forEach((tabId) => { const tab = tabs.find((t: TabData) => t.id === tabId); if (tab?.terminalRef?.current?.sendInput) { - tab.terminalRef.current.sendInput(snippet.content + "\n"); + tab.terminalRef.current.sendInput(snippet.content + "\r"); } }); toast.success( @@ -1296,6 +1411,30 @@ export function SSHToolsSidebar({ }) : t("snippets.executeOnCurrent")}

+
+ + +
{terminalTabs.map((tab) => ( + + +

+ {t("snippets.runTooltip")} +

+
+ + + + + + +

+ {t("snippets.copyTooltip")} +

+
+
+
+
+ ))} + + )} + + ); + })()} + {Array.from(groupSnippetsByFolder()).map( ([folderName, folderSnippets]) => { const folderMetadata = snippetFolders.find( @@ -1584,6 +1835,27 @@ export function SSHToolsSidebar({

+ + + + + + +

+ {t("snippets.shareTooltip")} +

+
+
))} @@ -1602,131 +1874,151 @@ export function SSHToolsSidebar({ value="command-history" className="flex flex-col flex-1 overflow-hidden" > -
-
- - { - setSearchQuery(e.target.value); - }} - className="pl-10 pr-10" - /> - {searchQuery && ( - - )} + {!commandHistoryEnabled ? ( +
+
+

+ {t("commandHistory.disabledTitle")} +

+

+ {t("commandHistory.disabledDescription")} +

+
-

- {t("commandHistory.tabHint")} -

-
- -
- {historyError ? ( -
-
-

- {t("commandHistory.error")} -

-

- {historyError} -

+ ) : ( + <> +
+
+ + { + setSearchQuery(e.target.value); + }} + className="pl-10 pr-10" + /> + {searchQuery && ( + + )}
- -
- ) : !activeTerminal ? ( -
- -

- {t("commandHistory.noTerminal")}{" "} -

-

- {t("commandHistory.noTerminalHint")} +

+ {t("commandHistory.tabHint")}

- ) : isHistoryLoading && commandHistory.length === 0 ? ( -
- -

- {t("commandHistory.loading")}{" "} -

-
- ) : filteredCommands.length === 0 ? ( -
- {searchQuery ? ( - <> - + +
+ {historyError ? ( +
+
+

+ {t("commandHistory.error")} +

+

+ {historyError} +

+
+ +
+ ) : !activeTerminal ? ( +
+

- {t("commandHistory.noResults")} + {t("commandHistory.noTerminal")}{" "}

- {t("commandHistory.noResultsHint", { - query: searchQuery, - })} + {t("commandHistory.noTerminalHint")}

- +
+ ) : isHistoryLoading && + commandHistory.length === 0 ? ( +
+ +

+ {t("commandHistory.loading")}{" "} +

+
+ ) : filteredCommands.length === 0 ? ( +
+ {searchQuery ? ( + <> + +

+ {t("commandHistory.noResults")} +

+

+ {t("commandHistory.noResultsHint", { + query: searchQuery, + })} +

+ + ) : ( + <> +

+ {t("commandHistory.empty")} +

+

+ {t("commandHistory.emptyHint")} +

+ + )} +
) : ( - <> -

- {t("commandHistory.empty")} -

-

- {t("commandHistory.emptyHint")} -

- +
+ {filteredCommands.map((command, index) => ( +
+
+ + handleCommandSelect(command) + } + title={command} + > + {command} + + +
+
+ ))} +
)}
- ) : ( -
- {filteredCommands.map((command, index) => ( -
-
- handleCommandSelect(command)} - title={command} - > - {command} - - -
-
- ))} -
- )} -
+ + )}
)} + + {shareDialogSnippet && ( +
+
+
+

+ {t("snippets.shareSnippet")} +

+ +
+ +

+ {shareDialogSnippet.name} +

+ +
+
+ + + + + +
+
+ + {shareAccessList.length > 0 && ( +
+ + {t("snippets.currentAccess")} + + {shareAccessList.map((access) => ( +
+ + {access.username || + access.roleDisplayName || + access.roleName} + + +
+ ))} +
+ )} +
+
+ )} ); } diff --git a/src/ui/desktop/authentication/Auth.tsx b/src/ui/desktop/authentication/Auth.tsx index 7c019ee4..92a4b87c 100644 --- a/src/ui/desktop/authentication/Auth.tsx +++ b/src/ui/desktop/authentication/Auth.tsx @@ -31,6 +31,7 @@ import { getOIDCAuthorizeUrl, verifyTOTPLogin, getServerConfig, + saveServerConfig, isElectron, getEmbeddedServerStatus, isEmbeddedMode, @@ -56,6 +57,7 @@ interface AuthProps extends React.ComponentProps<"div"> { loggedIn: boolean; authLoading: boolean; setDbError: (error: string | null) => void; + dbError?: string | null; onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; @@ -72,6 +74,7 @@ export function Auth({ loggedIn, authLoading, setDbError, + dbError: _dbError, onAuthSuccess, ...props }: AuthProps) { @@ -80,6 +83,10 @@ export function Auth({ const isDarkMode = theme === "dark" || + theme === "dracula" || + theme === "gentlemansChoice" || + theme === "midnightEspresso" || + theme === "catppuccinMocha" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); const lineColor = isDarkMode ? "#151517" : "#f9f9f9"; @@ -93,7 +100,7 @@ export function Auth({ return true; } } catch (_e) { - return false; + return true; } return false; }; @@ -147,7 +154,24 @@ export function Auth({ const handleElectronAuthSuccess = useCallback(async () => { try { - const meRes = await getUserInfo(); + let retries = 5; + let meRes = null; + while (retries-- > 0) { + try { + meRes = await getUserInfo(); + break; + } catch (err: any) { + const isNoServer = + err?.code === "NO_SERVER_CONFIGURED" || + err?.message?.includes("no-server-configured"); + if (isNoServer && retries > 0) { + await new Promise((r) => setTimeout(r, 500)); + } else { + throw err; + } + } + } + if (!meRes) throw new Error("Failed to get user info"); setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); @@ -255,12 +279,6 @@ export function Auth({ }); }, [setDbError, firstUserToastShown, showServerConfig, t]); - useEffect(() => { - if (!registrationAllowed && !internalLoggedIn) { - toast.warning(t("messages.registrationDisabled")); - } - }, [registrationAllowed, internalLoggedIn, t]); - useEffect(() => { if (!passwordLoginAllowed && oidcConfigured && tab !== "external") { setTab("external"); @@ -658,6 +676,11 @@ export function Auth({ if (success) { setOidcLoading(true); + const urlToken = urlParams.get("token"); + if (urlToken && (isElectron() || isInElectronWebView())) { + localStorage.setItem("jwt", urlToken); + } + getUserInfo() .then((meRes) => { if (isInElectronWebView()) { @@ -683,6 +706,13 @@ export function Auth({ } } + if (isElectron()) { + const token = getCookie("jwt"); + if (token) { + localStorage.setItem("jwt", token); + } + } + setInternalLoggedIn(true); setLoggedIn(true); setIsAdmin(!!meRes.is_admin); @@ -772,7 +802,12 @@ export function Auth({ getEmbeddedServerStatus(), ]); - if (status?.embedded && status?.running && !config?.serverUrl) { + if ( + status?.embedded && + status?.running && + config && + !config.serverUrl + ) { setCurrentServerUrl(""); setShowServerConfig(false); return; @@ -816,7 +851,11 @@ export function Auth({ onServerConfigured={() => { window.location.reload(); }} - onUseEmbedded={() => { + onUseEmbedded={async () => { + await saveServerConfig({ + serverUrl: "", + lastUpdated: new Date().toISOString(), + }); setShowServerConfig(false); setCurrentServerUrl(""); }} diff --git a/src/ui/desktop/authentication/ElectronLoginForm.tsx b/src/ui/desktop/authentication/ElectronLoginForm.tsx index efbe8ae7..504f930e 100644 --- a/src/ui/desktop/authentication/ElectronLoginForm.tsx +++ b/src/ui/desktop/authentication/ElectronLoginForm.tsx @@ -1,9 +1,8 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Button } from "@/components/ui/button.tsx"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; import { useTranslation } from "react-i18next"; import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react"; -import { getCookie } from "@/ui/main-axios.ts"; +import { setCookie } from "@/ui/main-axios.ts"; interface ElectronLoginFormProps { serverUrl: string; @@ -20,64 +19,75 @@ export function ElectronLoginForm({ const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isAuthenticating, setIsAuthenticating] = useState(false); + const isAuthenticatingRef = useRef(false); const iframeRef = useRef(null); const hasAuthenticatedRef = useRef(false); const [currentUrl, setCurrentUrl] = useState(serverUrl); const hasLoadedOnce = useRef(false); - + const onAuthSuccessRef = useRef(onAuthSuccess); useEffect(() => { - localStorage.removeItem("jwt"); - }, []); + onAuthSuccessRef.current = onAuthSuccess; + }, [onAuthSuccess]); + const handleAuthToken = useCallback( + async (token: string) => { + if (hasAuthenticatedRef.current || isAuthenticatingRef.current) return; + hasAuthenticatedRef.current = true; + isAuthenticatingRef.current = true; + setIsAuthenticating(true); + + try { + setCookie("jwt", token); + await new Promise((resolve) => setTimeout(resolve, 300)); + onAuthSuccessRef.current(); + } catch (_err) { + setError(t("errors.authTokenSaveFailed")); + isAuthenticatingRef.current = false; + setIsAuthenticating(false); + hasAuthenticatedRef.current = false; + } + }, + [t], + ); + + // postMessage from server's Auth.tsx (works if server has postMessage code) useEffect(() => { const handleMessage = async (event: MessageEvent) => { try { - const serverOrigin = new URL(serverUrl).origin; - if (event.origin !== serverOrigin) { - return; + if (!event.data || typeof event.data !== "object") return; + const { type, token, platform } = event.data; + if (type === "AUTH_SUCCESS" && token && platform === "desktop") { + await handleAuthToken(token); } - - if (event.data && typeof event.data === "object") { - const data = event.data; - - if ( - data.type === "AUTH_SUCCESS" && - data.token && - !hasAuthenticatedRef.current && - !isAuthenticating - ) { - hasAuthenticatedRef.current = true; - setIsAuthenticating(true); - - try { - localStorage.setItem("jwt", data.token); - - const savedToken = localStorage.getItem("jwt"); - if (!savedToken) { - throw new Error("Failed to save JWT to localStorage"); - } - - await new Promise((resolve) => setTimeout(resolve, 500)); - - onAuthSuccess(); - } catch (err) { - setError(t("errors.authTokenSaveFailed")); - setIsAuthenticating(false); - hasAuthenticatedRef.current = false; - } - } - } - } catch (err) { - console.error("Authentication operation failed:", err); + } catch (_err) { + // ignore } }; window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [handleAuthToken]); - return () => { - window.removeEventListener("message", handleMessage); - }; - }, [serverUrl, isAuthenticating, onAuthSuccess, t]); + // Poll iframe localStorage via main process executeJavaScriptInFrame (bypasses cross-origin) + useEffect(() => { + if (loading) return; + const electronAPI = (window as any).electronAPI; + if (!electronAPI?.invoke) return; + + const poll = setInterval(async () => { + if (hasAuthenticatedRef.current || isAuthenticatingRef.current) return; + try { + const token = await electronAPI.invoke("get-iframe-jwt"); + if (token && token.length > 20) { + await handleAuthToken(token); + } + } catch (_e) { + // ignore + } + }, 500); + + return () => clearInterval(poll); + }, [loading, handleAuthToken]); useEffect(() => { const iframe = iframeRef.current; @@ -92,116 +102,9 @@ export function ElectronLoginForm({ if (iframe.contentWindow) { setCurrentUrl(iframe.contentWindow.location.href); } - } catch (e) { + } catch (_e) { setCurrentUrl(serverUrl); } - - try { - const injectedScript = ` - (function() { - let hasNotified = false; - - function postJWTToParent(token, source) { - if (hasNotified) { - return; - } - hasNotified = true; - - try { - window.parent.postMessage({ - type: 'AUTH_SUCCESS', - token: token, - source: source, - platform: 'desktop', - timestamp: Date.now() - }, '*'); - } catch (e) { - } - } - - function checkAuth() { - try { - const localToken = localStorage.getItem('jwt'); - if (localToken && localToken.length > 20) { - postJWTToParent(localToken, 'localStorage'); - return true; - } - - const sessionToken = sessionStorage.getItem('jwt'); - if (sessionToken && sessionToken.length > 20) { - postJWTToParent(sessionToken, 'sessionStorage'); - return true; - } - - const cookies = document.cookie; - if (cookies && cookies.length > 0) { - const cookieArray = cookies.split('; '); - const tokenCookie = cookieArray.find(row => row.startsWith('jwt=')); - - if (tokenCookie) { - const token = tokenCookie.split('=')[1]; - if (token && token.length > 20) { - postJWTToParent(token, 'cookie'); - return true; - } - } - } - } catch (error) { - } - return false; - } - - const originalSetItem = localStorage.setItem; - localStorage.setItem = function(key, value) { - originalSetItem.apply(this, arguments); - if (key === 'jwt' && value && value.length > 20 && !hasNotified) { - setTimeout(() => checkAuth(), 100); - } - }; - - const originalSessionSetItem = sessionStorage.setItem; - sessionStorage.setItem = function(key, value) { - originalSessionSetItem.apply(this, arguments); - if (key === 'jwt' && value && value.length > 20 && !hasNotified) { - setTimeout(() => checkAuth(), 100); - } - }; - - const intervalId = setInterval(() => { - if (hasNotified) { - clearInterval(intervalId); - return; - } - if (checkAuth()) { - clearInterval(intervalId); - } - }, 500); - - setTimeout(() => { - clearInterval(intervalId); - }, 300000); - - setTimeout(() => checkAuth(), 500); - })(); - `; - - try { - if (iframe.contentWindow) { - try { - iframe.contentWindow.eval(injectedScript); - } catch (evalError) { - iframe.contentWindow.postMessage( - { type: "INJECT_SCRIPT", script: injectedScript }, - "*", - ); - } - } - } catch (err) { - console.error("Authentication operation failed:", err); - } - } catch (err) { - console.error("Authentication operation failed:", err); - } }; const handleError = () => { @@ -213,12 +116,11 @@ export function ElectronLoginForm({ iframe.addEventListener("load", handleLoad); iframe.addEventListener("error", handleError); - return () => { iframe.removeEventListener("load", handleLoad); iframe.removeEventListener("error", handleError); }; - }, [t]); + }, [serverUrl, t]); const handleRefresh = () => { if (iframeRef.current) { @@ -228,44 +130,47 @@ export function ElectronLoginForm({ } }; - const handleBack = () => { - onChangeServer(); - }; - const displayUrl = currentUrl.replace(/^https?:\/\//, ""); const isEmbeddedServer = serverUrl.includes("localhost:30001"); return (
-
- - {!isEmbeddedServer && ( -
- - {displayUrl} - -
- )} - {isEmbeddedServer &&
} - -
+ {isAuthenticating && ( +
+ +
+ )} - {error && ( + {!isAuthenticating && ( +
+ + {!isEmbeddedServer && ( +
+ + {displayUrl} + +
+ )} + {isEmbeddedServer &&
} + +
+ )} + + {error && !isAuthenticating && (
@@ -275,7 +180,7 @@ export function ElectronLoginForm({
)} - {loading && ( + {loading && !isAuthenticating && (
)} -
+