From 5cd9de9ac519ca0448e65790ac7d97b838d0cd39 Mon Sep 17 00:00:00 2001 From: Karmaa <88517757+LukeGus@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:42:00 -0500 Subject: [PATCH] v1.6.0 (#221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add documentation in Chinese language (#160) * Update file naming and structure for mobile support * Add conditional desktop/mobile rendering * Mobile terminal * Fix overwritten i18n (#161) * Add comprehensive Chinese internationalization support - Implemented i18n framework with react-i18next for multi-language support - Added Chinese (zh) and English (en) translation files with comprehensive coverage - Localized Admin interface, authentication flows, and error messages - Translated FileManager operations and UI elements - Updated HomepageAuth component with localized authentication messages - Localized LeftSidebar navigation and host management - Added language switcher component (shown after login only) - Configured default language as English with Chinese as secondary option - Localized TOTPSetup two-factor authentication interface - Updated Docker build to include translation files - Achieved 95%+ UI localization coverage across core components Co-Authored-By: Claude * Extend Chinese localization coverage to Host Manager components - Added comprehensive translations for HostManagerHostViewer component - Localized all host management UI text including import/export features - Translated error messages and confirmation dialogs for host operations - Added translations for HostManagerHostEditor validation messages - Localized connection details, organization settings, and form labels - Fixed syntax error in FileManagerOperations component - Achieved near-complete localization of SSH host management interface - Updated placeholders and tooltips for better user guidance Co-Authored-By: Claude * Complete comprehensive Chinese localization for Termix - Added full localization support for Tunnel components (connected/disconnected states, retry messages) - Localized all tunnel status messages and connection errors - Added translations for port forwarding UI elements - Verified Server, TopNavbar, and Tab components already have complete i18n support - Achieved 99%+ localization coverage across entire application - All core UI components now fully support Chinese and English languages This completes the comprehensive internationalization effort for the Termix SSH management platform. Co-Authored-By: Claude * Localize additional Host Manager components and authentication settings - Added translations for all authentication options (Password, Key, SSH Private Key) - Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager) - Translated Upload/Update Key button states - Localized Host Viewer and Add/Edit Host tab labels - Added Chinese translations for all host management settings - Fixed duplicate translation keys in JSON files Co-Authored-By: Claude * Extend localization coverage to UI components and common strings - Added comprehensive common translations (online/offline, success/error, etc.) - Localized status indicator component with all status states - Updated FileManagerLeftSidebar toast messages for rename/delete operations - Added translations for UI elements (close, toggle sidebar, etc.) - Expanded placeholder translations for form inputs - Added Chinese translations for all new common strings - Improved consistency across component status messages Co-Authored-By: Claude * Complete Chinese localization for remaining UI components - Add comprehensive Chinese translations for Host Manager component - Translate all form labels, buttons, and descriptions - Add translations for SSH configuration warnings and instructions - Localize tunnel connection settings and port forwarding options - Localize SSH Tools panel - Translate key recording functionality - Add translations for settings and configuration options - Translate homepage welcome messages and navigation elements - Add Chinese translations for login success messages - Localize "Updates & Releases" section title - Translate sidebar "Host Manager" button - Fix translation key display issues - Remove duplicate translation keys in both language files - Ensure all components properly reference translation keys - Fix hosts.tunnelConnections key mapping This completes the full Chinese localization of the Termix application, achieving near 100% UI translation coverage while maintaining English as the default language. * Complete final Chinese localization for Host Manager tunnel configuration - Add Chinese translations for authentication UI elements - Translate "Authentication", "Password", and "Key" tab labels - Localize SSH private key and key password fields - Add translations for key type selector - Localize tunnel connection configuration descriptions - Translate retry attempts and retry interval descriptions - Add dynamic tunnel forwarding description with port parameters - Localize endpoint SSH configuration labels - Fix missing translation keys - Add "upload" translation for file upload button - Ensure all FormLabel and FormDescription elements use translation keys This completes the comprehensive Chinese localization of the entire Termix application, achieving 100% UI translation coverage. * Fix PR feedback: Improve Profile section translations and UX - Fixed password reset translations in Profile section - Moved language selector from TopNavbar to Profile page - Added profile.selectPreferredLanguage translation key - Improved user experience for language preferences * Apply critical OIDC and notification system fixes while preserving i18n - Merge OIDC authentication fixes from 3877e90: * Enhanced JWKS discovery mechanism with multiple backup URLs * Better support for non-standard OIDC providers (Authentik, etc.) * Improved error handling for "Failed to get user information" - Migrate to unified Sonner toast notification system: * Replace custom success/error state management * Remove redundant alert state variables * Consistent user feedback across all components - Improve code quality and function naming conventions - PRESERVE all existing i18n functionality and Chinese translations 🤖 Generated with Claude Code Co-Authored-By: Claude * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Update env * Fix users.ts and schema for override * Convert web app to Electron desktop application - Add Electron main process with developer tools support - Create preload script for secure context bridge - Configure electron-builder for packaging - Update Vite config for Electron compatibility (base: './') - Add environment variable support for API host configuration - Fix i18n to use relative paths for Electron file protocol - Restore multi-port backend architecture (8081-8085) - Add enhanced backend startup script with port checking - Update package.json with Electron dependencies and build scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Complete Electron desktop application implementation - Add backend auto-start functionality in main process - Fix authentication token storage for Electron environment - Implement localStorage-based token management in Electron - Add proper Electron environment detection via preload script - Fix WebSocket connections for terminal functionality - Resolve font file loading issues in packaged application - Update API endpoints to work with backend auto-start - Streamline build scripts with unified electron:package command - Fix better-sqlite3 native module compatibility issues - Ensure all services start automatically in production mode 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Remove releases folder from git and force Desktop UI. * Improve mobile support with half-baked custom keyboard * Fix API routing * Upgrade mobile keyboard with more keys. * Add cross-platform support and clean up obsolete files - Add electron-packager scripts for Windows, macOS, and Linux - Include universal architecture support for macOS - Add electron:package:all for building all platforms - Remove obsolete start-backend.sh script (replaced by Electron auto-start) - Improve ignore patterns to exclude repo-images folder - Add platform-specific icon configurations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix build system by removing electron-builder dependency - Remove electron-builder and @electron/rebuild packages to resolve build errors - Clean up package.json scripts that depend on electron-builder - Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx - All build commands now work correctly: - npm run build (frontend + backend) - npm run build:frontend - npm run build:backend - npm run electron:package (using electron-packager) The build system is now stable and functional without signing requirements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude Co-authored-by: ZacharyZcR Co-authored-by: LukeGus * Mobile UI improvement * Electron dev (#185) * Add comprehensive Chinese internationalization support - Implemented i18n framework with react-i18next for multi-language support - Added Chinese (zh) and English (en) translation files with comprehensive coverage - Localized Admin interface, authentication flows, and error messages - Translated FileManager operations and UI elements - Updated HomepageAuth component with localized authentication messages - Localized LeftSidebar navigation and host management - Added language switcher component (shown after login only) - Configured default language as English with Chinese as secondary option - Localized TOTPSetup two-factor authentication interface - Updated Docker build to include translation files - Achieved 95%+ UI localization coverage across core components Co-Authored-By: Claude * Extend Chinese localization coverage to Host Manager components - Added comprehensive translations for HostManagerHostViewer component - Localized all host management UI text including import/export features - Translated error messages and confirmation dialogs for host operations - Added translations for HostManagerHostEditor validation messages - Localized connection details, organization settings, and form labels - Fixed syntax error in FileManagerOperations component - Achieved near-complete localization of SSH host management interface - Updated placeholders and tooltips for better user guidance Co-Authored-By: Claude * Complete comprehensive Chinese localization for Termix - Added full localization support for Tunnel components (connected/disconnected states, retry messages) - Localized all tunnel status messages and connection errors - Added translations for port forwarding UI elements - Verified Server, TopNavbar, and Tab components already have complete i18n support - Achieved 99%+ localization coverage across entire application - All core UI components now fully support Chinese and English languages This completes the comprehensive internationalization effort for the Termix SSH management platform. Co-Authored-By: Claude * Localize additional Host Manager components and authentication settings - Added translations for all authentication options (Password, Key, SSH Private Key) - Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager) - Translated Upload/Update Key button states - Localized Host Viewer and Add/Edit Host tab labels - Added Chinese translations for all host management settings - Fixed duplicate translation keys in JSON files Co-Authored-By: Claude * Extend localization coverage to UI components and common strings - Added comprehensive common translations (online/offline, success/error, etc.) - Localized status indicator component with all status states - Updated FileManagerLeftSidebar toast messages for rename/delete operations - Added translations for UI elements (close, toggle sidebar, etc.) - Expanded placeholder translations for form inputs - Added Chinese translations for all new common strings - Improved consistency across component status messages Co-Authored-By: Claude * Complete Chinese localization for remaining UI components - Add comprehensive Chinese translations for Host Manager component - Translate all form labels, buttons, and descriptions - Add translations for SSH configuration warnings and instructions - Localize tunnel connection settings and port forwarding options - Localize SSH Tools panel - Translate key recording functionality - Add translations for settings and configuration options - Translate homepage welcome messages and navigation elements - Add Chinese translations for login success messages - Localize "Updates & Releases" section title - Translate sidebar "Host Manager" button - Fix translation key display issues - Remove duplicate translation keys in both language files - Ensure all components properly reference translation keys - Fix hosts.tunnelConnections key mapping This completes the full Chinese localization of the Termix application, achieving near 100% UI translation coverage while maintaining English as the default language. * Complete final Chinese localization for Host Manager tunnel configuration - Add Chinese translations for authentication UI elements - Translate "Authentication", "Password", and "Key" tab labels - Localize SSH private key and key password fields - Add translations for key type selector - Localize tunnel connection configuration descriptions - Translate retry attempts and retry interval descriptions - Add dynamic tunnel forwarding description with port parameters - Localize endpoint SSH configuration labels - Fix missing translation keys - Add "upload" translation for file upload button - Ensure all FormLabel and FormDescription elements use translation keys This completes the comprehensive Chinese localization of the entire Termix application, achieving 100% UI translation coverage. * Fix PR feedback: Improve Profile section translations and UX - Fixed password reset translations in Profile section - Moved language selector from TopNavbar to Profile page - Added profile.selectPreferredLanguage translation key - Improved user experience for language preferences * Apply critical OIDC and notification system fixes while preserving i18n - Merge OIDC authentication fixes from 3877e90: * Enhanced JWKS discovery mechanism with multiple backup URLs * Better support for non-standard OIDC providers (Authentik, etc.) * Improved error handling for "Failed to get user information" - Migrate to unified Sonner toast notification system: * Replace custom success/error state management * Remove redundant alert state variables * Consistent user feedback across all components - Improve code quality and function naming conventions - PRESERVE all existing i18n functionality and Chinese translations 🤖 Generated with Claude Code Co-Authored-By: Claude * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Update env * Fix users.ts and schema for override * Convert web app to Electron desktop application - Add Electron main process with developer tools support - Create preload script for secure context bridge - Configure electron-builder for packaging - Update Vite config for Electron compatibility (base: './') - Add environment variable support for API host configuration - Fix i18n to use relative paths for Electron file protocol - Restore multi-port backend architecture (8081-8085) - Add enhanced backend startup script with port checking - Update package.json with Electron dependencies and build scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Complete Electron desktop application implementation - Add backend auto-start functionality in main process - Fix authentication token storage for Electron environment - Implement localStorage-based token management in Electron - Add proper Electron environment detection via preload script - Fix WebSocket connections for terminal functionality - Resolve font file loading issues in packaged application - Update API endpoints to work with backend auto-start - Streamline build scripts with unified electron:package command - Fix better-sqlite3 native module compatibility issues - Ensure all services start automatically in production mode 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Remove releases folder from git and force Desktop UI. * Improve mobile support with half-baked custom keyboard * Fix API routing * Upgrade mobile keyboard with more keys. * Add cross-platform support and clean up obsolete files - Add electron-packager scripts for Windows, macOS, and Linux - Include universal architecture support for macOS - Add electron:package:all for building all platforms - Remove obsolete start-backend.sh script (replaced by Electron auto-start) - Improve ignore patterns to exclude repo-images folder - Add platform-specific icon configurations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix build system by removing electron-builder dependency - Remove electron-builder and @electron/rebuild packages to resolve build errors - Clean up package.json scripts that depend on electron-builder - Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx - All build commands now work correctly: - npm run build (frontend + backend) - npm run build:frontend - npm run build:backend - npm run electron:package (using electron-packager) The build system is now stable and functional without signing requirements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude Co-authored-by: ZacharyZcR Co-authored-by: LukeGus Co-authored-by: Karmaa <88517757+LukeGus@users.noreply.github.com> * Add navigation and hardcoded hosts * Update mobile sidebar to use API, add auth and tab system to mobile. * Update sidebar state * Mobile support (#190) * Add vibration to keyboard * Fix keyboard keys * Fix keyboard keys * Fix keyboard keys * Rename files, improve keyboard usability * Improve keyboard view and fix various issues with it * Add mobile chinese translation * Disable OS keyboard from appearing * Fix fit addon not resizing with "more" on keyboard * Disable OS keyboard on terminal load * Merge Luke and Zac * feat: add export option for ssh hosts (#173) (#187) * Update issue templates * feat: add export JSON option for SSH hosts (#173) --------- Co-authored-by: Karmaa <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus * feat(profile): display version number from .env in profile menu (#182) * feat(profile): display version number from .env in profile menu * Update version checking process --------- Co-authored-by: LukeGus * Add pretier * feat(auth): Add password visibility toggle to auth forms (#166) * added hide and unhide password button * Undo admin settings changes --------- Co-authored-by: LukeGus * Re-added password input * Remove encrpytion, improve logging and merge interfaces. * Improve logging (backend and frontend) and added dedicde OIDC clear * feat: Added option to paste private key (#203) * Improve logging frontend/backend, fix host form being reversed. * Improve logging more, fix credentials sync issues, migrate more to be toasts * Improve logging more, fix credentials sync issues, migrate more to be toasts * More error to toast migration * Remove more inline styles and run npm updates * Update homepage appearing over everything and terminal incorrect bg * Improved server stat generation and UI by caching and supporting more platforms * Update mobile app with the same stat changes and remove rate limiting * Put user profle in its own tab, add code rabbit support * Improve code rabbit yaml * Update chinese translation and fix z indexs causing delay to hide * Bump vite from 7.1.3 to 7.1.5 (#204) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.3 to 7.1.5. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.1.5 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update read me * Update electron builder and fix mobile terminal background * Update logo, move translations, update electron building. * Remove backend from electon, switching to server manager * Add electron server configurator * Fix backend builder on Dockerfile * Fix langauge file for Dockerfile * Fix architecture issues in Dockerfile * Fix architecture issues in Dockerfile * Fix architecture issues in Dockerfile * Fix backend building for docker image * Add electron builder * Fix node starting in entrypoint and remove release from electron build * Remove double packaing in electron build * Fix folder nesting for electron gbuilder * Fix native module docker build (better-sql and bcrypt) * Fix api routes and missing translations and improve reconnection for terminals * Update read me for new installation method * Update CONTRIBUTING.md with color scheme * Fix terrminal not closing afer 3 tries * Fix electronm api routing, fikx ssh not connecting, and OIDC redirect errors * Fix more electron API issues (ssh/oidc), make server manager force API check, and login saving. * Add electron API routes * Fix more electron APi routes and issues * Hide admin settings on electron and fix server manager URl verification * Hide admin settings on electron and fix server manager URl verification * Fix admin setting visiblity on electron * Add links to docs in respective places * Migrate all getCookies to use main-axios. * Migrate all isElectron to use main-axios. * Clean up backend files * Clean up frontend files and read me translations * Run prettier * Fix terminal in web, and update translations and prep for release. * Update API to work on devs and remove random letter * Run prettier * Update read me for release * Update read me for release * Fixed delete issue (ready for release) * Ensure retention days for artifact upload are set --------- Signed-off-by: dependabot[bot] Co-authored-by: starry <115192496+sky22333@users.noreply.github.com> Co-authored-by: ZacharyZcR Co-authored-by: Claude Co-authored-by: ZacharyZcR Co-authored-by: Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com> Co-authored-by: Abhilash Gandhamalla <150357125+AbhilashG12@users.noreply.github.com> Co-authored-by: jedi04 <78037206+jedi04@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .coderabbit.yaml | 578 + .env | 3 +- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 32 + .github/ISSUE_TEMPLATE/feature_request.md | 19 + .github/dependabot.yml | 4 +- .github/workflows/docker-image.yml | 4 +- .github/workflows/electron-build.yml | 6 +- .gitignore | 2 + .prettierignore | 3 + .prettierrc | 1 + CONTRIBUTING.md | 89 +- README-CN.md | 109 + README.md | 44 +- components.json | 2 +- docker/Dockerfile | 30 +- docker/docker-compose.yml | 2 +- docker/entrypoint.sh | 4 +- docker/nginx.conf | 30 +- electron-builder.json | 44 + electron/main.cjs | 334 + electron/preload.js | 29 + eslint.config.js | 20 +- openapi.json | 4065 +++---- package-lock.json | 9507 +++++++++++++++-- package.json | 27 +- public/favicon.ico | Bin 226046 -> 171947 bytes public/icon.icns | Bin 0 -> 125606 bytes public/icon.ico | Bin 0 -> 361102 bytes public/icon.png | Bin 0 -> 16007 bytes public/icon.svg | 81 +- public/icons/1024x1024.png | Bin 0 -> 56926 bytes public/icons/128x128.png | Bin 0 -> 4072 bytes public/icons/16x16.png | Bin 0 -> 483 bytes public/icons/24x24.png | Bin 0 -> 749 bytes public/icons/256x256.png | Bin 0 -> 8597 bytes public/icons/32x32.png | Bin 0 -> 986 bytes public/icons/48x48.png | Bin 0 -> 1554 bytes public/icons/512x512.png | Bin 0 -> 20523 bytes public/icons/64x64.png | Bin 0 -> 1989 bytes public/icons/icon.icns | Bin 0 -> 125606 bytes public/icons/icon.ico | Bin 0 -> 361102 bytes src/App.tsx | 228 - src/backend/database/database.ts | 462 +- src/backend/database/db/index.ts | 686 +- src/backend/database/db/schema.ts | 226 +- src/backend/database/routes/alerts.ts | 419 +- src/backend/database/routes/credentials.ts | 664 ++ src/backend/database/routes/ssh.ts | 1645 +-- src/backend/database/routes/users.ts | 2572 +++-- src/backend/ssh/file-manager.ts | 2128 ++-- src/backend/ssh/server-stats.ts | 1235 ++- src/backend/ssh/terminal.ts | 847 +- src/backend/ssh/tunnel.ts | 1961 ++-- src/backend/starter.ts | 107 +- src/backend/utils/logger.ts | 174 + src/components/theme-provider.tsx | 104 +- src/components/ui/accordion.tsx | 20 +- src/components/ui/alert.tsx | 22 +- src/components/ui/badge.tsx | 18 +- src/components/ui/button-group.tsx | 42 +- src/components/ui/button.tsx | 29 +- src/components/ui/card.tsx | 26 +- src/components/ui/checkbox.tsx | 14 +- src/components/ui/dropdown-menu.tsx | 198 + src/components/ui/form.tsx | 79 +- src/components/ui/input.tsx | 10 +- src/components/ui/label.tsx | 12 +- src/components/ui/password-input.tsx | 41 + src/components/ui/popover.tsx | 18 +- src/components/ui/progress.tsx | 12 +- src/components/ui/resizable.tsx | 26 +- src/components/ui/scroll-area.tsx | 14 +- src/components/ui/select.tsx | 44 +- src/components/ui/separator.tsx | 14 +- src/components/ui/shadcn-io/status/index.tsx | 50 +- src/components/ui/sheet.tsx | 40 +- src/components/ui/sidebar.tsx | 244 +- src/components/ui/skeleton.tsx | 6 +- src/components/ui/sonner.tsx | 12 +- src/components/ui/switch.tsx | 14 +- src/components/ui/table.tsx | 30 +- src/components/ui/tabs.tsx | 20 +- src/components/ui/textarea.tsx | 24 + src/components/ui/tooltip.tsx | 20 +- src/hooks/use-confirmation.ts | 68 + src/hooks/use-mobile.ts | 24 +- src/i18n/i18n.ts | 54 +- src/index.css | 275 +- src/lib/frontend-logger.ts | 388 + src/lib/utils.ts | 6 +- {public => src}/locales/en/translation.json | 239 +- {public => src}/locales/zh/translation.json | 292 +- src/main.tsx | 84 +- src/types/index.ts | 440 + src/ui/Admin/AdminSettings.tsx | 450 - .../File Manager/FIleManagerTopNavbar.tsx | 24 - src/ui/Apps/File Manager/FileManager.tsx | 692 -- .../File Manager/FileManagerFileEditor.tsx | 350 - .../Apps/File Manager/FileManagerHomeView.tsx | 210 - .../File Manager/FileManagerLeftSidebar.tsx | 574 - .../FileManagerLeftSidebarFileViewer.tsx | 110 - .../File Manager/FileManagerOperations.tsx | 626 -- .../Apps/File Manager/FileManagerTabList.tsx | 52 - src/ui/Apps/Host Manager/HostManager.tsx | 103 - .../Host Manager/HostManagerHostEditor.tsx | 1038 -- .../Host Manager/HostManagerHostViewer.tsx | 489 - src/ui/Apps/Server/Server.tsx | 289 - src/ui/Apps/Terminal/Terminal.tsx | 418 - src/ui/Apps/Tunnel/Tunnel.tsx | 204 - src/ui/Apps/Tunnel/TunnelObject.tsx | 490 - src/ui/Apps/Tunnel/TunnelViewer.tsx | 93 - src/ui/Desktop/Admin/AdminSettings.tsx | 690 ++ .../Apps/Credentials/CredentialEditor.tsx | 849 ++ .../Apps/Credentials/CredentialSelector.tsx | 226 + .../Apps/Credentials/CredentialViewer.tsx | 533 + .../Apps/Credentials/CredentialsManager.tsx | 692 ++ .../File Manager/FIleManagerTopNavbar.tsx | 26 + .../Desktop/Apps/File Manager/FileManager.tsx | 713 ++ .../File Manager/FileManagerFileEditor.tsx | 338 + .../Apps/File Manager/FileManagerHomeView.tsx | 234 + .../File Manager/FileManagerLeftSidebar.tsx | 630 ++ .../FileManagerLeftSidebarFileViewer.tsx | 128 + .../File Manager/FileManagerOperations.tsx | 805 ++ .../Apps/File Manager/FileManagerTabList.tsx | 62 + .../Desktop/Apps/Host Manager/HostManager.tsx | 142 + .../Apps/Host Manager/HostManagerEditor.tsx | 1507 +++ .../Apps/Host Manager/HostManagerViewer.tsx | 1102 ++ src/ui/Desktop/Apps/Server/Server.tsx | 478 + src/ui/Desktop/Apps/Terminal/Terminal.tsx | 634 ++ src/ui/Desktop/Apps/Tunnel/Tunnel.tsx | 206 + src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx | 533 + src/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx | 77 + src/ui/Desktop/DesktopApp.tsx | 198 + src/ui/Desktop/Electron Only/ServerConfig.tsx | 233 + src/ui/Desktop/Homepage/Homepage.tsx | 155 + src/ui/Desktop/Homepage/HomepageAlertCard.tsx | 157 + .../Desktop/Homepage/HomepageAlertManager.tsx | 179 + src/ui/Desktop/Homepage/HomepageAuth.tsx | 998 ++ src/ui/Desktop/Homepage/HompageUpdateLog.tsx | 182 + src/ui/Desktop/Navigation/AppView.tsx | 560 + .../Desktop/Navigation/Hosts/FolderCard.tsx | 91 + src/ui/Desktop/Navigation/Hosts/Host.tsx | 110 + src/ui/Desktop/Navigation/LeftSidebar.tsx | 569 + src/ui/Desktop/Navigation/Tabs/Tab.tsx | 165 + src/ui/Desktop/Navigation/Tabs/TabContext.tsx | 173 + .../Desktop/Navigation/Tabs/TabDropdown.tsx | 113 + src/ui/Desktop/Navigation/TopNavbar.tsx | 489 + .../Desktop/User}/LanguageSwitcher.tsx | 24 +- src/ui/Desktop/User/PasswordReset.tsx | 290 + src/ui/Desktop/User/TOTPSetup.tsx | 473 + src/ui/Desktop/User/UserProfile.tsx | 264 + src/ui/Homepage/Homepage.tsx | 170 - src/ui/Homepage/HomepageAlertCard.tsx | 153 - src/ui/Homepage/HomepageAlertManager.tsx | 179 - src/ui/Homepage/HomepageAuth.tsx | 801 -- src/ui/Homepage/HompageUpdateLog.tsx | 172 - .../Mobile/Apps/Navigation/BottomNavbar.tsx | 54 + .../Apps/Navigation/Hosts/FolderCard.tsx | 92 + src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx | 100 + src/ui/Mobile/Apps/Navigation/LeftSidebar.tsx | 254 + .../Apps/Navigation/Tabs/TabContext.tsx | 103 + src/ui/Mobile/Apps/Terminal/Terminal.tsx | 372 + .../Mobile/Apps/Terminal/TerminalKeyboard.tsx | 203 + src/ui/Mobile/Apps/Terminal/kb-dark-theme.css | 40 + src/ui/Mobile/Homepage/HomepageAuth.tsx | 892 ++ src/ui/Mobile/MobileApp.tsx | 209 + src/ui/Mobile/Navigation/BottomNavbar.tsx | 56 + src/ui/Mobile/Navigation/Hosts/FolderCard.tsx | 92 + src/ui/Mobile/Navigation/Hosts/Host.tsx | 100 + src/ui/Mobile/Navigation/LeftSidebar.tsx | 254 + src/ui/Mobile/Navigation/Tabs/TabContext.tsx | 103 + src/ui/Navigation/AppView.tsx | 553 - src/ui/Navigation/Hosts/FolderCard.tsx | 81 - src/ui/Navigation/Hosts/Host.tsx | 112 - src/ui/Navigation/LeftSidebar.tsx | 539 - src/ui/Navigation/Tabs/Tab.tsx | 142 - src/ui/Navigation/Tabs/TabContext.tsx | 135 - src/ui/Navigation/TopNavbar.tsx | 455 - src/ui/User/PasswordReset.tsx | 243 - src/ui/User/TOTPSetup.tsx | 439 - src/ui/User/UserProfile.tsx | 179 - src/ui/main-axios.ts | 2126 ++-- tsconfig.app.json | 4 +- tsconfig.json | 2 +- tsconfig.node.json | 2 +- vite.config.ts | 11 +- 187 files changed, 42370 insertions(+), 20790 deletions(-) create mode 100644 .coderabbit.yaml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 README-CN.md create mode 100644 electron-builder.json create mode 100644 electron/main.cjs create mode 100644 electron/preload.js create mode 100644 public/icon.icns create mode 100644 public/icon.ico create mode 100644 public/icon.png create mode 100644 public/icons/1024x1024.png create mode 100644 public/icons/128x128.png create mode 100644 public/icons/16x16.png create mode 100644 public/icons/24x24.png create mode 100644 public/icons/256x256.png create mode 100644 public/icons/32x32.png create mode 100644 public/icons/48x48.png create mode 100644 public/icons/512x512.png create mode 100644 public/icons/64x64.png create mode 100644 public/icons/icon.icns create mode 100644 public/icons/icon.ico delete mode 100644 src/App.tsx create mode 100644 src/backend/database/routes/credentials.ts create mode 100644 src/backend/utils/logger.ts create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/password-input.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/hooks/use-confirmation.ts create mode 100644 src/lib/frontend-logger.ts rename {public => src}/locales/en/translation.json (74%) rename {public => src}/locales/zh/translation.json (76%) create mode 100644 src/types/index.ts delete mode 100644 src/ui/Admin/AdminSettings.tsx delete mode 100644 src/ui/Apps/File Manager/FIleManagerTopNavbar.tsx delete mode 100644 src/ui/Apps/File Manager/FileManager.tsx delete mode 100644 src/ui/Apps/File Manager/FileManagerFileEditor.tsx delete mode 100644 src/ui/Apps/File Manager/FileManagerHomeView.tsx delete mode 100644 src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx delete mode 100644 src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx delete mode 100644 src/ui/Apps/File Manager/FileManagerOperations.tsx delete mode 100644 src/ui/Apps/File Manager/FileManagerTabList.tsx delete mode 100644 src/ui/Apps/Host Manager/HostManager.tsx delete mode 100644 src/ui/Apps/Host Manager/HostManagerHostEditor.tsx delete mode 100644 src/ui/Apps/Host Manager/HostManagerHostViewer.tsx delete mode 100644 src/ui/Apps/Server/Server.tsx delete mode 100644 src/ui/Apps/Terminal/Terminal.tsx delete mode 100644 src/ui/Apps/Tunnel/Tunnel.tsx delete mode 100644 src/ui/Apps/Tunnel/TunnelObject.tsx delete mode 100644 src/ui/Apps/Tunnel/TunnelViewer.tsx create mode 100644 src/ui/Desktop/Admin/AdminSettings.tsx create mode 100644 src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx create mode 100644 src/ui/Desktop/Apps/Credentials/CredentialSelector.tsx create mode 100644 src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx create mode 100644 src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FileManager.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx create mode 100644 src/ui/Desktop/Apps/File Manager/FileManagerTabList.tsx create mode 100644 src/ui/Desktop/Apps/Host Manager/HostManager.tsx create mode 100644 src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx create mode 100644 src/ui/Desktop/Apps/Host Manager/HostManagerViewer.tsx create mode 100644 src/ui/Desktop/Apps/Server/Server.tsx create mode 100644 src/ui/Desktop/Apps/Terminal/Terminal.tsx create mode 100644 src/ui/Desktop/Apps/Tunnel/Tunnel.tsx create mode 100644 src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx create mode 100644 src/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx create mode 100644 src/ui/Desktop/DesktopApp.tsx create mode 100644 src/ui/Desktop/Electron Only/ServerConfig.tsx create mode 100644 src/ui/Desktop/Homepage/Homepage.tsx create mode 100644 src/ui/Desktop/Homepage/HomepageAlertCard.tsx create mode 100644 src/ui/Desktop/Homepage/HomepageAlertManager.tsx create mode 100644 src/ui/Desktop/Homepage/HomepageAuth.tsx create mode 100644 src/ui/Desktop/Homepage/HompageUpdateLog.tsx create mode 100644 src/ui/Desktop/Navigation/AppView.tsx create mode 100644 src/ui/Desktop/Navigation/Hosts/FolderCard.tsx create mode 100644 src/ui/Desktop/Navigation/Hosts/Host.tsx create mode 100644 src/ui/Desktop/Navigation/LeftSidebar.tsx create mode 100644 src/ui/Desktop/Navigation/Tabs/Tab.tsx create mode 100644 src/ui/Desktop/Navigation/Tabs/TabContext.tsx create mode 100644 src/ui/Desktop/Navigation/Tabs/TabDropdown.tsx create mode 100644 src/ui/Desktop/Navigation/TopNavbar.tsx rename src/{components => ui/Desktop/User}/LanguageSwitcher.tsx (54%) create mode 100644 src/ui/Desktop/User/PasswordReset.tsx create mode 100644 src/ui/Desktop/User/TOTPSetup.tsx create mode 100644 src/ui/Desktop/User/UserProfile.tsx delete mode 100644 src/ui/Homepage/Homepage.tsx delete mode 100644 src/ui/Homepage/HomepageAlertCard.tsx delete mode 100644 src/ui/Homepage/HomepageAlertManager.tsx delete mode 100644 src/ui/Homepage/HomepageAuth.tsx delete mode 100644 src/ui/Homepage/HompageUpdateLog.tsx create mode 100644 src/ui/Mobile/Apps/Navigation/BottomNavbar.tsx create mode 100644 src/ui/Mobile/Apps/Navigation/Hosts/FolderCard.tsx create mode 100644 src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx create mode 100644 src/ui/Mobile/Apps/Navigation/LeftSidebar.tsx create mode 100644 src/ui/Mobile/Apps/Navigation/Tabs/TabContext.tsx create mode 100644 src/ui/Mobile/Apps/Terminal/Terminal.tsx create mode 100644 src/ui/Mobile/Apps/Terminal/TerminalKeyboard.tsx create mode 100644 src/ui/Mobile/Apps/Terminal/kb-dark-theme.css create mode 100644 src/ui/Mobile/Homepage/HomepageAuth.tsx create mode 100644 src/ui/Mobile/MobileApp.tsx create mode 100644 src/ui/Mobile/Navigation/BottomNavbar.tsx create mode 100644 src/ui/Mobile/Navigation/Hosts/FolderCard.tsx create mode 100644 src/ui/Mobile/Navigation/Hosts/Host.tsx create mode 100644 src/ui/Mobile/Navigation/LeftSidebar.tsx create mode 100644 src/ui/Mobile/Navigation/Tabs/TabContext.tsx delete mode 100644 src/ui/Navigation/AppView.tsx delete mode 100644 src/ui/Navigation/Hosts/FolderCard.tsx delete mode 100644 src/ui/Navigation/Hosts/Host.tsx delete mode 100644 src/ui/Navigation/LeftSidebar.tsx delete mode 100644 src/ui/Navigation/Tabs/Tab.tsx delete mode 100644 src/ui/Navigation/Tabs/TabContext.tsx delete mode 100644 src/ui/Navigation/TopNavbar.tsx delete mode 100644 src/ui/User/PasswordReset.tsx delete mode 100644 src/ui/User/TOTPSetup.tsx delete mode 100644 src/ui/User/UserProfile.tsx diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..e2e5500d --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,578 @@ +language: "en" +early_access: false +reviews: + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + path_filters: + - "!**/.xml" + - "!**/__generated__/**" + - "!**/generated/**" + - "!**/*.json" + - "!**/*.svg" + - "!**/*.png" + - "!**/*.jpg" + - "!**/*.gif" + - "!**/*.lock" + - "!**/node_modules/**" + - "!**/dist/**" + - "!**/public/locales/**" + - "!**/repo-images/**" + path_instructions: + - path: "**/*.{ts,tsx}" + instructions: | + Review TypeScript and React code for Termix server management platform. Key considerations: + + **Architecture & Patterns:** + - Follow the established multi-port backend architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Use proper separation between Desktop and Mobile UI components + - Maintain consistent state management patterns with React hooks and context + - Follow the established tab-based navigation system + + **Database & Backend:** + - Use Drizzle ORM with SQLite for database operations + - Implement proper JWT authentication middleware patterns + - Follow the established API error handling patterns in main-axios.ts + - Use proper logging with the structured logger system (apiLogger, authLogger, sshLogger, etc.) + - Maintain proper input validation and sanitization + + **UI/UX Guidelines:** + - Use Shadcn/UI components with Tailwind CSS for consistent styling + - Follow the established theme system with dark/light mode support + - Use proper responsive design patterns for Desktop/Mobile views + - Implement proper loading states and error handling + - Use the established confirmation patterns with useConfirmation hook + - Use CSS variables and classes from index.css instead of hardcoding colors + - Follow the established color token system (--primary, --secondary, --background, etc.) + - Use proper Tailwind CSS classes instead of inline styles + - Implement proper focus states and accessibility indicators + + **SSH & Security:** + - Implement proper SSH connection management with session handling + - Use secure credential storage and management patterns + - Follow the established authentication flow (password, key, credential-based) + - Implement proper file operation security and validation + + **Code Quality:** + - Use proper TypeScript types from the centralized types/index.ts + - Follow the established API patterns in main-axios.ts + - Implement proper error boundaries and fallback UI + - Use proper React patterns (hooks, context, refs) + - Maintain consistent naming conventions and file organization + - All API interactions should go through main-axios.ts functions, not direct axios calls + - Use proper component interaction patterns through props and callbacks + - Follow the established state management patterns with useState and useEffect + - Use proper event handling and form submission patterns + + **Bug Detection & Fixes:** + - Identify and fix memory leaks in useEffect cleanup functions + - Fix missing dependency arrays in useEffect hooks + - Resolve infinite re-render loops caused by object/array dependencies + - Fix race conditions in async operations and API calls + - Identify and fix potential null/undefined access errors + - Fix improper state updates that cause stale closures + - Resolve event handler memory leaks and proper cleanup + - Fix improper error handling that could crash the application + - Identify and fix accessibility issues and keyboard navigation problems + - Fix responsive design issues and mobile compatibility problems + - Resolve TypeScript type errors and missing type definitions + - Fix improper form validation and submission handling + - Identify and fix performance issues and unnecessary re-renders + - Fix improper API error handling and user feedback + - Resolve authentication state inconsistencies and token management issues + + **Internationalization:** + - Use the i18next translation system with proper t() function calls + - Support both English and Chinese locales + - Use proper translation keys and fallbacks + + **Performance:** + - Implement proper cleanup in useEffect hooks + - Use proper memoization where appropriate + - Follow the established polling and refresh patterns + - Implement proper connection pooling and resource management + + **Specific to Termix:** + - Maintain compatibility with Electron and web versions + - Follow the established terminal integration patterns with xterm.js + - Use proper file manager operations and SSH session management + - Implement proper tunnel management and status tracking + - Follow the established alert and notification system patterns + + Highlight any deviations from these patterns and suggest improvements for maintainability, security, and user experience. + + **General Bug Detection & Fixes:** + - Identify and fix common React bugs (missing keys, improper state updates, memory leaks) + - Fix TypeScript errors and type safety issues + - Resolve accessibility violations and keyboard navigation problems + - Fix responsive design issues and mobile compatibility problems + - Identify and fix performance bottlenecks and unnecessary re-renders + - Fix improper error handling that could crash the application + - Resolve security vulnerabilities and improper data handling + - Fix improper form validation and user input handling + - Identify and fix race conditions and async operation issues + - Fix improper cleanup and resource management + - Resolve improper authentication and authorization issues + - Fix improper API error handling and user feedback + - Identify and fix potential null/undefined access errors + - Fix improper event handling and memory leaks + - Resolve improper state management and data flow issues + + - path: "**/backend/**/*.{ts,js}" + instructions: | + Review backend code for Termix server management platform. Key considerations: + + **Backend Architecture:** + - Follow the multi-port microservice architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Use Express.js with proper middleware patterns + - Implement proper CORS and security headers + - Use proper request/response logging with structured logging + + **Database Operations:** + - Use Drizzle ORM with proper schema definitions + - Implement proper database migrations and schema updates + - Use proper transaction handling for critical operations + - Follow the established database connection patterns + + **Authentication & Security:** + - Implement proper JWT token validation and refresh + - Use bcryptjs for password hashing with proper salt rounds + - Implement proper input validation and sanitization + - Use proper CORS configuration for security + - Implement proper rate limiting and security headers + + **SSH Operations:** + - Use ssh2 library with proper connection management + - Implement proper SSH key handling and validation + - Use proper session management and cleanup + - Implement proper error handling for SSH operations + - Use proper file operation security and validation + + **API Design:** + - Follow RESTful API patterns with proper HTTP status codes + - Implement proper error response formatting + - Use proper request/response validation + - Implement proper API versioning and backward compatibility + - All API routes should be defined in main-axios.ts, not scattered across components + - Use the established multi-port API architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Follow the established error handling patterns with handleApiError function + - Use proper structured logging with service-specific loggers (apiLogger, authLogger, sshLogger, etc.) + + **Logging & Monitoring:** + - Use the structured logging system with proper context + - Implement proper error tracking and reporting + - Use proper performance monitoring and metrics + - Implement proper health checks and status endpoints + + Highlight any security vulnerabilities, performance issues, or architectural deviations. + + - path: "**/components/**/*.{ts,tsx}" + instructions: | + Review UI components for Termix server management platform. Key considerations: + + **Component Design:** + - Use Shadcn/UI components as the foundation + - Implement proper component composition and reusability + - Use proper TypeScript interfaces and prop types + - Follow the established component naming conventions + + **Styling & Theming:** + - Use Tailwind CSS with proper responsive design + - Implement proper dark/light theme support + - Use proper color tokens and design system consistency + - Implement proper accessibility features (ARIA labels, keyboard navigation) + - Use CSS variables from index.css instead of hardcoding colors (--primary, --secondary, --background, etc.) + - Follow the established color scheme and design tokens + - Use proper Tailwind CSS utility classes instead of custom CSS + - Implement proper focus states and hover effects + + **State Management:** + - Use proper React hooks and context patterns + - Implement proper state lifting and prop drilling avoidance + - Use proper memoization with useMemo and useCallback + - Implement proper cleanup in useEffect hooks + + **Form Handling:** + - Use react-hook-form with proper validation + - Implement proper form state management + - Use proper error handling and user feedback + - Implement proper accessibility for form elements + + **SSH Integration:** + - Implement proper SSH connection status indicators + - Use proper terminal integration with xterm.js + - Implement proper file manager operations + - Use proper tunnel status and management UI + + Highlight any UI/UX issues, accessibility problems, or performance concerns. + + - path: "**/types/**/*.{ts,js}" + instructions: | + Review type definitions for Termix server management platform. Key considerations: + + **Type Design:** + - Use proper TypeScript interfaces and type definitions + - Implement proper type safety and validation + - Use proper generic types and utility types + - Follow the established type naming conventions + + **API Types:** + - Define proper request/response types for all API endpoints + - Use proper error types and status codes + - Implement proper validation types and schemas + - Use proper pagination and filtering types + + **SSH Types:** + - Define proper SSH connection and configuration types + - Use proper tunnel and credential types + - Implement proper file operation types + - Use proper authentication and security types + + **Type Safety:** + - Ensure proper type coverage and completeness + - Use proper strict type checking + - Implement proper type narrowing and guards + - Use proper type assertions and casting + + Highlight any type safety issues, missing types, or type inconsistencies. + + - path: "**/hooks/**/*.{ts,tsx}" + instructions: | + Review custom hooks for Termix server management platform. Key considerations: + + **Hook Design:** + - Use proper React hooks patterns and conventions + - Implement proper hook composition and reusability + - Use proper TypeScript types for hook parameters and return values + - Follow the established hook naming conventions + + **State Management:** + - Implement proper state management with useState and useReducer + - Use proper context and provider patterns + - Implement proper state persistence and synchronization + - Use proper state cleanup and memory management + + **Side Effects:** + - Use proper useEffect patterns with proper dependencies + - Implement proper cleanup functions and resource management + - Use proper async operations and error handling + - Implement proper polling and refresh patterns + + **Performance:** + - Use proper memoization with useMemo and useCallback + - Implement proper debouncing and throttling + - Use proper lazy loading and code splitting + - Implement proper optimization patterns + + **SSH Integration:** + - Implement proper SSH connection management hooks + - Use proper terminal integration hooks + - Implement proper file manager operation hooks + - Use proper tunnel management hooks + + **Hook-Specific Bug Detection:** + - Fix missing cleanup functions in useEffect hooks that cause memory leaks + - Resolve infinite loops caused by incorrect dependency arrays + - Fix stale closure issues in event handlers and async operations + - Identify and fix improper state updates that cause unnecessary re-renders + - Fix race conditions in async hooks and API calls + - Resolve improper ref usage and null reference errors + - Fix improper context usage and provider nesting issues + - Identify and fix custom hook dependency issues + - Resolve improper memoization that causes stale data + - Fix improper error handling in custom hooks + + Highlight any hook design issues, performance problems, or reusability concerns. + + - path: "**/lib/**/*.{ts,js}" + instructions: | + Review utility libraries and helper functions for Termix server management platform. Key considerations: + + **Utility Functions:** + - Implement proper utility functions with clear purposes + - Use proper TypeScript types and JSDoc documentation + - Implement proper error handling and validation + - Follow the established utility naming conventions + + **Logging System:** + - Use proper structured logging with context and metadata + - Implement proper log levels and filtering + - Use proper log formatting and output + - Implement proper log rotation and cleanup + + **API Utilities:** + - Implement proper API client configuration and management + - Use proper request/response interceptors + - Implement proper error handling and retry logic + - Use proper authentication and authorization handling + - All API functions should be centralized in main-axios.ts + - Use proper service-specific API instances (sshHostApi, tunnelApi, fileManagerApi, statsApi, authApi) + - Follow the established error handling patterns with handleApiError function + - Use proper structured logging with service-specific loggers + + **Security Utilities:** + - Implement proper input validation and sanitization + - Use proper encryption and decryption functions + - Implement proper secure random generation + - Use proper security headers and CORS handling + + **SSH Utilities:** + - Implement proper SSH connection utilities + - Use proper SSH key handling and validation + - Implement proper SSH command execution + - Use proper SSH file operation utilities + + **Utility Bug Detection:** + - Fix improper error handling in utility functions that could crash the application + - Resolve null/undefined access errors in utility functions + - Fix improper input validation that could cause security vulnerabilities + - Identify and fix memory leaks in utility functions + - Fix improper async/await usage and promise handling + - Resolve improper type checking and validation errors + - Fix improper logging that could expose sensitive information + - Identify and fix performance bottlenecks in utility functions + - Fix improper data transformation and serialization issues + - Resolve improper configuration and environment variable handling + + Highlight any utility design issues, performance problems, or security concerns. + + - path: "**/main-axios.ts" + instructions: | + Review main-axios.ts API client configuration for Termix server management platform. Key considerations: + + **API Client Architecture:** + - Maintain the multi-port API architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Use proper service-specific API instances (sshHostApi, tunnelApi, fileManagerApi, statsApi, authApi) + - Implement proper API instance creation with createApiInstance function + - Use proper base URL configuration for different environments (dev, production, Electron) + + **Error Handling:** + - Use the centralized handleApiError function for consistent error handling + - Implement proper error classification (auth, network, validation, server errors) + - Use proper error logging with service-specific loggers + - Implement proper error response formatting and user-friendly messages + + **Request/Response Interceptors:** + - Implement proper JWT token handling in request interceptors + - Use proper request timing and performance logging + - Implement proper response logging and error tracking + - Use proper authentication token refresh and cleanup + + **API Function Organization:** + - Group API functions by service (SSH Host Management, Tunnel Management, File Manager, etc.) + - Use proper TypeScript types for all API functions + - Implement proper parameter validation and sanitization + - Use proper return type definitions and error handling + + **Authentication:** + - Implement proper JWT token management and refresh + - Use proper cookie handling for web and Electron environments + - Implement proper authentication state management + - Use proper token expiration and cleanup + + **Logging:** + - Use proper structured logging with context and metadata + - Implement proper request/response logging with performance metrics + - Use proper error logging with appropriate log levels + - Implement proper service-specific logger selection + + **Performance:** + - Implement proper request timeout and retry logic + - Use proper connection pooling and resource management + - Implement proper request deduplication and caching + - Use proper performance monitoring and metrics + + **Security:** + - Implement proper input validation and sanitization + - Use proper CORS and security header handling + - Implement proper authentication and authorization + - Use proper secure communication and data handling + + **API Bug Detection:** + - Fix improper error handling that could expose sensitive information + - Resolve race conditions in concurrent API calls + - Fix improper token management and authentication state issues + - Identify and fix memory leaks in API interceptors + - Fix improper request/response validation that could cause crashes + - Resolve improper timeout handling and retry logic + - Fix improper error response formatting and user feedback + - Identify and fix performance issues in API calls + - Fix improper request deduplication and caching issues + - Resolve improper authentication token refresh and cleanup + - Fix improper CORS and security header configuration + - Identify and fix potential security vulnerabilities in API handling + + Highlight any API design issues, error handling problems, or security concerns. + + - path: "**/electron/**/*.{ts,js,cjs}" + instructions: | + Review Electron application code for Termix server management platform. Key considerations: + + **Electron Architecture:** + - Use proper Electron main and renderer process separation + - Implement proper IPC (Inter-Process Communication) patterns + - Use proper security and sandboxing configurations + - Follow the established Electron best practices + + **Security:** + - Implement proper security policies and configurations + - Use proper context isolation and node integration + - Implement proper CSP and security headers + - Use proper authentication and authorization handling + + **Performance:** + - Implement proper memory management and cleanup + - Use proper resource optimization and caching + - Implement proper background processing and threading + - Use proper performance monitoring and profiling + + **Electron Bug Detection:** + - Fix improper IPC communication that could cause crashes + - Resolve memory leaks in Electron main and renderer processes + - Fix improper window management and lifecycle issues + - Identify and fix security vulnerabilities in Electron configuration + - Fix improper context isolation and node integration issues + - Resolve improper event handling and cleanup in Electron + - Fix improper file system access and permission issues + - Identify and fix performance issues in Electron processes + - Fix improper auto-updater and version management + - Resolve improper tray and menu functionality issues + - Fix improper security policies and CSP configuration + - Identify and fix potential security vulnerabilities in Electron setup + + Highlight any Electron-specific issues, security vulnerabilities, or performance problems. + + - path: "**/docker/**/*" + instructions: | + Review Docker configuration files for Termix server management platform. Key considerations: + + **Dockerfile Design:** + - Use proper multi-stage builds for optimization + - Implement proper layer caching and optimization + - Use proper security and minimal base images + - Follow the established Docker best practices + + **Security:** + - Implement proper user and permission management + - Use proper security scanning and vulnerability assessment + - Implement proper secrets and credential management + - Use proper network security and isolation + + **Performance:** + - Implement proper resource optimization and allocation + - Use proper caching and build optimization + - Implement proper monitoring and logging + - Use proper health checks and status monitoring + + **Docker Bug Detection:** + - Fix improper multi-stage build optimization that causes large images + - Resolve security vulnerabilities in base images and dependencies + - Fix improper volume and data persistence configuration + - Identify and fix resource limit and constraint issues + - Fix improper networking and port configuration + - Resolve improper environment variable and secret management + - Fix improper health check and status monitoring configuration + - Identify and fix performance issues in container startup + - Fix improper logging and monitoring configuration + - Resolve improper backup and recovery procedures + - Fix improper scaling and load balancing configuration + - Identify and fix potential security vulnerabilities in Docker setup + + Highlight any Docker configuration issues, security vulnerabilities, or performance problems. + + - path: "**/*.md" + instructions: | + Review documentation files for Termix server management platform. Key considerations: + + **Documentation Quality:** + - Ensure proper grammar, spelling, and clarity + - Use proper formatting and structure + - Implement proper code examples and snippets + - Follow the established documentation standards + + **Content Accuracy:** + - Ensure proper technical accuracy and completeness + - Use proper up-to-date information and examples + - Implement proper cross-references and links + - Use proper version and compatibility information + + **User Experience:** + - Ensure proper user-friendly language and explanations + - Use proper step-by-step instructions and guides + - Implement proper troubleshooting and FAQ sections + - Use proper visual aids and diagrams where appropriate + + Highlight any documentation issues, inaccuracies, or missing information. + + - path: "**/index.css" + instructions: | + Review index.css styling configuration for Termix server management platform. Key considerations: + + **CSS Variable System:** + - Define proper CSS custom properties for colors, spacing, and typography + - Use consistent naming conventions for CSS variables (--primary, --secondary, --background, etc.) + - Implement proper dark/light theme variable definitions + - Use proper semantic color naming (--destructive, --muted, --accent, etc.) + + **Design System:** + - Follow the established design token system + - Use proper color palette definitions with proper contrast ratios + - Implement proper typography scale and font family definitions + - Use proper spacing and sizing scale definitions + + **Theme Support:** + - Implement proper dark and light theme variable definitions + - Use proper CSS custom property fallbacks + - Implement proper theme switching support + - Use proper color scheme media queries + + **Component Styling:** + - Define proper base styles for common components + - Use proper utility classes and helper styles + - Implement proper responsive design utilities + - Use proper accessibility-focused styling + + **Color Management:** + - Avoid hardcoded color values, use CSS variables instead + - Implement proper color contrast and accessibility + - Use proper semantic color definitions + - Implement proper color state variations (hover, focus, active) + + **Typography:** + - Define proper font family and weight definitions + - Use proper line height and letter spacing + - Implement proper text size and hierarchy + - Use proper font loading and fallback strategies + + **Layout Utilities:** + - Define proper spacing and margin utilities + - Use proper flexbox and grid utilities + - Implement proper responsive breakpoint utilities + - Use proper container and layout helpers + + **Accessibility:** + - Implement proper focus indicators and states + - Use proper color contrast ratios + - Implement proper reduced motion support + - Use proper screen reader friendly styling + + **Performance:** + - Use efficient CSS selectors and properties + - Implement proper CSS organization and structure + - Use proper CSS custom property optimization + - Implement proper critical CSS and loading strategies + + Highlight any styling issues, accessibility problems, or design system inconsistencies. + auto_review: + enabled: true + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + - "DRAFT" + - "EXPERIMENTAL" + - "TEST" + drafts: false +chat: + auto_reply: true diff --git a/.env b/.env index c1e19f61..6f985423 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VERSION=1.5.0 \ No newline at end of file +VERSION=1.6.0 +VITE_API_HOST=localhost \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 14c1d8b7..908aaba5 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [LukeGus] \ No newline at end of file +github: [LukeGus] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..55f187ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help Termix improve +title: "[BUG]" +labels: bug +assignees: "" +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots/Logs** +If applicable, add screenshots or console/Docker logs to help explain your problem. + +**Environment (please complete the following information):** + +- Browser [e.g. chrome, safari] +- Version [e.g. 1.6.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..8f421adb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for Termix +title: "[FEATURE]" +labels: enhancement +assignees: "" +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 89762628..8bb2f443 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,7 +21,7 @@ updates: dependency-type: "production" update-types: - "minor" - + - package-ecosystem: "docker" directory: "/docker" schedule: @@ -37,4 +37,4 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" \ No newline at end of file + interval: "weekly" diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b2bf6f80..0cb53035 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -5,8 +5,8 @@ on: branches: - development paths-ignore: - - '**.md' - - '.gitignore' + - "**.md" + - ".gitignore" workflow_dispatch: inputs: tag_name: diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index fb9e6004..6f1bbbff 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -88,11 +88,13 @@ jobs: - name: Create Linux Portable zip run: | - zip -r Termix-Linux-Portable.zip release/linux-unpacked/* + cd release/linux-unpacked + zip -r ../../Termix-Linux-Portable.zip * + cd ../.. - name: Upload Linux Portable Artifact uses: actions/upload-artifact@v4 with: name: Termix-Linux-Portable path: Termix-Linux-Portable.zip - retention-days: 3 + retention-days: 30 diff --git a/.gitignore b/.gitignore index d0adddb5..9066858c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ dist-ssr *.sln *.sw? /db/ +/release/ +/.claude/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..1b8ac889 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a2a2e8a..d3e7f12f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing +\_# Contributing ## Prerequisites @@ -9,13 +9,13 @@ ## Installation 1. Clone the repository: - ```sh - git clone https://github.com/LukeGus/Termix - ``` + ```sh + git clone https://github.com/LukeGus/Termix + ``` 2. Install the dependencies: - ```sh - npm install - ``` + ```sh + npm install + ``` ## Running the development server @@ -23,10 +23,10 @@ Run the following commands: ```sh npm run dev -npx tsc -p tsconfig.node.json -node ./dist/backend/starter.js +npm run dev:backend ``` +a This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`. ## Contributing @@ -34,23 +34,74 @@ This will start the backend and the frontend Vite server. You can access Termix 1. **Fork the repository**: Click the "Fork" button at the top right of the [repository page](https://github.com/LukeGus/Termix). 2. **Create a new branch**: - ```sh - git checkout -b feature/my-new-feature - ``` + ```sh + git checkout -b feature/my-new-feature + ``` 3. **Make your changes**: Implement your feature, fix, or improvement. 4. **Commit your changes**: - ```sh - git commit -m "Add feature: my new feature" - ``` + ```sh + git commit -m "Feature request my new feature" + ``` 5. **Push to your fork**: - ```sh - git push origin feature/my-new-feature - ``` + ```sh + git push origin feature/my-feature-request + ``` 6. **Open a pull request**: Go to the original repository and create a PR with a clear description. ## 📝 Guidelines - Follow the existing code style. Use Tailwind CSS with shadcn components. +- Use the below color scheme with the respective CSS variable placed in the `className` of a div/component. - Place all API routes in the `main-axios.ts` file. Updating the `openapi.json` is unneeded. - Include meaningful commit messages. -- Link related issues when applicable. \ No newline at end of file +- Link related issues when applicable. +- `MobileApp.tsx` renders when the users screen width is less than 768px, otherwise it loads the usual `DesktopApp.tsx`. + +## Color Scheme + +### Background Colors + +| CSS Variable | Color Value | Usage | Description | +| ----------------------------- | ----------- | --------------------------- | ---------------------------------------- | +| `--color-dark-bg` | `#18181b` | Main dark background | Primary dark background color | +| `--color-dark-bg-darker` | `#0e0e10` | Darker backgrounds | Darker variant for panels and containers | +| `--color-dark-bg-darkest` | `#09090b` | Darkest backgrounds | Darkest background (terminal) | +| `--color-dark-bg-light` | `#141416` | Light dark backgrounds | Lighter variant of dark background | +| `--color-dark-bg-very-light` | `#101014` | Very light dark backgrounds | Very light variant of dark background | +| `--color-dark-bg-panel` | `#1b1b1e` | Panel backgrounds | Background for panels and cards | +| `--color-dark-bg-panel-hover` | `#232327` | Panel hover states | Background for panels on hover | + +### Element-Specific Backgrounds + +| CSS Variable | Color Value | Usage | Description | +| ------------------------ | ----------- | ------------------ | --------------------------------------------- | +| `--color-dark-bg-input` | `#222225` | Input fields | Background for input fields and form elements | +| `--color-dark-bg-button` | `#23232a` | Button backgrounds | Background for buttons and clickable elements | +| `--color-dark-bg-active` | `#1d1d1f` | Active states | Background for active/selected elements | +| `--color-dark-bg-header` | `#131316` | Header backgrounds | Background for headers and navigation bars | + +### Border Colors + +| CSS Variable | Color Value | Usage | Description | +| ---------------------------- | ----------- | --------------- | ---------------------------------------- | +| `--color-dark-border` | `#303032` | Default borders | Standard border color | +| `--color-dark-border-active` | `#2d2d30` | Active borders | Border color for active elements | +| `--color-dark-border-hover` | `#434345` | Hover borders | Border color on hover states | +| `--color-dark-border-light` | `#5a5a5d` | Light borders | Lighter border color for subtle elements | +| `--color-dark-border-medium` | `#373739` | Medium borders | Medium weight border color | +| `--color-dark-border-panel` | `#222224` | Panel borders | Border color for panels and cards | + +### Interactive States + +| CSS Variable | Color Value | Usage | Description | +| ------------------------ | ----------- | ----------------- | --------------------------------------------- | +| `--color-dark-hover` | `#2d2d30` | Hover states | Background color for hover effects | +| `--color-dark-active` | `#2a2a2c` | Active states | Background color for active elements | +| `--color-dark-pressed` | `#1a1a1c` | Pressed states | Background color for pressed/clicked elements | +| `--color-dark-hover-alt` | `#2a2a2d` | Alternative hover | Alternative hover state color | + +## Support + +If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support +channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) +repo. diff --git a/README-CN.md b/README-CN.md new file mode 100644 index 00000000..5cee2e1f --- /dev/null +++ b/README-CN.md @@ -0,0 +1,109 @@ +# 仓库统计 + +

+ English 英文 | + 中文 中文 +

+ +![GitHub Repo stars](https://img.shields.io/github/stars/LukeGus/Termix?style=flat&label=Stars) +![GitHub forks](https://img.shields.io/github/forks/LukeGus/Termix?style=flat&label=Forks) +![GitHub Release](https://img.shields.io/github/v/release/LukeGus/Termix?style=flat&label=Release) +Discord + +#### 核心技术 + +[![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) +[![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) +[![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) +[![Vite Badge](https://img.shields.io/badge/-Vite-646CFF?style=flat-square&labelColor=black&logo=vite&logoColor=646CFF)](#) +[![Tailwind CSS Badge](https://img.shields.io/badge/-TailwindCSS-38B2AC?style=flat-square&labelColor=black&logo=tailwindcss&logoColor=38B2AC)](#) +[![Docker Badge](https://img.shields.io/badge/-Docker-2496ED?style=flat-square&labelColor=black&logo=docker&logoColor=2496ED)](#) +[![SQLite Badge](https://img.shields.io/badge/-SQLite-003B57?style=flat-square&labelColor=black&logo=sqlite&logoColor=003B57)](#) +[![Radix UI Badge](https://img.shields.io/badge/-Radix%20UI-161618?style=flat-square&labelColor=black&logo=radixui&logoColor=161618)](#) + +
+

+ + Termix Banner +

+ +如果你愿意,可以在这里支持这个项目!\ +[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) + +# 概览 + +

+ + Termix Banner +

+ +Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix +提供 SSH 终端访问、SSH 隧道功能以及远程文件编辑,还会陆续添加更多工具。 + +# 功能 + +- **SSH 终端访问** - 功能完整的终端,支持分屏(最多 4 个面板)和标签系统 +- **SSH 隧道管理** - 创建和管理 SSH 隧道,支持自动重连和健康监控 +- **远程文件编辑器** - 直接在远程服务器编辑文件,支持语法高亮和文件管理功能(上传、删除、重命名等) +- **SSH 主机管理器** - 保存、组织和管理 SSH 连接,支持标签和文件夹 +- **服务器统计** - 查看任意 SSH 服务器的 CPU、内存和硬盘使用情况 +- **用户认证** - 安全的用户管理,支持管理员控制、OIDC 和双因素认证(TOTP) +- **现代化界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁界面 +- **语言支持** - 内置中英文支持 + +# 计划功能 + +- **增强管理员控制** - 提供更精细的用户和管理员权限控制、共享主机等功能 +- **主题定制** - 修改所有工具的主题风格 +- **增强终端支持** - 添加更多终端协议,如 VNC 和 RDP(有类似 Apache Guacamole 的 RDP 集成经验者请通过创建 issue 联系我) +- **移动端支持** - 支持移动应用或 Termix 网站移动版,让你在手机上管理服务器 + +# 安装 + +访问 Termix [文档](https://docs.termix.site/install) 获取安装信息。或者可以参考以下示例 docker-compose 文件: + +```yaml +services: + termix: + image: ghcr.io/lukegus/termix:latest + container_name: termix + restart: unless-stopped + ports: + - "8080:8080" + volumes: + - termix-data:/app/data + environment: + PORT: "8080" + +volumes: + termix-data: + driver: local +``` + +# 支持 + +如果你需要 Termix 的帮助,可以加入 [Discord](https://discord.gg/jVQGdvHDrf) +服务器并访问支持频道。你也可以在 [GitHub](https://github.com/LukeGus/Termix/issues) 仓库提交 issue 或 pull request。 + +# 展示 + +

+ Termix Demo 1 + Termix Demo 2 +

+ +

+ Termix Demo 3 + Termix Demo 4 + Termix Demo 5 +

+ +

+ +

+ +# 许可证 + +根据 Apache 2.0 许可证发布。更多信息请参见 LICENSE。 diff --git a/README.md b/README.md index 64fcca64..5fad8156 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ # Repo Stats + +

+ English English | + 中文 中文 +

+ ![GitHub Repo stars](https://img.shields.io/github/stars/LukeGus/Termix?style=flat&label=Stars) ![GitHub forks](https://img.shields.io/github/forks/LukeGus/Termix?style=flat&label=Forks) ![GitHub Release](https://img.shields.io/github/v/release/LukeGus/Termix?style=flat&label=Release) Discord + #### Top Technologies + [![React Badge](https://img.shields.io/badge/-React-61DBFB?style=flat-square&labelColor=black&logo=react&logoColor=61DBFB)](#) [![TypeScript Badge](https://img.shields.io/badge/-TypeScript-3178C6?style=flat-square&labelColor=black&logo=typescript&logoColor=3178C6)](#) [![Node.js Badge](https://img.shields.io/badge/-Node.js-3C873A?style=flat-square&labelColor=black&logo=node.js&logoColor=3C873A)](#) @@ -29,26 +37,34 @@ If you would like, you can support the project here!\ Termix Banner

-Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal access, SSH tunneling capabilities, and remote file editing, with many more tools to come. +Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based +solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal +access, SSH tunneling capabilities, and remote file editing, with many more tools to come. # Features + - **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) and tab system - **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring -- **Remote File Editor** - Edit files directly on remote servers with syntax highlighting, file management features (uploading, removing, renaming, deleting files) +- **Remote File Editor** - Edit files directly on remote servers with syntax highlighting, file management features ( + uploading, removing, renaming, deleting files) - **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders - **Server Stats** - View CPU, memory, and HDD usage on any SSH server - **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support -- **Modern UI** - Clean interface built with React, Tailwind CSS, and Shadcn +- **Modern UI** - Clean desktop/mobile friendly (in progress) interface built with React, Tailwind CSS, and Shadcn - **Languages** - Built-in support for English and Chinese +- **Improved Platform Support** - Now includes an installable Electron app (in progress) for desktop, with a dedicated + mobile app also planned. # Planned Features -- **Improved Admin Control** - Give more fine-grained control over user and admin permissions, share hosts, etc -- **Theming** - Modify theming for all tools -- **Improved Terminal Support** - Add more terminal protocols such as VNC and RDP (anyone who has experience in integrating RDP into a web-application similar to Apache Guacamole, please contact me by creating an issue) -- **Mobile Support** - Support a mobile app or version of the Termix website to manage servers from your phone + +See [Projects](https://github.com/users/LukeGus/projects/3). If you are looking to contribute, +see [Contributing](https://github.com/LukeGus/Termix/blob/main/CONTRIBUTING.md), # Installation -Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix. Otherwise, view a sample docker-compose file here: + +Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix. Otherwise, view +a sample docker-compose file here: + ```yaml services: termix: @@ -64,11 +80,18 @@ services: volumes: termix-data: - driver: local + driver: local ``` +Pre-built binaries are now available for download, including a Windows installer/portable app and a Linux portable app ( +built with Electron). See [Docs](http://localhost:5174/install#pre-built-binaries) for details. A native iOS/Android app +is planned. + # Support -If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) repo. + +If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support +channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) +repo. # Show-off @@ -90,4 +113,5 @@ If you need help with Termix, you can join the [Discord](https://discord.gg/jVQG

# License + Distributed under the Apache License Version 2.0. See LICENSE for more information. diff --git a/components.json b/components.json index 2082f482..8bfc737f 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 7a95cabb..92d774a6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,7 +6,11 @@ RUN apk add --no-cache python3 make g++ COPY package*.json ./ -RUN npm ci --force && \ +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + +RUN npm ci --force --ignore-scripts && \ npm cache clean --force # Stage 2: Build frontend @@ -23,6 +27,12 @@ WORKDIR /app COPY . . +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + +RUN npm rebuild better-sqlite3 --force + RUN npm run build:backend # Stage 4: Production dependencies @@ -31,6 +41,10 @@ WORKDIR /app COPY package*.json ./ +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + RUN npm ci --only=production --ignore-scripts --force && \ npm cache clean --force @@ -42,7 +56,13 @@ RUN apk add --no-cache python3 make g++ COPY package*.json ./ -RUN npm ci --only=production bcryptjs better-sqlite3 --force && \ +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + +# Install native modules and compile them properly +RUN npm ci --only=production --force && \ + npm rebuild better-sqlite3 bcryptjs --force && \ npm cache clean --force # Stage 6: Final image @@ -57,14 +77,12 @@ RUN apk add --no-cache nginx gettext su-exec && \ COPY docker/nginx.conf /etc/nginx/nginx.conf COPY --from=frontend-builder /app/dist /usr/share/nginx/html -COPY --from=frontend-builder /app/public/locales /usr/share/nginx/html/locales +COPY --from=frontend-builder /app/src/locales /usr/share/nginx/html/locales RUN chown -R nginx:nginx /usr/share/nginx/html WORKDIR /app -COPY --from=production-deps /app/node_modules /app/node_modules -COPY --from=native-builder /app/node_modules/bcryptjs /app/node_modules/bcryptjs -COPY --from=native-builder /app/node_modules/better-sqlite3 /app/node_modules/better-sqlite3 +COPY --from=native-builder /app/node_modules /app/node_modules COPY --from=backend-builder /app/dist/backend ./dist/backend COPY package.json ./ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a4c55fad..5e7ec7e9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -12,4 +12,4 @@ services: volumes: termix-data: - driver: local \ No newline at end of file + driver: local diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index d6f5033a..a45affd0 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -19,9 +19,9 @@ cd /app export NODE_ENV=production if command -v su-exec > /dev/null 2>&1; then - su-exec node node dist/backend/starter.js + su-exec node node dist/backend/backend/starter.js else - su -s /bin/sh node -c "node dist/backend/starter.js" + su -s /bin/sh node -c "node dist/backend/backend/starter.js" fi echo "All services started" diff --git a/docker/nginx.conf b/docker/nginx.conf index 728aad3b..2a943a46 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -18,7 +18,7 @@ http { index index.html index.htm; } - location /users/ { + location ~ ^/users(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -27,7 +27,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /version/ { + location ~ ^/version(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -36,7 +36,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /releases/ { + location ~ ^/releases(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -45,7 +45,16 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /alerts/ { + location ~ ^/alerts(/.*)?$ { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ ^/credentials(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -129,7 +138,16 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /status/ { + location /health { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ ^/status(/.*)?$ { proxy_pass http://127.0.0.1:8085; proxy_http_version 1.1; proxy_set_header Host $host; @@ -138,7 +156,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /metrics/ { + location ~ ^/metrics(/.*)?$ { proxy_pass http://127.0.0.1:8085; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/electron-builder.json b/electron-builder.json new file mode 100644 index 00000000..21bdb711 --- /dev/null +++ b/electron-builder.json @@ -0,0 +1,44 @@ +{ + "appId": "com.termix.app", + "productName": "Termix", + "directories": { + "output": "release" + }, + "files": [ + "dist/**/*", + "electron/**/*", + "public/**/*", + "!**/node_modules/**/*", + "!src/**/*", + "!*.md", + "!tsconfig*.json", + "!vite.config.ts", + "!eslint.config.js" + ], + "asarUnpack": ["node_modules/node-fetch/**/*"], + "extraMetadata": { + "main": "electron/main.cjs" + }, + "buildDependenciesFromSource": false, + "nodeGypRebuild": false, + "npmRebuild": false, + "win": { + "target": "nsis", + "icon": "public/icon.ico", + "executableName": "Termix" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "artifactName": "${productName}-Setup-${version}.${ext}", + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "Termix", + "uninstallDisplayName": "Termix" + }, + "linux": { + "target": "AppImage", + "icon": "public/icon.png", + "category": "Development" + } +} diff --git a/electron/main.cjs b/electron/main.cjs new file mode 100644 index 00000000..7c42cdf5 --- /dev/null +++ b/electron/main.cjs @@ -0,0 +1,334 @@ +const { app, BrowserWindow, shell, ipcMain } = require("electron"); +const path = require("path"); +const fs = require("fs"); + +let mainWindow = null; + +const isDev = process.env.NODE_ENV === "development" || !app.isPackaged; + +const gotTheLock = app.requestSingleInstanceLock(); +if (!gotTheLock) { + console.log("Another instance is already running, quitting..."); + app.quit(); + process.exit(0); +} else { + app.on("second-instance", (event, commandLine, workingDirectory) => { + console.log("Second instance detected, focusing existing window..."); + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + mainWindow.show(); + } + }); +} + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 800, + minHeight: 600, + title: "Termix", + icon: isDev + ? path.join(__dirname, "..", "public", "icon.png") + : path.join(process.resourcesPath, "public", "icon.png"), + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + webSecurity: !isDev, + preload: path.join(__dirname, "preload.js"), + }, + show: false, + }); + + if (process.platform !== "darwin") { + mainWindow.setMenuBarVisibility(false); + } + + if (isDev) { + mainWindow.loadURL("http://localhost:5173"); + mainWindow.webContents.openDevTools(); + } else { + const indexPath = path.join(__dirname, "..", "dist", "index.html"); + console.log("Loading frontend from:", indexPath); + mainWindow.loadFile(indexPath); + } + + mainWindow.once("ready-to-show", () => { + console.log("Window ready to show"); + mainWindow.show(); + }); + + mainWindow.webContents.on( + "did-fail-load", + (event, errorCode, errorDescription, validatedURL) => { + console.error( + "Failed to load:", + errorCode, + errorDescription, + validatedURL, + ); + }, + ); + + mainWindow.webContents.on("did-finish-load", () => { + console.log("Frontend loaded successfully"); + }); + + mainWindow.on("close", (event) => { + if (process.platform === "darwin") { + event.preventDefault(); + mainWindow.hide(); + } + }); + + mainWindow.on("closed", () => { + mainWindow = null; + }); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: "deny" }; + }); +} + +ipcMain.handle("get-app-version", () => { + return app.getVersion(); +}); + +ipcMain.handle("get-platform", () => { + return process.platform; +}); + +ipcMain.handle("get-server-config", () => { + try { + const userDataPath = app.getPath("userData"); + const configPath = path.join(userDataPath, "server-config.json"); + + if (fs.existsSync(configPath)) { + const configData = fs.readFileSync(configPath, "utf8"); + return JSON.parse(configData); + } + return null; + } catch (error) { + console.error("Error reading server config:", error); + return null; + } +}); + +ipcMain.handle("save-server-config", (event, config) => { + try { + const userDataPath = app.getPath("userData"); + const configPath = path.join(userDataPath, "server-config.json"); + + if (!fs.existsSync(userDataPath)) { + fs.mkdirSync(userDataPath, { recursive: true }); + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + return { success: true }; + } catch (error) { + console.error("Error saving server config:", error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle("test-server-connection", async (event, serverUrl) => { + try { + let fetch; + try { + fetch = globalThis.fetch || require("node:fetch"); + } catch (e) { + const https = require("https"); + const http = require("http"); + const { URL } = require("url"); + + fetch = (url, options = {}) => { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === "https:"; + const client = isHttps ? https : http; + + const req = client.request( + url, + { + method: options.method || "GET", + headers: options.headers || {}, + timeout: options.timeout || 5000, + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + text: () => Promise.resolve(data), + json: () => Promise.resolve(JSON.parse(data)), + }); + }); + }, + ); + + req.on("error", reject); + req.on("timeout", () => { + req.destroy(); + reject(new Error("Request timeout")); + }); + + if (options.body) { + req.write(options.body); + } + req.end(); + }); + }; + } + + const normalizedServerUrl = serverUrl.replace(/\/$/, ""); + + const healthUrl = `${normalizedServerUrl}/health`; + + try { + const response = await fetch(healthUrl, { + method: "GET", + timeout: 5000, + }); + + if (response.ok) { + const data = await response.text(); + + if ( + data.includes("") || + data.includes("") + ) { + console.log( + "Health endpoint returned HTML instead of JSON - not a Termix server", + ); + return { + success: false, + error: + "Server returned HTML instead of JSON. This does not appear to be a Termix server.", + }; + } + + try { + const healthData = JSON.parse(data); + if ( + healthData && + (healthData.status === "ok" || + healthData.status === "healthy" || + healthData.healthy === true || + healthData.database === "connected") + ) { + return { + success: true, + status: response.status, + testedUrl: healthUrl, + }; + } + } catch (parseError) { + console.log("Health endpoint did not return valid JSON"); + } + } + } catch (urlError) { + console.error("Health check failed:", urlError); + } + + try { + const versionUrl = `${normalizedServerUrl}/version`; + const response = await fetch(versionUrl, { + method: "GET", + timeout: 5000, + }); + + if (response.ok) { + const data = await response.text(); + + if ( + data.includes("") || + data.includes("") + ) { + console.log( + "Version endpoint returned HTML instead of JSON - not a Termix server", + ); + return { + success: false, + error: + "Server returned HTML instead of JSON. This does not appear to be a Termix server.", + }; + } + + try { + const versionData = JSON.parse(data); + if ( + versionData && + (versionData.status === "up_to_date" || + versionData.status === "requires_update" || + (versionData.localVersion && + versionData.version && + versionData.latest_release)) + ) { + return { + success: true, + status: response.status, + testedUrl: versionUrl, + warning: + "Health endpoint not available, but server appears to be running", + }; + } + } catch (parseError) { + console.log("Version endpoint did not return valid JSON"); + } + } + } catch (versionError) { + console.error("Version check failed:", versionError); + } + + return { + success: false, + error: + "Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.", + }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +app.whenReady().then(() => { + createWindow(); + console.log("Termix started successfully"); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } else if (mainWindow) { + mainWindow.show(); + } +}); + +app.on("before-quit", () => { + console.log("App is quitting..."); +}); + +app.on("will-quit", () => { + console.log("App will quit..."); +}); + +process.on("uncaughtException", (error) => { + console.error("Uncaught Exception:", error); +}); + +process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection at:", promise, "reason:", reason); +}); diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 00000000..e1e436d8 --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,29 @@ +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("electronAPI", { + getAppVersion: () => ipcRenderer.invoke("get-app-version"), + getPlatform: () => ipcRenderer.invoke("get-platform"), + + getServerConfig: () => ipcRenderer.invoke("get-server-config"), + saveServerConfig: (config) => + ipcRenderer.invoke("save-server-config", config), + testServerConnection: (serverUrl) => + ipcRenderer.invoke("test-server-connection", serverUrl), + + showSaveDialog: (options) => ipcRenderer.invoke("show-save-dialog", options), + showOpenDialog: (options) => ipcRenderer.invoke("show-open-dialog", options), + + onUpdateAvailable: (callback) => ipcRenderer.on("update-available", callback), + onUpdateDownloaded: (callback) => + ipcRenderer.on("update-downloaded", callback), + + removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel), + isElectron: true, + isDev: process.env.NODE_ENV === "development", + + invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), +}); + +window.IS_ELECTRON = true; + +console.log("electronAPI exposed to window"); diff --git a/eslint.config.js b/eslint.config.js index d94e7deb..f4616740 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,18 +1,18 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { globalIgnores } from "eslint/config"; export default tseslint.config([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ js.configs.recommended, tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], + reactHooks.configs["recommended-latest"], reactRefresh.configs.vite, ], languageOptions: { @@ -20,4 +20,4 @@ export default tseslint.config([ globals: globals.browser, }, }, -]) +]); diff --git a/openapi.json b/openapi.json index b8d6ce05..8c8c0a50 100644 --- a/openapi.json +++ b/openapi.json @@ -1,2214 +1,2257 @@ { - "openapi": "3.0.3", - "info": { - "title": "Termix API", - "version": "1.0.0", - "description": "Comprehensive API for Termix SSH management, file operations, tunneling, and server monitoring. This API provides endpoints for managing SSH hosts, file operations, tunnel connections, server monitoring, user management, and system alerts.", - "contact": { - "name": "Termix Development Team" + "openapi": "3.0.3", + "info": { + "title": "Termix API", + "version": "1.0.0", + "description": "Comprehensive API for Termix SSH management, file operations, tunneling, and server monitoring. This API provides endpoints for managing SSH hosts, file operations, tunnel connections, server monitoring, user management, and system alerts.", + "contact": { + "name": "Termix Development Team" + } + }, + "servers": [ + { + "url": "http://localhost:8081", + "description": "Main database and authentication server" + }, + { + "url": "http://localhost:8083", + "description": "SSH tunnel management server" + }, + { + "url": "http://localhost:8084", + "description": "SSH file manager server" + }, + { + "url": "http://localhost:8085", + "description": "Server statistics and monitoring server" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + { + "name": "System", + "description": "System health, version, and release information endpoints" + }, + { + "name": "SSH Hosts", + "description": "SSH host management, creation, updates, and deletion" + }, + { + "name": "File Manager", + "description": "File manager operations including recent, pinned, and shortcuts" + }, + { + "name": "SSH File Operations", + "description": "SSH file operations like reading, writing, creating, and deleting files" + }, + { + "name": "Tunnel Management", + "description": "SSH tunnel connection, disconnection, and status management" + }, + { + "name": "Server Statistics", + "description": "Server status monitoring and metrics collection" + }, + { + "name": "User Management", + "description": "User account management and administration" + }, + { + "name": "Authentication", + "description": "User authentication, login, and password management" + }, + { + "name": "TOTP", + "description": "Two-factor authentication using TOTP (Time-based One-Time Password)" + }, + { + "name": "Alerts", + "description": "System alerts and notifications management" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" } }, - "servers": [ - { - "url": "http://localhost:8081", - "description": "Main database and authentication server" + "schemas": { + "SSHHost": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "ip": { "type": "string" }, + "port": { "type": "integer" }, + "username": { "type": "string" }, + "folder": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "pin": { "type": "boolean" }, + "authType": { "type": "string", "enum": ["password", "key"] }, + "password": { "type": "string" }, + "key": { "type": "string" }, + "keyPassword": { "type": "string" }, + "keyType": { "type": "string" }, + "enableTerminal": { "type": "boolean" }, + "enableTunnel": { "type": "boolean" }, + "enableFileManager": { "type": "boolean" }, + "defaultPath": { "type": "string" }, + "tunnelConnections": { + "type": "array", + "items": { "type": "object" } + }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["id", "ip", "port", "username", "authType"] }, - { - "url": "http://localhost:8083", - "description": "SSH tunnel management server" + "SSHHostData": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "ip": { "type": "string" }, + "port": { "type": "integer" }, + "username": { "type": "string" }, + "folder": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "pin": { "type": "boolean" }, + "authType": { "type": "string", "enum": ["password", "key"] }, + "password": { "type": "string" }, + "key": { "type": "string" }, + "keyPassword": { "type": "string" }, + "keyType": { "type": "string" }, + "enableTerminal": { "type": "boolean" }, + "enableTunnel": { "type": "boolean" }, + "enableFileManager": { "type": "boolean" }, + "defaultPath": { "type": "string" }, + "tunnelConnections": { + "type": "array", + "items": { "type": "object" } + } + }, + "required": ["ip", "port", "username", "authType"] }, - { - "url": "http://localhost:8084", - "description": "SSH file manager server" + "TunnelConfig": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "hostName": { "type": "string" }, + "sourceIP": { "type": "string" }, + "sourceSSHPort": { "type": "integer" }, + "sourceUsername": { "type": "string" }, + "sourcePassword": { "type": "string" }, + "sourceAuthMethod": { "type": "string" }, + "sourceSSHKey": { "type": "string" }, + "sourceKeyPassword": { "type": "string" }, + "sourceKeyType": { "type": "string" }, + "endpointIP": { "type": "string" }, + "endpointSSHPort": { "type": "integer" }, + "endpointUsername": { "type": "string" }, + "endpointPassword": { "type": "string" }, + "endpointAuthMethod": { "type": "string" }, + "endpointSSHKey": { "type": "string" }, + "endpointKeyPassword": { "type": "string" }, + "endpointKeyType": { "type": "string" }, + "sourcePort": { "type": "integer" }, + "endpointPort": { "type": "integer" }, + "maxRetries": { "type": "integer" }, + "retryInterval": { "type": "integer" }, + "autoStart": { "type": "boolean" }, + "isPinned": { "type": "boolean" } + }, + "required": [ + "name", + "hostName", + "sourceIP", + "sourceSSHPort", + "sourceUsername", + "endpointIP", + "endpointSSHPort", + "endpointUsername", + "sourcePort", + "endpointPort" + ] }, - { - "url": "http://localhost:8085", - "description": "Server statistics and monitoring server" - } - ], - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - { - "name": "System", - "description": "System health, version, and release information endpoints" - }, - { - "name": "SSH Hosts", - "description": "SSH host management, creation, updates, and deletion" - }, - { - "name": "File Manager", - "description": "File manager operations including recent, pinned, and shortcuts" - }, - { - "name": "SSH File Operations", - "description": "SSH file operations like reading, writing, creating, and deleting files" - }, - { - "name": "Tunnel Management", - "description": "SSH tunnel connection, disconnection, and status management" - }, - { - "name": "Server Statistics", - "description": "Server status monitoring and metrics collection" - }, - { - "name": "User Management", - "description": "User account management and administration" - }, - { - "name": "Authentication", - "description": "User authentication, login, and password management" - }, - { - "name": "TOTP", - "description": "Two-factor authentication using TOTP (Time-based One-Time Password)" - }, - { - "name": "Alerts", - "description": "System alerts and notifications management" - } - ], - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" + "TunnelStatus": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "reason": { "type": "string" }, + "errorType": { "type": "string" }, + "retryCount": { "type": "integer" }, + "maxRetries": { "type": "integer" }, + "nextRetryIn": { "type": "integer" }, + "retryExhausted": { "type": "boolean" } } }, - "schemas": { - "SSHHost": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "name": { "type": "string" }, - "ip": { "type": "string" }, - "port": { "type": "integer" }, - "username": { "type": "string" }, - "folder": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } }, - "pin": { "type": "boolean" }, - "authType": { "type": "string", "enum": ["password", "key"] }, - "password": { "type": "string" }, - "key": { "type": "string" }, - "keyPassword": { "type": "string" }, - "keyType": { "type": "string" }, - "enableTerminal": { "type": "boolean" }, - "enableTunnel": { "type": "boolean" }, - "enableFileManager": { "type": "boolean" }, - "defaultPath": { "type": "string" }, - "tunnelConnections": { "type": "array", "items": { "type": "object" } }, - "createdAt": { "type": "string", "format": "date-time" }, - "updatedAt": { "type": "string", "format": "date-time" } - }, - "required": ["id", "ip", "port", "username", "authType"] - }, - "SSHHostData": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "ip": { "type": "string" }, - "port": { "type": "integer" }, - "username": { "type": "string" }, - "folder": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } }, - "pin": { "type": "boolean" }, - "authType": { "type": "string", "enum": ["password", "key"] }, - "password": { "type": "string" }, - "key": { "type": "string" }, - "keyPassword": { "type": "string" }, - "keyType": { "type": "string" }, - "enableTerminal": { "type": "boolean" }, - "enableTunnel": { "type": "boolean" }, - "enableFileManager": { "type": "boolean" }, - "defaultPath": { "type": "string" }, - "tunnelConnections": { "type": "array", "items": { "type": "object" } } - }, - "required": ["ip", "port", "username", "authType"] - }, - "TunnelConfig": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "hostName": { "type": "string" }, - "sourceIP": { "type": "string" }, - "sourceSSHPort": { "type": "integer" }, - "sourceUsername": { "type": "string" }, - "sourcePassword": { "type": "string" }, - "sourceAuthMethod": { "type": "string" }, - "sourceSSHKey": { "type": "string" }, - "sourceKeyPassword": { "type": "string" }, - "sourceKeyType": { "type": "string" }, - "endpointIP": { "type": "string" }, - "endpointSSHPort": { "type": "integer" }, - "endpointUsername": { "type": "string" }, - "endpointPassword": { "type": "string" }, - "endpointAuthMethod": { "type": "string" }, - "endpointSSHKey": { "type": "string" }, - "endpointKeyPassword": { "type": "string" }, - "endpointKeyType": { "type": "string" }, - "sourcePort": { "type": "integer" }, - "endpointPort": { "type": "integer" }, - "maxRetries": { "type": "integer" }, - "retryInterval": { "type": "integer" }, - "autoStart": { "type": "boolean" }, - "isPinned": { "type": "boolean" } - }, - "required": ["name", "hostName", "sourceIP", "sourceSSHPort", "sourceUsername", "endpointIP", "endpointSSHPort", "endpointUsername", "sourcePort", "endpointPort"] - }, - "TunnelStatus": { - "type": "object", - "properties": { - "status": { "type": "string" }, - "reason": { "type": "string" }, - "errorType": { "type": "string" }, - "retryCount": { "type": "integer" }, - "maxRetries": { "type": "integer" }, - "nextRetryIn": { "type": "integer" }, - "retryExhausted": { "type": "boolean" } - } - }, - "ServerStatus": { - "type": "object", - "properties": { - "status": { "type": "string", "enum": ["online", "offline"] }, - "lastChecked": { "type": "string", "format": "date-time" } - } - }, - "ServerMetrics": { - "type": "object", - "properties": { - "cpu": { - "type": "object", - "properties": { - "percent": { "type": "number" }, - "cores": { "type": "number" }, - "load": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 } - } - }, - "memory": { - "type": "object", - "properties": { - "percent": { "type": "number" }, - "usedGiB": { "type": "number" }, - "totalGiB": { "type": "number" } - } - }, - "disk": { - "type": "object", - "properties": { - "percent": { "type": "number" }, - "usedHuman": { "type": "string" }, - "totalHuman": { "type": "string" } - } - }, - "lastChecked": { "type": "string", "format": "date-time" } - } - }, - "FileManagerFile": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["file", "directory"] }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" } - }, - "required": ["name", "path"] - }, - "UserInfo": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "username": { "type": "string" }, - "is_admin": { "type": "boolean" } - }, - "required": ["id", "username", "is_admin"] - }, - "AuthResponse": { - "type": "object", - "properties": { - "token": { "type": "string" } - }, - "required": ["token"] - }, - "Error": { - "type": "object", - "properties": { - "error": { "type": "string" }, - "details": { "type": "string" } - } + "ServerStatus": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["online", "offline"] }, + "lastChecked": { "type": "string", "format": "date-time" } } }, - "parameters": { - "hostId": { - "name": "hostId", - "in": "query", - "description": "The ID of the SSH host", - "required": true, - "schema": { - "type": "integer" - } - }, - "sessionId": { - "name": "sessionId", - "in": "query", - "description": "The SSH session identifier", - "required": true, - "schema": { - "type": "string" - } - }, - "path": { - "name": "path", - "in": "query", - "description": "The file or directory path", - "required": true, - "schema": { - "type": "string" - } - }, - "tunnelName": { - "name": "tunnelName", - "in": "path", - "description": "The name of the tunnel", - "required": true, - "schema": { - "type": "string" - } - }, - "userId": { - "name": "userId", - "in": "path", - "description": "The user identifier", - "required": true, - "schema": { - "type": "string" - } - }, - "hostIdPath": { - "name": "id", - "in": "path", - "description": "The SSH host identifier", - "required": true, - "schema": { - "type": "integer" - } - }, - "serverIdPath": { - "name": "id", - "in": "path", - "description": "The server identifier", - "required": true, - "schema": { - "type": "integer" - } + "ServerMetrics": { + "type": "object", + "properties": { + "cpu": { + "type": "object", + "properties": { + "percent": { "type": "number" }, + "cores": { "type": "number" }, + "load": { + "type": "array", + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + } + } + }, + "memory": { + "type": "object", + "properties": { + "percent": { "type": "number" }, + "usedGiB": { "type": "number" }, + "totalGiB": { "type": "number" } + } + }, + "disk": { + "type": "object", + "properties": { + "percent": { "type": "number" }, + "usedHuman": { "type": "string" }, + "totalHuman": { "type": "string" } + } + }, + "lastChecked": { "type": "string", "format": "date-time" } } }, - "responses": { - "BadRequest": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "FileManagerFile": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "type": { "type": "string", "enum": ["file", "directory"] }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" } }, - "Unauthorized": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "required": ["name", "path"] + }, + "UserInfo": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "username": { "type": "string" }, + "is_admin": { "type": "boolean" } }, - "NotFound": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "required": ["id", "username", "is_admin"] + }, + "AuthResponse": { + "type": "object", + "properties": { + "token": { "type": "string" } }, - "InternalServerError": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "required": ["token"] + }, + "Error": { + "type": "object", + "properties": { + "error": { "type": "string" }, + "details": { "type": "string" } } } }, - "paths": { - "/health": { - "get": { - "summary": "Health check endpoint", - "description": "Simple health check to verify the API server is running and responsive. **Server: localhost:8081**", - "operationId": "getHealth", - "tags": ["System"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { "type": "string", "example": "ok" } - } + "parameters": { + "hostId": { + "name": "hostId", + "in": "query", + "description": "The ID of the SSH host", + "required": true, + "schema": { + "type": "integer" + } + }, + "sessionId": { + "name": "sessionId", + "in": "query", + "description": "The SSH session identifier", + "required": true, + "schema": { + "type": "string" + } + }, + "path": { + "name": "path", + "in": "query", + "description": "The file or directory path", + "required": true, + "schema": { + "type": "string" + } + }, + "tunnelName": { + "name": "tunnelName", + "in": "path", + "description": "The name of the tunnel", + "required": true, + "schema": { + "type": "string" + } + }, + "userId": { + "name": "userId", + "in": "path", + "description": "The user identifier", + "required": true, + "schema": { + "type": "string" + } + }, + "hostIdPath": { + "name": "id", + "in": "path", + "description": "The SSH host identifier", + "required": true, + "schema": { + "type": "integer" + } + }, + "serverIdPath": { + "name": "id", + "in": "path", + "description": "The server identifier", + "required": true, + "schema": { + "type": "integer" + } + } + }, + "responses": { + "BadRequest": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "Unauthorized": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "NotFound": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "InternalServerError": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + } + } + }, + "paths": { + "/health": { + "get": { + "summary": "Health check endpoint", + "description": "Simple health check to verify the API server is running and responsive. **Server: localhost:8081**", + "operationId": "getHealth", + "tags": ["System"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", "example": "ok" } } } } } } } - }, - "/version": { - "get": { - "summary": "Get version information and check for updates", - "description": "Get version information and check for updates. **Server: localhost:8081**", - "operationId": "getVersion", - "tags": ["System"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { "type": "string", "enum": ["up_to_date", "requires_update"] }, - "version": { "type": "string" }, - "latest_release": { - "type": "object", - "properties": { - "tag_name": { "type": "string" }, - "name": { "type": "string" }, - "published_at": { "type": "string" }, - "html_url": { "type": "string" } - } - }, - "cached": { "type": "boolean" }, - "cache_age": { "type": "number" } - } + } + }, + "/version": { + "get": { + "summary": "Get version information and check for updates", + "description": "Get version information and check for updates. **Server: localhost:8081**", + "operationId": "getVersion", + "tags": ["System"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["up_to_date", "requires_update"] + }, + "version": { "type": "string" }, + "latest_release": { + "type": "object", + "properties": { + "tag_name": { "type": "string" }, + "name": { "type": "string" }, + "published_at": { "type": "string" }, + "html_url": { "type": "string" } + } + }, + "cached": { "type": "boolean" }, + "cache_age": { "type": "number" } } } } - }, - "401": { - "description": "Version information not available", - "content": { - "text/plain": { - "schema": { "type": "string" } - } + } + }, + "401": { + "description": "Version information not available", + "content": { + "text/plain": { + "schema": { "type": "string" } } } } } - }, - "/releases/rss": { - "get": { - "summary": "Get releases in RSS format", - "description": "Get releases in RSS format. **Server: localhost:8081**", - "operationId": "getReleasesRSS", - "tags": ["System"], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { "type": "integer", "default": 1 } - }, - { - "name": "per_page", - "in": "query", - "schema": { "type": "integer", "default": 20, "maximum": 100 } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "feed": { + } + }, + "/releases/rss": { + "get": { + "summary": "Get releases in RSS format", + "description": "Get releases in RSS format. **Server: localhost:8081**", + "operationId": "getReleasesRSS", + "tags": ["System"], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { "type": "integer", "default": 1 } + }, + { + "name": "per_page", + "in": "query", + "schema": { "type": "integer", "default": 20, "maximum": 100 } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feed": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" }, + "link": { "type": "string" }, + "updated": { "type": "string" } + } + }, + "items": { + "type": "array", + "items": { "type": "object", "properties": { + "id": { "type": "integer" }, "title": { "type": "string" }, "description": { "type": "string" }, "link": { "type": "string" }, - "updated": { "type": "string" } - } - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "link": { "type": "string" }, - "pubDate": { "type": "string" }, - "version": { "type": "string" }, - "isPrerelease": { "type": "boolean" }, - "isDraft": { "type": "boolean" }, - "assets": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "size": { "type": "number" }, - "download_count": { "type": "number" }, - "download_url": { "type": "string" } - } + "pubDate": { "type": "string" }, + "version": { "type": "string" }, + "isPrerelease": { "type": "boolean" }, + "isDraft": { "type": "boolean" }, + "assets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "size": { "type": "number" }, + "download_count": { "type": "number" }, + "download_url": { "type": "string" } } } } } - }, - "total_count": { "type": "integer" }, - "cached": { "type": "boolean" }, - "cache_age": { "type": "number" } - } + } + }, + "total_count": { "type": "integer" }, + "cached": { "type": "boolean" }, + "cache_age": { "type": "number" } } } } } } } - }, - "/ssh/db/host": { - "get": { - "summary": "Get all SSH hosts", - "description": "Retrieve a list of all configured SSH hosts in the system. This endpoint requires authentication and returns host information including connection details, authentication methods, and enabled features. **Server: localhost:8081**", - "operationId": "getSSHHosts", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "Successfully retrieved SSH hosts", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/SSHHost" } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "post": { - "summary": "Create a new SSH host", - "description": "Create a new SSH host configuration. **Server: localhost:8081**", - "operationId": "createSSHHost", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + } + }, + "/ssh/db/host": { + "get": { + "summary": "Get all SSH hosts", + "description": "Retrieve a list of all configured SSH hosts in the system. This endpoint requires authentication and returns host information including connection details, authentication methods, and enabled features. **Server: localhost:8081**", + "operationId": "getSSHHosts", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "Successfully retrieved SSH hosts", "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "key": { - "type": "string", - "format": "binary", - "description": "SSH private key file (optional)" - }, - "data": { - "type": "string", - "description": "JSON string containing host data" - } - } - } - }, "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHostData" } + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/SSHHost" } + } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHost" } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/db/host/{id}": { - "get": { - "summary": "Get SSH host by ID", - "description": "Get SSH host by ID. **Server: localhost:8081**", - "operationId": "getSSHHostById", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostIdPath" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHost" } + "post": { + "summary": "Create a new SSH host", + "description": "Create a new SSH host configuration. **Server: localhost:8081**", + "operationId": "createSSHHost", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "binary", + "description": "SSH private key file (optional)" + }, + "data": { + "type": "string", + "description": "JSON string containing host data" + } } } }, - "404": { "$ref": "#/components/responses/NotFound" }, - "401": { "$ref": "#/components/responses/Unauthorized" } + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHostData" } + } } }, - "put": { - "summary": "Update SSH host", - "description": "Update SSH host configuration. **Server: localhost:8081**", - "operationId": "updateSSHHost", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostIdPath" - } - ], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "key": { - "type": "string", - "format": "binary", - "description": "SSH private key file (optional)" - }, - "data": { - "type": "string", - "description": "JSON string containing host data" - } - } - } - }, "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHostData" } + "schema": { "$ref": "#/components/schemas/SSHHost" } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHost" } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/db/host/{id}": { + "get": { + "summary": "Get SSH host by ID", + "description": "Get SSH host by ID. **Server: localhost:8081**", + "operationId": "getSSHHostById", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostIdPath" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHost" } + } + } + }, + "404": { "$ref": "#/components/responses/NotFound" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + }, + "put": { + "summary": "Update SSH host", + "description": "Update SSH host configuration. **Server: localhost:8081**", + "operationId": "updateSSHHost", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostIdPath" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "binary", + "description": "SSH private key file (optional)" + }, + "data": { + "type": "string", + "description": "JSON string containing host data" + } } } }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { "$ref": "#/components/responses/NotFound" } + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHostData" } + } } }, - "delete": { - "summary": "Delete SSH host", - "description": "Delete SSH host configuration. **Server: localhost:8081**", - "operationId": "deleteSSHHost", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostIdPath" + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHost" } + } } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { "$ref": "#/components/responses/NotFound" } - } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { "$ref": "#/components/responses/NotFound" } } }, - "/ssh/db/folders": { - "get": { - "summary": "Get all SSH host folders", - "description": "Get all SSH host folders. **Server: localhost:8081**", - "operationId": "getSSHFolders", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "type": "string" } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + "delete": { + "summary": "Delete SSH host", + "description": "Delete SSH host configuration. **Server: localhost:8081**", + "operationId": "deleteSSHHost", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostIdPath" } - } - }, - "/ssh/bulk-import": { - "post": { - "summary": "Bulk import SSH hosts", - "description": "Bulk import SSH hosts. **Server: localhost:8081**", - "operationId": "bulkImportSSHHosts", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "hosts": { + "message": { "type": "string" } + } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { "$ref": "#/components/responses/NotFound" } + } + } + }, + "/ssh/db/folders": { + "get": { + "summary": "Get all SSH host folders", + "description": "Get all SSH host folders. **Server: localhost:8081**", + "operationId": "getSSHFolders", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/bulk-import": { + "post": { + "summary": "Bulk import SSH hosts", + "description": "Bulk import SSH hosts. **Server: localhost:8081**", + "operationId": "bulkImportSSHHosts", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "hosts": { + "type": "array", + "items": { "$ref": "#/components/schemas/SSHHostData" } + } + }, + "required": ["hosts"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "success": { "type": "integer" }, + "failed": { "type": "integer" }, + "errors": { "type": "array", - "items": { "$ref": "#/components/schemas/SSHHostData" } + "items": { "type": "string" } } - }, - "required": ["hosts"] + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" }, - "success": { "type": "integer" }, - "failed": { "type": "integer" }, - "errors": { - "type": "array", - "items": { "type": "string" } - } - } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/file_manager/recent": { + "get": { + "summary": "Get recent files for a host", + "description": "Get recent files for a host. **Server: localhost:8081**", + "operationId": "getFileManagerRecent", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostId" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/FileManagerFile" } } } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/recent": { - "get": { - "summary": "Get recent files for a host", - "description": "Get recent files for a host. **Server: localhost:8081**", - "operationId": "getFileManagerRecent", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/FileManagerFile" } - } - } + "post": { + "summary": "Add file to recent list", + "description": "Add file to recent list. **Server: localhost:8081**", + "operationId": "addFileManagerRecent", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" }, + "hostId": { "type": "integer" } + }, + "required": ["name", "path", "isSSH", "hostId"] } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } }, - "post": { - "summary": "Add file to recent list", - "description": "Add file to recent list. **Server: localhost:8081**", - "operationId": "addFileManagerRecent", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" }, - "hostId": { "type": "integer" } - }, - "required": ["name", "path", "isSSH", "hostId"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "delete": { - "summary": "Remove file from recent list", - "description": "Remove file from recent list. **Server: localhost:8081**", - "operationId": "removeFileManagerRecent", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "name": "name", - "in": "query", - "description": "File name", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isSSH", - "in": "query", - "description": "Whether this is an SSH file", - "required": true, - "schema": { "type": "boolean" } - }, - { - "name": "sshSessionId", - "in": "query", - "description": "SSH session ID", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "hostId", - "in": "query", - "description": "Host ID", - "required": true, - "schema": { "type": "integer" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/pinned": { - "get": { - "summary": "Get pinned files for a host", - "description": "Get pinned files for a host. **Server: localhost:8081**", - "operationId": "getFileManagerPinned", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/FileManagerFile" } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "post": { - "summary": "Add file to pinned list", - "description": "Add file to pinned list. **Server: localhost:8081**", - "operationId": "addFileManagerPinned", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "requestBody": { + "delete": { + "summary": "Remove file from recent list", + "description": "Remove file from recent list. **Server: localhost:8081**", + "operationId": "removeFileManagerRecent", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "File name", "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isSSH", + "in": "query", + "description": "Whether this is an SSH file", + "required": true, + "schema": { "type": "boolean" } + }, + { + "name": "sshSessionId", + "in": "query", + "description": "SSH session ID", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "hostId", + "in": "query", + "description": "Host ID", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" }, - "hostId": { "type": "integer" } - }, - "required": ["name", "path", "isSSH", "hostId"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/file_manager/pinned": { + "get": { + "summary": "Get pinned files for a host", + "description": "Get pinned files for a host. **Server: localhost:8081**", + "operationId": "getFileManagerPinned", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostId" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/FileManagerFile" } } } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "delete": { - "summary": "Remove file from pinned list", - "description": "Remove file from pinned list. **Server: localhost:8081**", - "operationId": "removeFileManagerPinned", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "name": "name", - "in": "query", - "description": "File name", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isSSH", - "in": "query", - "description": "Whether this is an SSH file", - "required": true, - "schema": { "type": "boolean" } - }, - { - "name": "sshSessionId", - "in": "query", - "description": "SSH session ID", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "hostId", - "in": "query", - "description": "Host ID", - "required": true, - "schema": { "type": "integer" } } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/shortcuts": { - "get": { - "summary": "Get file shortcuts for a host", - "description": "Get file shortcuts for a host. **Server: localhost:8081**", - "operationId": "getFileManagerShortcuts", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "path": { "type": "string" } - }, - "required": ["name", "path"] - } - } - } + "post": { + "summary": "Add file to pinned list", + "description": "Add file to pinned list. **Server: localhost:8081**", + "operationId": "addFileManagerPinned", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" }, + "hostId": { "type": "integer" } + }, + "required": ["name", "path", "isSSH", "hostId"] } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } }, - "post": { - "summary": "Add file shortcut", - "description": "Add file shortcut. **Server: localhost:8081**", - "operationId": "addFileManagerShortcut", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" }, - "hostId": { "type": "integer" } - }, - "required": ["name", "path", "isSSH", "hostId"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "delete": { - "summary": "Remove file shortcut", - "description": "Remove file shortcut. **Server: localhost:8081**", - "operationId": "removeFileManagerShortcut", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "name": "name", - "in": "query", - "description": "File name", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isSSH", - "in": "query", - "description": "Whether this is an SSH file", - "required": true, - "schema": { "type": "boolean" } - }, - { - "name": "sshSessionId", - "in": "query", - "description": "SSH session ID", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "hostId", - "in": "query", - "description": "Host ID", - "required": true, - "schema": { "type": "integer" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/ssh/connect": { - "post": { - "summary": "Connect to SSH server", - "description": "Connect to SSH server. **Server: localhost:8084**", - "operationId": "connectSSH", - "tags": ["SSH File Operations"], - "requestBody": { + "delete": { + "summary": "Remove file from pinned list", + "description": "Remove file from pinned list. **Server: localhost:8081**", + "operationId": "removeFileManagerPinned", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "File name", "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isSSH", + "in": "query", + "description": "Whether this is an SSH file", + "required": true, + "schema": { "type": "boolean" } + }, + { + "name": "sshSessionId", + "in": "query", + "description": "SSH session ID", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "hostId", + "in": "query", + "description": "Host ID", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "sessionId": { "type": "string" }, - "ip": { "type": "string" }, - "port": { "type": "integer" }, - "username": { "type": "string" }, - "password": { "type": "string" }, - "sshKey": { "type": "string" }, - "keyPassword": { "type": "string" } - }, - "required": ["sessionId", "ip", "username", "port"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } } - }, - "/ssh/file_manager/ssh/disconnect": { - "post": { - "summary": "Disconnect from SSH server", - "description": "Disconnect from SSH server. **Server: localhost:8084**", - "operationId": "disconnectSSH", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, + } + }, + "/ssh/file_manager/shortcuts": { + "get": { + "summary": "Get file shortcuts for a host", + "description": "Get file shortcuts for a host. **Server: localhost:8081**", + "operationId": "getFileManagerShortcuts", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostId" + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" } - }, - "required": ["sessionId"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { + "type": "array", + "items": { "type": "object", "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/status": { - "get": { - "summary": "Get SSH connection status", - "description": "Get SSH connection status. **Server: localhost:8084**", - "operationId": "getSSHStatus", - "tags": ["SSH File Operations"], - "parameters": [ - { - "$ref": "#/components/parameters/sessionId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "connected": { "type": "boolean" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/listFiles": { - "get": { - "summary": "List files in SSH directory", - "description": "List files in SSH directory. **Server: localhost:8084**", - "operationId": "listSSHFiles", - "tags": ["SSH File Operations"], - "parameters": [ - { - "$ref": "#/components/parameters/sessionId" - }, - { - "$ref": "#/components/parameters/path" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["file", "directory"] }, - "size": { "type": "number" }, - "modified": { "type": "string" }, - "permissions": { "type": "string" } - } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/readFile": { - "get": { - "summary": "Read SSH file content", - "description": "Read SSH file content. **Server: localhost:8084**", - "operationId": "readSSHFile", - "tags": ["SSH File Operations"], - "parameters": [ - { - "$ref": "#/components/parameters/sessionId" - }, - { - "$ref": "#/components/parameters/path" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "content": { "type": "string" }, + "name": { "type": "string" }, "path": { "type": "string" } - } + }, + "required": ["name", "path"] } } } } - } - } - }, - "/ssh/file_manager/ssh/writeFile": { - "post": { - "summary": "Write content to SSH file", - "description": "Write content to SSH file. **Server: localhost:8084**", - "operationId": "writeSSHFile", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "path": { "type": "string" }, - "content": { "type": "string" } - }, - "required": ["sessionId", "path", "content"] - } - } - } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/ssh/createFile": { - "post": { - "summary": "Create new SSH file", - "description": "Create new SSH file. **Server: localhost:8084**", - "operationId": "createSSHFile", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "path": { "type": "string" }, - "fileName": { "type": "string" }, - "content": { "type": "string" } - }, - "required": ["sessionId", "path", "fileName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/createFolder": { - "post": { - "summary": "Create new SSH folder", - "description": "Create new SSH folder. **Server: localhost:8084**", - "operationId": "createSSHFolder", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "path": { "type": "string" }, - "folderName": { "type": "string" } - }, - "required": ["sessionId", "path", "folderName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/deleteItem": { - "delete": { - "summary": "Delete SSH file or folder", - "description": "Delete SSH file or folder. **Server: localhost:8084**", - "operationId": "deleteSSHItem", - "tags": ["SSH File Operations"], - "parameters": [ - { - "name": "sessionId", - "in": "query", - "description": "SSH session ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File or directory path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isDirectory", - "in": "query", - "description": "Whether the item is a directory", - "required": true, - "schema": { "type": "boolean" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/renameItem": { - "put": { - "summary": "Rename SSH file or folder", - "description": "Rename SSH file or folder. **Server: localhost:8084**", - "operationId": "renameSSHItem", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "oldPath": { "type": "string" }, - "newName": { "type": "string" } - }, - "required": ["sessionId", "oldPath", "newName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/tunnel/status": { - "get": { - "summary": "Get all tunnel statuses", - "description": "Get all tunnel statuses. **Server: localhost:8083**", - "operationId": "getTunnelStatuses", - "tags": ["Tunnel Management"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/TunnelStatus" } - } - } - } - } - } - } - }, - "/ssh/tunnel/status/{tunnelName}": { - "get": { - "summary": "Get tunnel status by name", - "description": "Get tunnel status by name. **Server: localhost:8083**", - "operationId": "getTunnelStatusByName", - "tags": ["Tunnel Management"], - "parameters": [ - { - "$ref": "#/components/parameters/tunnelName" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TunnelStatus" } - } - } - } - } - } - }, - "/ssh/tunnel/connect": { - "post": { - "summary": "Connect to tunnel", - "description": "Connect to tunnel. **Server: localhost:8083**", - "operationId": "connectTunnel", - "tags": ["Tunnel Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TunnelConfig" } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/tunnel/disconnect": { - "post": { - "summary": "Disconnect tunnel", - "description": "Disconnect tunnel. **Server: localhost:8083**", - "operationId": "disconnectTunnel", - "tags": ["Tunnel Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tunnelName": { "type": "string" } - }, - "required": ["tunnelName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/tunnel/cancel": { - "post": { - "summary": "Cancel tunnel connection", - "description": "Cancel tunnel connection. **Server: localhost:8083**", - "operationId": "cancelTunnel", - "tags": ["Tunnel Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tunnelName": { "type": "string" } - }, - "required": ["tunnelName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/status": { - "get": { - "summary": "Get all server statuses", - "description": "Get all server statuses. **Server: localhost:8085**", - "operationId": "getAllServerStatuses", - "tags": ["Server Statistics"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/ServerStatus" } - } - } - } - } - } - } - }, - "/status/{id}": { - "get": { - "summary": "Get server status by ID", - "description": "Get server status by ID. **Server: localhost:8085**", - "operationId": "getServerStatusById", - "tags": ["Server Statistics"], - "parameters": [ - { - "$ref": "#/components/parameters/serverIdPath" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ServerStatus" } - } - } - } - } - } - }, - "/metrics/{id}": { - "get": { - "summary": "Get server metrics by ID", - "description": "Get server metrics by ID. **Server: localhost:8085**", - "operationId": "getServerMetricsById", - "tags": ["Server Statistics"], - "parameters": [ - { - "$ref": "#/components/parameters/serverIdPath" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ServerMetrics" } - } - } - } - } - } - }, - "/refresh": { - "post": { - "summary": "Refresh server statistics", - "description": "Refresh server statistics. **Server: localhost:8085**", - "operationId": "refreshServerStats", - "tags": ["Server Statistics"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/users/create": { - "post": { - "summary": "Create new user account", - "description": "Create new user account. **Server: localhost:8081**", - "operationId": "createUser", - "tags": ["User Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "username": { "type": "string" }, - "password": { "type": "string" } - }, - "required": ["username", "password"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" } - } - } - }, - "/users/login": { - "post": { - "summary": "User login", - "description": "User login. **Server: localhost:8081**", - "operationId": "loginUser", - "tags": ["Authentication"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "username": { "type": "string" }, - "password": { "type": "string" } - }, - "required": ["username", "password"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AuthResponse" } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - } - }, - "/users/me": { - "get": { - "summary": "Get current user info", - "description": "Get current user info. **Server: localhost:8081**", - "operationId": "getUserInfo", - "tags": ["User Management"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/UserInfo" } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - } - }, - "/users/count": { - "get": { - "summary": "Get total user count", - "description": "Get total user count. **Server: localhost:8081**", - "operationId": "getUserCount", - "tags": ["User Management"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { "type": "integer" } - } - } - } - } - } - } - } - }, - "/users/registration-allowed": { - "get": { - "summary": "Check if user registration is allowed", - "description": "Check if user registration is allowed. **Server: localhost:8081**", - "operationId": "getRegistrationAllowed", - "tags": ["User Management"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "allowed": { "type": "boolean" } - } - } - } + "post": { + "summary": "Add file shortcut", + "description": "Add file shortcut. **Server: localhost:8081**", + "operationId": "addFileManagerShortcut", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" }, + "hostId": { "type": "integer" } + }, + "required": ["name", "path", "isSSH", "hostId"] } } } }, - "patch": { - "summary": "Update registration allowed status", - "description": "Update registration allowed status. **Server: localhost:8081**", - "operationId": "updateRegistrationAllowed", - "tags": ["User Management"], - "security": [{ "bearerAuth": [] }], - "requestBody": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + }, + "delete": { + "summary": "Remove file shortcut", + "description": "Remove file shortcut. **Server: localhost:8081**", + "operationId": "removeFileManagerShortcut", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "File name", "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isSSH", + "in": "query", + "description": "Whether this is an SSH file", + "required": true, + "schema": { "type": "boolean" } + }, + { + "name": "sshSessionId", + "in": "query", + "description": "SSH session ID", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "hostId", + "in": "query", + "description": "Host ID", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/file_manager/ssh/connect": { + "post": { + "summary": "Connect to SSH server", + "description": "Connect to SSH server. **Server: localhost:8084**", + "operationId": "connectSSH", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "ip": { "type": "string" }, + "port": { "type": "integer" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "sshKey": { "type": "string" }, + "keyPassword": { "type": "string" } + }, + "required": ["sessionId", "ip", "username", "port"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" } + } + } + }, + "/ssh/file_manager/ssh/disconnect": { + "post": { + "summary": "Disconnect from SSH server", + "description": "Disconnect from SSH server. **Server: localhost:8084**", + "operationId": "disconnectSSH", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" } + }, + "required": ["sessionId"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/status": { + "get": { + "summary": "Get SSH connection status", + "description": "Get SSH connection status. **Server: localhost:8084**", + "operationId": "getSSHStatus", + "tags": ["SSH File Operations"], + "parameters": [ + { + "$ref": "#/components/parameters/sessionId" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connected": { "type": "boolean" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/listFiles": { + "get": { + "summary": "List files in SSH directory", + "description": "List files in SSH directory. **Server: localhost:8084**", + "operationId": "listSSHFiles", + "tags": ["SSH File Operations"], + "parameters": [ + { + "$ref": "#/components/parameters/sessionId" + }, + { + "$ref": "#/components/parameters/path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "type": { + "type": "string", + "enum": ["file", "directory"] + }, + "size": { "type": "number" }, + "modified": { "type": "string" }, + "permissions": { "type": "string" } + } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/readFile": { + "get": { + "summary": "Read SSH file content", + "description": "Read SSH file content. **Server: localhost:8084**", + "operationId": "readSSHFile", + "tags": ["SSH File Operations"], + "parameters": [ + { + "$ref": "#/components/parameters/sessionId" + }, + { + "$ref": "#/components/parameters/path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { "type": "string" }, + "path": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/writeFile": { + "post": { + "summary": "Write content to SSH file", + "description": "Write content to SSH file. **Server: localhost:8084**", + "operationId": "writeSSHFile", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "path": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["sessionId", "path", "content"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/createFile": { + "post": { + "summary": "Create new SSH file", + "description": "Create new SSH file. **Server: localhost:8084**", + "operationId": "createSSHFile", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "path": { "type": "string" }, + "fileName": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["sessionId", "path", "fileName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/createFolder": { + "post": { + "summary": "Create new SSH folder", + "description": "Create new SSH folder. **Server: localhost:8084**", + "operationId": "createSSHFolder", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "path": { "type": "string" }, + "folderName": { "type": "string" } + }, + "required": ["sessionId", "path", "folderName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/deleteItem": { + "delete": { + "summary": "Delete SSH file or folder", + "description": "Delete SSH file or folder. **Server: localhost:8084**", + "operationId": "deleteSSHItem", + "tags": ["SSH File Operations"], + "parameters": [ + { + "name": "sessionId", + "in": "query", + "description": "SSH session ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File or directory path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isDirectory", + "in": "query", + "description": "Whether the item is a directory", + "required": true, + "schema": { "type": "boolean" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/renameItem": { + "put": { + "summary": "Rename SSH file or folder", + "description": "Rename SSH file or folder. **Server: localhost:8084**", + "operationId": "renameSSHItem", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "oldPath": { "type": "string" }, + "newName": { "type": "string" } + }, + "required": ["sessionId", "oldPath", "newName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/tunnel/status": { + "get": { + "summary": "Get all tunnel statuses", + "description": "Get all tunnel statuses. **Server: localhost:8083**", + "operationId": "getTunnelStatuses", + "tags": ["Tunnel Management"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TunnelStatus" + } + } + } + } + } + } + } + }, + "/ssh/tunnel/status/{tunnelName}": { + "get": { + "summary": "Get tunnel status by name", + "description": "Get tunnel status by name. **Server: localhost:8083**", + "operationId": "getTunnelStatusByName", + "tags": ["Tunnel Management"], + "parameters": [ + { + "$ref": "#/components/parameters/tunnelName" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TunnelStatus" } + } + } + } + } + } + }, + "/ssh/tunnel/connect": { + "post": { + "summary": "Connect to tunnel", + "description": "Connect to tunnel. **Server: localhost:8083**", + "operationId": "connectTunnel", + "tags": ["Tunnel Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TunnelConfig" } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/tunnel/disconnect": { + "post": { + "summary": "Disconnect tunnel", + "description": "Disconnect tunnel. **Server: localhost:8083**", + "operationId": "disconnectTunnel", + "tags": ["Tunnel Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tunnelName": { "type": "string" } + }, + "required": ["tunnelName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/tunnel/cancel": { + "post": { + "summary": "Cancel tunnel connection", + "description": "Cancel tunnel connection. **Server: localhost:8083**", + "operationId": "cancelTunnel", + "tags": ["Tunnel Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tunnelName": { "type": "string" } + }, + "required": ["tunnelName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/status": { + "get": { + "summary": "Get all server statuses", + "description": "Get all server statuses. **Server: localhost:8085**", + "operationId": "getAllServerStatuses", + "tags": ["Server Statistics"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ServerStatus" + } + } + } + } + } + } + } + }, + "/status/{id}": { + "get": { + "summary": "Get server status by ID", + "description": "Get server status by ID. **Server: localhost:8085**", + "operationId": "getServerStatusById", + "tags": ["Server Statistics"], + "parameters": [ + { + "$ref": "#/components/parameters/serverIdPath" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ServerStatus" } + } + } + } + } + } + }, + "/metrics/{id}": { + "get": { + "summary": "Get server metrics by ID", + "description": "Get server metrics by ID. **Server: localhost:8085**", + "operationId": "getServerMetricsById", + "tags": ["Server Statistics"], + "parameters": [ + { + "$ref": "#/components/parameters/serverIdPath" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ServerMetrics" } + } + } + } + } + } + }, + "/refresh": { + "post": { + "summary": "Refresh server statistics", + "description": "Refresh server statistics. **Server: localhost:8085**", + "operationId": "refreshServerStats", + "tags": ["Server Statistics"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/users/create": { + "post": { + "summary": "Create new user account", + "description": "Create new user account. **Server: localhost:8081**", + "operationId": "createUser", + "tags": ["User Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "password": { "type": "string" } + }, + "required": ["username", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" } + } + } + }, + "/users/login": { + "post": { + "summary": "User login", + "description": "User login. **Server: localhost:8081**", + "operationId": "loginUser", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "password": { "type": "string" } + }, + "required": ["username", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AuthResponse" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/me": { + "get": { + "summary": "Get current user info", + "description": "Get current user info. **Server: localhost:8081**", + "operationId": "getUserInfo", + "tags": ["User Management"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UserInfo" } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/count": { + "get": { + "summary": "Get total user count", + "description": "Get total user count. **Server: localhost:8081**", + "operationId": "getUserCount", + "tags": ["User Management"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { "type": "integer" } + } + } + } + } + } + } + } + }, + "/users/registration-allowed": { + "get": { + "summary": "Check if user registration is allowed", + "description": "Check if user registration is allowed. **Server: localhost:8081**", + "operationId": "getRegistrationAllowed", + "tags": ["User Management"], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "allowed": { "type": "boolean" } - }, - "required": ["allowed"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } } } } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } } }, - "/users/initiate-reset": { - "post": { - "summary": "Initiate password reset", - "description": "Initiate password reset. **Server: localhost:8081**", - "operationId": "initiatePasswordReset", - "tags": ["Authentication"], - "requestBody": { - "required": true, + "patch": { + "summary": "Update registration allowed status", + "description": "Update registration allowed status. **Server: localhost:8081**", + "operationId": "updateRegistrationAllowed", + "tags": ["User Management"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "allowed": { "type": "boolean" } + }, + "required": ["allowed"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "username": { "type": "string" } - }, - "required": ["username"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } + "message": { "type": "string" } } } } } - } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } } - }, - "/users/verify-reset-code": { - "post": { - "summary": "Verify password reset code", - "description": "Verify password reset code. **Server: localhost:8081**", - "operationId": "verifyPasswordResetCode", - "tags": ["Authentication"], - "requestBody": { - "required": true, + } + }, + "/users/initiate-reset": { + "post": { + "summary": "Initiate password reset", + "description": "Initiate password reset. **Server: localhost:8081**", + "operationId": "initiatePasswordReset", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" } + }, + "required": ["username"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "username": { "type": "string" }, - "resetCode": { "type": "string" } - }, - "required": ["username", "resetCode"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tempToken": { "type": "string" } - } + "message": { "type": "string" } } } } } } } - }, - "/users/complete-reset": { - "post": { - "summary": "Complete password reset", - "description": "Complete password reset. **Server: localhost:8081**", - "operationId": "completePasswordReset", - "tags": ["Authentication"], - "requestBody": { - "required": true, + } + }, + "/users/verify-reset-code": { + "post": { + "summary": "Verify password reset code", + "description": "Verify password reset code. **Server: localhost:8081**", + "operationId": "verifyPasswordResetCode", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "resetCode": { "type": "string" } + }, + "required": ["username", "resetCode"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "username": { "type": "string" }, - "tempToken": { "type": "string" }, - "newPassword": { "type": "string" } - }, - "required": ["username", "tempToken", "newPassword"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } + "tempToken": { "type": "string" } } } } } } } - }, - "/users/totp/setup": { - "post": { - "summary": "Setup TOTP authentication", - "description": "Setup TOTP authentication. **Server: localhost:8081**", - "operationId": "setupTOTP", - "tags": ["TOTP"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "secret": { "type": "string" }, - "qr_code": { "type": "string" } - } - } - } + } + }, + "/users/complete-reset": { + "post": { + "summary": "Complete password reset", + "description": "Complete password reset. **Server: localhost:8081**", + "operationId": "completePasswordReset", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "tempToken": { "type": "string" }, + "newPassword": { "type": "string" } + }, + "required": ["username", "tempToken", "newPassword"] } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } - } - }, - "/users/totp/enable": { - "post": { - "summary": "Enable TOTP authentication", - "description": "Enable TOTP authentication. **Server: localhost:8081**", - "operationId": "enableTOTP", - "tags": ["TOTP"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "totp_code": { "type": "string" } - }, - "required": ["totp_code"] + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/users/totp/setup": { + "post": { + "summary": "Setup TOTP authentication", + "description": "Setup TOTP authentication. **Server: localhost:8081**", + "operationId": "setupTOTP", + "tags": ["TOTP"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secret": { "type": "string" }, + "qr_code": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/totp/enable": { + "post": { + "summary": "Enable TOTP authentication", + "description": "Enable TOTP authentication. **Server: localhost:8081**", + "operationId": "enableTOTP", + "tags": ["TOTP"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "totp_code": { "type": "string" } + }, + "required": ["totp_code"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "backup_codes": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/totp/verify-login": { + "post": { + "summary": "Verify TOTP during login", + "description": "Verify TOTP during login. **Server: localhost:8081**", + "operationId": "verifyTOTPLogin", + "tags": ["TOTP"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "temp_token": { "type": "string" }, + "totp_code": { "type": "string" } + }, + "required": ["temp_token", "totp_code"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AuthResponse" } + } + } + } + } + } + }, + "/alerts": { + "get": { + "summary": "Get all system alerts", + "description": "Get all system alerts. **Server: localhost:8081**", + "operationId": "getAllAlerts", + "tags": ["Alerts"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "type": "object", "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, "message": { "type": "string" }, - "backup_codes": { - "type": "array", - "items": { "type": "string" } - } + "expiresAt": { "type": "string" }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "critical"] + }, + "type": { + "type": "string", + "enum": ["info", "warning", "error", "success"] + }, + "actionUrl": { "type": "string" }, + "actionText": { "type": "string" } } } } } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } } - }, - "/users/totp/verify-login": { - "post": { - "summary": "Verify TOTP during login", - "description": "Verify TOTP during login. **Server: localhost:8081**", - "operationId": "verifyTOTPLogin", - "tags": ["TOTP"], - "requestBody": { - "required": true, + } + }, + "/alerts/user/{userId}": { + "get": { + "summary": "Get alerts for specific user", + "description": "Get alerts for specific user. **Server: localhost:8081**", + "operationId": "getUserAlerts", + "tags": ["Alerts"], + "parameters": [ + { + "$ref": "#/components/parameters/userId" + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "temp_token": { "type": "string" }, - "totp_code": { "type": "string" } - }, - "required": ["temp_token", "totp_code"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AuthResponse" } - } - } - } - } - } - }, - "/alerts": { - "get": { - "summary": "Get all system alerts", - "description": "Get all system alerts. **Server: localhost:8081**", - "operationId": "getAllAlerts", - "tags": ["Alerts"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "title": { "type": "string" }, - "message": { "type": "string" }, - "expiresAt": { "type": "string" }, - "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, - "type": { "type": "string", "enum": ["info", "warning", "error", "success"] }, - "actionUrl": { "type": "string" }, - "actionText": { "type": "string" } - } - } - } - } - } - } - } - } - }, - "/alerts/user/{userId}": { - "get": { - "summary": "Get alerts for specific user", - "description": "Get alerts for specific user. **Server: localhost:8081**", - "operationId": "getUserAlerts", - "tags": ["Alerts"], - "parameters": [ - { - "$ref": "#/components/parameters/userId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "alerts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "title": { "type": "string" }, - "message": { "type": "string" }, - "expiresAt": { "type": "string" }, - "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, - "type": { "type": "string", "enum": ["info", "warning", "error", "success"] }, - "actionUrl": { "type": "string" }, - "actionText": { "type": "string" } - } + "alerts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "message": { "type": "string" }, + "expiresAt": { "type": "string" }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "critical"] + }, + "type": { + "type": "string", + "enum": ["info", "warning", "error", "success"] + }, + "actionUrl": { "type": "string" }, + "actionText": { "type": "string" } } } } @@ -2218,38 +2261,38 @@ } } } - }, - "/alerts/dismiss": { - "post": { - "summary": "Dismiss an alert", - "description": "Dismiss an alert. **Server: localhost:8081**", - "operationId": "dismissAlert", - "tags": ["Alerts"], - "requestBody": { - "required": true, + } + }, + "/alerts/dismiss": { + "post": { + "summary": "Dismiss an alert", + "description": "Dismiss an alert. **Server: localhost:8081**", + "operationId": "dismissAlert", + "tags": ["Alerts"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "alertId": { "type": "string" } + }, + "required": ["userId", "alertId"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "userId": { "type": "string" }, - "alertId": { "type": "string" } - }, - "required": ["userId", "alertId"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } + "message": { "type": "string" } } } } @@ -2259,4 +2302,4 @@ } } } - \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index a3195012..d34f84e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "termix", - "version": "0.0.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "termix", - "version": "0.0.0", + "version": "1.6.0", "dependencies": { "@hookform/resolvers": "^5.1.1", "@radix-ui/react-accordion": "^1.2.11", @@ -69,6 +69,7 @@ "react-hook-form": "^7.60.0", "react-i18next": "^15.7.3", "react-resizable-panels": "^3.0.3", + "react-simple-keyboard": "^3.8.120", "react-xtermjs": "^1.0.10", "sonner": "^2.0.7", "speakeasy": "^2.0.0", @@ -92,43 +93,37 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", + "concurrently": "^9.2.1", + "electron": "^38.0.0", + "electron-builder": "^26.0.12", + "electron-icon-builder": "^2.0.1", + "electron-packager": "^17.1.2", "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "prettier": "3.6.2", "ts-node": "^10.9.2", "tw-animate-css": "^1.3.5", "typescript": "~5.9.2", "typescript-eslint": "^8.40.0", - "vite": "^7.1.3" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "vite": "^7.1.5", + "wait-on": "^8.0.4" } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.6", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", - "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "version": "6.18.7", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.7.tgz", + "integrity": "sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -264,22 +259,10 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@codemirror/lang-lezer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-lezer/-/lang-lezer-6.0.2.tgz", - "integrity": "sha512-mcVAf8lw+sCfSlr2ivMqV8JtNmOQjSXdA1vHKRtoW0OZsz1k6qhF+DX0K2TbWlAThqiGgRkRSZyYzIoEtKB2uQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/lezer": "^1.0.0" - } - }, "node_modules/@codemirror/lang-liquid": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.2.3.tgz", - "integrity": "sha512-yeN+nMSrf/lNii3FJxVVEGQwFG0/2eDyH6gNOj+TGCa0hlNO4bhQnoO5ISnd7JOG+7zTEcI/GOoyraisFVY7jQ==", + "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==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -293,9 +276,9 @@ } }, "node_modules/@codemirror/lang-markdown": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.3.tgz", - "integrity": "sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.4.tgz", + "integrity": "sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.7.1", @@ -357,9 +340,9 @@ } }, "node_modules/@codemirror/lang-sql": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.9.0.tgz", - "integrity": "sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.9.1.tgz", + "integrity": "sha512-ecSk3gm/mlINcURMcvkCZmXgdzPSq8r/yfCtTB4vgqGGIbBC2IJIAy7GqYTy5pgBEooTVmHP2GZK6Z7h63CDGg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -426,9 +409,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", - "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -522,9 +505,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", - "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "version": "6.38.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.2.tgz", + "integrity": "sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -557,10 +540,786 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz", + "integrity": "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "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": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "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/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/node-gyp/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@electron/node-gyp/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/node-gyp/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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==", + "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", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@electron/rebuild/node_modules/y18n": { + "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" + } + }, + "node_modules/@electron/rebuild/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@electron/rebuild/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", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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==", + "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/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -574,9 +1333,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -590,9 +1349,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -606,9 +1365,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -622,9 +1381,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -638,9 +1397,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -654,9 +1413,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -670,9 +1429,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -686,9 +1445,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -702,9 +1461,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -718,9 +1477,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -734,9 +1493,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -750,9 +1509,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -766,9 +1525,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -782,9 +1541,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -798,9 +1557,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -814,9 +1573,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -830,9 +1589,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -846,9 +1605,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -862,9 +1621,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -878,9 +1637,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -894,9 +1653,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -910,9 +1669,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -926,9 +1685,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -942,9 +1701,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -958,9 +1717,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -974,9 +1733,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1091,9 +1850,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -1128,31 +1887,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.2", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", - "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "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==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.2" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -1165,10 +1924,34 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@hookform/resolvers": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz", - "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz", + "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==", "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" @@ -1188,33 +1971,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1243,6 +2012,132 @@ "url": "https://github.com/sponsors/nzakas" } }, + "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==", + "dev": true, + "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==", + "dev": true, + "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", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.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" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "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", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.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" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1255,16 +2150,519 @@ "node": ">=18.0.0" } }, + "node_modules/@jimp/bmp": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.13.tgz", + "integrity": "sha512-9edAxu7N2FX7vzkdl5Jo1BbACfycUtBQX+XBMcHA2bk62P8R0otgkHg798frgAk/WxQIzwxqOH6wMiCwrlAzdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.16.13.tgz", + "integrity": "sha512-qXpA1tzTnlkTku9yqtuRtS/wVntvE6f3m3GNxdTdtmc+O+Wcg9Xo2ABPMh7Nc0AHbMKzwvwgB2JnjZmlmJEObg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "load-bmfont": "^1.3.1", + "mkdirp": "^0.5.1", + "phin": "^2.9.1", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.4.1" + } + }, + "node_modules/@jimp/custom": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.16.13.tgz", + "integrity": "sha512-LTATglVUPGkPf15zX1wTMlZ0+AU7cGEGF6ekVF1crA8eHUWsGjrYTB+Ht4E3HTrCok8weQG+K01rJndCp/l4XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/core": "^0.16.13" + } + }, + "node_modules/@jimp/gif": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.16.13.tgz", + "integrity": "sha512-yFAMZGv3o+YcjXilMWWwS/bv1iSqykFahFMSO169uVMtfQVfa90kt4/kDwrXNR6Q9i6VHpFiGZMlF2UnHClBvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "gifwrap": "^0.9.2", + "omggif": "^1.0.9" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/jpeg": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.16.13.tgz", + "integrity": "sha512-BJHlDxzTlCqP2ThqP8J0eDrbBfod7npWCbJAcfkKqdQuFk0zBPaZ6KKaQKyKxmWJ87Z6ohANZoMKEbtvrwz1AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "jpeg-js": "^0.4.2" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.16.13.tgz", + "integrity": "sha512-8Z1k96ZFxlhK2bgrY1JNWNwvaBeI/bciLM0yDOni2+aZwfIIiC7Y6PeWHTAvjHNjphz+XCt01WQmOYWCn0ML6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.16.13.tgz", + "integrity": "sha512-PvLrfa8vkej3qinlebyhLpksJgCF5aiysDMSVhOZqwH5nQLLtDE9WYbnsofGw4r0VVpyw3H/ANCIzYTyCtP9Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.16.13.tgz", + "integrity": "sha512-RNave7EFgZrb5V5EpdvJGAEHMnDAJuwv05hKscNfIYxf0kR3KhViBTDy+MoTnMlIvaKFULfwIgaZWzyhuINMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.16.13.tgz", + "integrity": "sha512-xW+9BtEvoIkkH/Wde9ql4nAFbYLkVINhpgAE7VcBUsuuB34WUbcBl/taOuUYQrPEFQJ4jfXiAJZ2H/rvKjCVnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.16.13.tgz", + "integrity": "sha512-QayTXw4tXMwU6q6acNTQrTTFTXpNRBe+MgTGMDU0lk+23PjlFCO/9sacflelG8lsp7vNHhAxFeHptDMAksEYzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.16.13.tgz", + "integrity": "sha512-BSsP71GTNaqWRcvkbWuIVH+zK7b3TSNebbhDkFK0fVaUTzHuKMS/mgY4hDZIEVt7Rf5FjadAYtsujHN9w0iSYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.16.13.tgz", + "integrity": "sha512-WEl2tPVYwzYL8OKme6Go2xqiWgKsgxlMwyHabdAU4tXaRwOCnOI7v4021gCcBb9zn/oWwguHuKHmK30Fw2Z/PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.16.13.tgz", + "integrity": "sha512-qt9WKq8vWrcjySa9DyQ0x/RBMHQeiVjdVSY1SJsMjssPUf0pS74qorcuAkGi89biN3YoGUgPkpqECnAWnYwgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.16.13.tgz", + "integrity": "sha512-5/N3yJggbWQTlGZHQYJPmQXEwR52qaXjEzkp1yRBbtdaekXE3BG/suo0fqeoV/csf8ooI78sJzYmIrxNoWVtgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.16.13.tgz", + "integrity": "sha512-2rZmTdFbT/cF9lEZIkXCYO0TsT114Q27AX5IAo0Sju6jVQbvIk1dFUTnwLDadTo8wkJlFzGqMQ24Cs8cHWOliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.16.13.tgz", + "integrity": "sha512-EmcgAA74FTc5u7Z+hUO/sRjWwfPPLuOQP5O64x5g4j0T12Bd29IgsYZxoutZo/rb3579+JNa/3wsSEmyVv1EpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-rotate": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-gaussian": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.16.13.tgz", + "integrity": "sha512-A1XKfGQD0iDdIiKqFYi8nZMv4dDVYdxbrmgR7y/CzUHhSYdcmoljLIIsZZM3Iks/Wa353W3vtvkWLuDbQbch1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-invert": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.16.13.tgz", + "integrity": "sha512-xFMrIn7czEZbdbMzZWuaZFnlLGJDVJ82y5vlsKsXRTG2kcxRsMPXvZRWHV57nSs1YFsNqXSbrC8B98n0E32njQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.16.13.tgz", + "integrity": "sha512-wLRYKVBXql2GAYgt6FkTnCfE+q5NomM7Dlh0oIPGAoMBWDyTx0eYutRK6PlUrRK2yMHuroAJCglICTbxqGzowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-normalize": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.16.13.tgz", + "integrity": "sha512-3tfad0n9soRna4IfW9NzQdQ2Z3ijkmo21DREHbE6CGcMIxOSvfRdSvf1qQPApxjTSo8LTU4MCi/fidx/NZ0GqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.16.13.tgz", + "integrity": "sha512-0m6i3p01PGRkGAK9r53hDYrkyMq+tlhLOIbsSTmZyh6HLshUKlTB7eXskF5OpVd5ZUHoltlNc6R+ggvKIzxRFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "load-bmfont": "^1.4.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.16.13.tgz", + "integrity": "sha512-qoqtN8LDknm3fJm9nuPygJv30O3vGhSBD2TxrsCnhtOsxKAqVPJtFVdGd/qVuZ8nqQANQmTlfqTiK9mVWQ7MiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.16.13.tgz", + "integrity": "sha512-Ev+Jjmj1nHYw897z9C3R9dYsPv7S2/nxdgfFb/h8hOwK0Ovd1k/+yYS46A0uj/JCKK0pQk8wOslYBkPwdnLorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-scale": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.16.13.tgz", + "integrity": "sha512-05POQaEJVucjTiSGMoH68ZiELc7QqpIpuQlZ2JBbhCV+WCbPFUBcGSmE7w4Jd0E2GvCho/NoMODLwgcVGQA97A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-shadow": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.16.13.tgz", + "integrity": "sha512-nmu5VSZ9hsB1JchTKhnnCY+paRBnwzSyK5fhkhtQHHoFD5ArBQ/5wU8y6tCr7k/GQhhGq1OrixsECeMjPoc8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blur": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.16.13.tgz", + "integrity": "sha512-+3zArBH0OE3Rhjm4HyAokMsZlIq5gpQec33CncyoSwxtRBM2WAhUVmCUKuBo+Lr/2/4ISoY4BWpHKhMLDix6cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-color": ">=0.8.0", + "@jimp/plugin-resize": ">=0.8.0" + } + }, + "node_modules/@jimp/plugins": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.16.13.tgz", + "integrity": "sha512-CJLdqODEhEVs4MgWCxpWL5l95sCBlkuSLz65cxEm56X5akIsn4LOlwnKoSEZioYcZUBvHhCheH67AyPTudfnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/plugin-blit": "^0.16.13", + "@jimp/plugin-blur": "^0.16.13", + "@jimp/plugin-circle": "^0.16.13", + "@jimp/plugin-color": "^0.16.13", + "@jimp/plugin-contain": "^0.16.13", + "@jimp/plugin-cover": "^0.16.13", + "@jimp/plugin-crop": "^0.16.13", + "@jimp/plugin-displace": "^0.16.13", + "@jimp/plugin-dither": "^0.16.13", + "@jimp/plugin-fisheye": "^0.16.13", + "@jimp/plugin-flip": "^0.16.13", + "@jimp/plugin-gaussian": "^0.16.13", + "@jimp/plugin-invert": "^0.16.13", + "@jimp/plugin-mask": "^0.16.13", + "@jimp/plugin-normalize": "^0.16.13", + "@jimp/plugin-print": "^0.16.13", + "@jimp/plugin-resize": "^0.16.13", + "@jimp/plugin-rotate": "^0.16.13", + "@jimp/plugin-scale": "^0.16.13", + "@jimp/plugin-shadow": "^0.16.13", + "@jimp/plugin-threshold": "^0.16.13", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.16.13.tgz", + "integrity": "sha512-8cGqINvbWJf1G0Her9zbq9I80roEX0A+U45xFby3tDWfzn+Zz8XKDF1Nv9VUwVx0N3zpcG1RPs9hfheG4Cq2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "pngjs": "^3.3.3" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@jimp/tiff": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.16.13.tgz", + "integrity": "sha512-oJY8d9u95SwW00VPHuCNxPap6Q1+E/xM5QThb9Hu+P6EGuu6lIeLaNBMmFZyblwFbwrH+WBOZlvIzDhi4Dm/6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "utif": "^2.0.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/types": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.16.13.tgz", + "integrity": "sha512-mC0yVNUobFDjoYLg4hoUwzMKgNlxynzwt3cDXzumGvRJ7Kb8qQGOWJQjQFo5OxmGExqzPphkirdbBF88RVLBCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/bmp": "^0.16.13", + "@jimp/gif": "^0.16.13", + "@jimp/jpeg": "^0.16.13", + "@jimp/png": "^0.16.13", + "@jimp/tiff": "^0.16.13", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/utils": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.16.13.tgz", + "integrity": "sha512-VyCpkZzFTHXtKgVO35iKN0sYR10psGpV6SkcSeV4oF7eSYlR8Bl6aQLCzVeFjvESF7mxTmIiI3/XrMobVrtxDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "regenerator-runtime": "^0.13.3" + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "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==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1275,15 +2673,15 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1361,9 +2759,9 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz", - "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.3.tgz", + "integrity": "sha512-jexmlKq5NpGiB7t+0QkyhSXRgaiab5YisHIQW9C7EcU19KSUsDguZe9WY+rmRDg34nXoNH2LQ4SxpC+aJUchSQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1382,16 +2780,6 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@lezer/lezer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/lezer/-/lezer-1.1.2.tgz", - "integrity": "sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==", - "license": "MIT", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@lezer/lr": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", @@ -1412,9 +2800,9 @@ } }, "node_modules/@lezer/php": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.3.tgz", - "integrity": "sha512-NDwgktd5qh/EfEn7Dogf2N6eNnC5WPJ5NslB8nKhgXSuH2uSJamCEom1g4VGo+ibfoADK8D69NMCMhuuYbVskg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.4.tgz", + "integrity": "sha512-D2dJ0t8Z28/G1guztRczMFvPDUqzeMLSQbdWQmaiHV7urc8NlEOnjYk9UrZ531OcLiRxD4Ihcbv7AsDpNKDRaQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1477,31 +2865,90 @@ "@lezer/lr": "^1.4.0" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@marijn/find-cluster-break": { "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==", "license": "MIT" }, - "node_modules/@nextjournal/lang-clojure": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nextjournal/lang-clojure/-/lang-clojure-1.0.0.tgz", - "integrity": "sha512-gOCV71XrYD0DhwGoPMWZmZ0r92/lIHsqQu9QWdpZYYBwiChNwMO4sbVMP7eTuAqffFB2BTtCSC+1skSH9d3bNg==", - "license": "ISC", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@nextjournal/lezer-clojure": "1.0.0" - } - }, - "node_modules/@nextjournal/lezer-clojure": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nextjournal/lezer-clojure/-/lezer-clojure-1.0.0.tgz", - "integrity": "sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==", - "license": "ISC", - "dependencies": { - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1540,6 +2987,59 @@ "node": ">= 8" } }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -1547,19 +3047,19 @@ "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, "node_modules/@radix-ui/react-accordion": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", - "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "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==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -1634,15 +3134,15 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", - "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "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==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", @@ -1664,16 +3164,16 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", - "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "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==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -1785,13 +3285,22 @@ } } }, - "node_modules/@radix-ui/react-dialog/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==", - "license": "MIT" + "node_modules/@radix-ui/react-direction": { + "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==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "node_modules/@radix-ui/react-dismissable-layer": { "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==", @@ -1818,98 +3327,17 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { - "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==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "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==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", - "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "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==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -1929,9 +3357,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "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==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2010,25 +3438,25 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", @@ -2050,21 +3478,21 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", - "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "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==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", @@ -2087,9 +3515,9 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -2143,9 +3571,9 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -2214,12 +3642,12 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "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==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -2245,17 +3673,17 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", - "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "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==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2276,22 +3704,22 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", - "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "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==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", @@ -2342,13 +3770,13 @@ } }, "node_modules/@radix-ui/react-slider": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", - "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -2393,12 +3821,12 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", - "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", @@ -2422,18 +3850,18 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", - "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "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==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -2485,95 +3913,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/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==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { - "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==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2757,21 +4096,6 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, - "node_modules/@replit/codemirror-lang-csharp": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz", - "integrity": "sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==", - "license": "MIT", - "peerDependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@replit/codemirror-lang-nix": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz", @@ -2819,16 +4143,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", - "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", "cpu": [ "arm" ], @@ -2839,9 +4163,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", - "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", "cpu": [ "arm64" ], @@ -2852,9 +4176,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", - "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", "cpu": [ "arm64" ], @@ -2865,9 +4189,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", - "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", "cpu": [ "x64" ], @@ -2878,9 +4202,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", - "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", "cpu": [ "arm64" ], @@ -2891,9 +4215,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", - "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", "cpu": [ "x64" ], @@ -2904,9 +4228,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", - "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", "cpu": [ "arm" ], @@ -2917,9 +4241,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", - "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", "cpu": [ "arm" ], @@ -2930,9 +4254,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", - "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", "cpu": [ "arm64" ], @@ -2943,9 +4267,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", - "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", "cpu": [ "arm64" ], @@ -2956,9 +4280,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", - "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", "cpu": [ "loong64" ], @@ -2968,10 +4292,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", - "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", "cpu": [ "ppc64" ], @@ -2982,9 +4306,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", - "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", "cpu": [ "riscv64" ], @@ -2995,9 +4319,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", - "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", "cpu": [ "riscv64" ], @@ -3008,9 +4332,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", - "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", "cpu": [ "s390x" ], @@ -3021,9 +4345,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", - "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", "cpu": [ "x64" ], @@ -3034,9 +4358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", - "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", "cpu": [ "x64" ], @@ -3046,10 +4370,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", - "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", "cpu": [ "arm64" ], @@ -3060,9 +4397,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", - "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", "cpu": [ "ia32" ], @@ -3073,9 +4410,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", - "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", "cpu": [ "x64" ], @@ -3085,6 +4422,43 @@ "win32" ] }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -3092,15 +4466,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz", - "integrity": "sha512-CJSn2vstd17ddWIHBsjuD4OQnn9krQfaq6EO+w9YfId5DKznyPmzxAARlOXG99cC8/3Kli8ysKy6phL43bSr0w==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.23" + "@swc/types": "^0.1.24" }, "engines": { "node": ">=10" @@ -3110,16 +4484,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.12.14", - "@swc/core-darwin-x64": "1.12.14", - "@swc/core-linux-arm-gnueabihf": "1.12.14", - "@swc/core-linux-arm64-gnu": "1.12.14", - "@swc/core-linux-arm64-musl": "1.12.14", - "@swc/core-linux-x64-gnu": "1.12.14", - "@swc/core-linux-x64-musl": "1.12.14", - "@swc/core-win32-arm64-msvc": "1.12.14", - "@swc/core-win32-ia32-msvc": "1.12.14", - "@swc/core-win32-x64-msvc": "1.12.14" + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -3131,9 +4505,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.14.tgz", - "integrity": "sha512-HNukQoOKgMsHSETj8vgGGKK3SEcH7Cz6k4bpntCxBKNkO3sH7RcBTDulWGGHJfZaDNix7Rw2ExUVWtLZlzkzXg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", "cpu": [ "arm64" ], @@ -3148,9 +4522,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.14.tgz", - "integrity": "sha512-4Ttf3Obtk3MvFrR0e04qr6HfXh4L1Z+K3dRej63TAFuYpo+cPXeOZdPUddAW73lSUGkj+61IHnGPoXD3OQYy4Q==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", "cpu": [ "x64" ], @@ -3165,9 +4539,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.14.tgz", - "integrity": "sha512-zhJOH2KWjtQpzJ27Xjw/RKLVOa1aiEJC2b70xbCwEX6ZTVAl8tKbhkZ3GMphhfVmLJ9gf/2UQR58oxVnsXqX5Q==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", "cpu": [ "arm" ], @@ -3182,9 +4556,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.14.tgz", - "integrity": "sha512-akUAe1YrBqZf1EDdUxahQ8QZnJi8Ts6Ya0jf6GBIMvnXL4Y6QIuvKTRwfNxy7rJ+x9zpzP1Vlh14ZZkSKZ1EGA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", "cpu": [ "arm64" ], @@ -3199,9 +4573,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.14.tgz", - "integrity": "sha512-ZkOOIpSMXuPAjfOXEIAEQcrPOgLi6CaXvA5W+GYnpIpFG21Nd0qb0WbwFRv4K8BRtl993Q21v0gPpOaFHU+wdA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", "cpu": [ "arm64" ], @@ -3216,9 +4590,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.14.tgz", - "integrity": "sha512-71EPPccwJiJUxd2aMwNlTfom2mqWEWYGdbeTju01tzSHsEuD7E6ePlgC3P3ngBqB3urj41qKs87z7zPOswT5Iw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", "cpu": [ "x64" ], @@ -3233,9 +4607,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.14.tgz", - "integrity": "sha512-nImF1hZJqKTcl0WWjHqlelOhvuB9rU9kHIw/CmISBUZXogjLIvGyop1TtJNz0ULcz2Oxr3Q2YpwfrzsgvgbGkA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", "cpu": [ "x64" ], @@ -3250,9 +4624,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.14.tgz", - "integrity": "sha512-sABFQFxSuStFoxvEWZUHWYldtB1B4A9eDNFd4Ty50q7cemxp7uoscFoaCqfXSGNBwwBwpS5EiPB6YN4y6hqmLQ==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", "cpu": [ "arm64" ], @@ -3267,9 +4641,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.14.tgz", - "integrity": "sha512-KBznRB02NASkpepRdWIK4f1AvmaJCDipKWdW1M1xV9QL2tE4aySJFojVuG1+t0tVDkjRfwcZjycQfRoJ4RjD7Q==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", "cpu": [ "ia32" ], @@ -3284,9 +4658,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.14.tgz", - "integrity": "sha512-SymoP2CJHzrYaFKjWvuQljcF7BkTpzaS1vpywv7K9EzdTb5N8qPDvNd+PhWUqBz9JHBhbJxpaeTDQBXF/WWPmw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", "cpu": [ "x64" ], @@ -3308,34 +4682,47 @@ "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", - "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", "lightningcss": "1.30.1", - "magic-string": "^0.30.17", + "magic-string": "^0.30.18", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.13" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3346,24 +4733,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", "cpu": [ "arm64" ], @@ -3377,9 +4764,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", "cpu": [ "arm64" ], @@ -3393,9 +4780,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", "cpu": [ "x64" ], @@ -3409,9 +4796,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", "cpu": [ "x64" ], @@ -3425,9 +4812,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", "cpu": [ "arm" ], @@ -3441,9 +4828,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", "cpu": [ "arm64" ], @@ -3457,9 +4844,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", "cpu": [ "arm64" ], @@ -3473,9 +4860,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", "cpu": [ "x64" ], @@ -3489,9 +4876,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", "cpu": [ "x64" ], @@ -3505,9 +4892,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3522,75 +4909,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", "cpu": [ "arm64" ], @@ -3604,9 +4937,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", "cpu": [ "x64" ], @@ -3620,19 +4953,36 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", - "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "tailwindcss": "4.1.11" + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "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" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3687,6 +5037,19 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -3706,6 +5069,16 @@ "@types/node": "*" } }, + "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": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3735,6 +5108,23 @@ "@types/send": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -3759,6 +5149,16 @@ "@types/node": "*" } }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3782,14 +5182,26 @@ } }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "license": "MIT", "dependencies": { "undici-types": "~7.10.0" } }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/qrcode": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", @@ -3812,9 +5224,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3822,15 +5234,25 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", @@ -3872,9 +5294,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.119", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.119.tgz", - "integrity": "sha512-d0F6m9itIPaKnrvEMlzE48UjwZaAnFW7Jwibacw9MNdqadjKNpUm9tfJYDwmShJmgqcoqYUX3EMKO1+RWiuuNg==", + "version": "18.19.124", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.124.tgz", + "integrity": "sha512-hY4YWZFLs3ku6D2Gqo3RchTd9VRCcrjqp/I0mmohYeUVA5Y8eCXKJEasHxLAJVZRJuQogfd1GiJ9lgogBgKeuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3888,6 +5310,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -3898,18 +5328,29 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", - "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz", + "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/type-utils": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/type-utils": "8.43.0", + "@typescript-eslint/utils": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3923,7 +5364,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -3939,16 +5380,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", - "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz", + "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4" }, "engines": { @@ -3964,14 +5405,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", + "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", + "@typescript-eslint/tsconfig-utils": "^8.43.0", + "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "engines": { @@ -3986,14 +5427,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", - "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", + "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0" + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4004,9 +5445,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", + "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==", "dev": true, "license": "MIT", "engines": { @@ -4021,15 +5462,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", - "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz", + "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/utils": "8.43.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4046,9 +5487,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", + "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", "dev": true, "license": "MIT", "engines": { @@ -4060,16 +5501,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", + "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/project-service": "8.43.0", + "@typescript-eslint/tsconfig-utils": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4115,16 +5556,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", - "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz", + "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0" + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4139,13 +5580,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", + "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4157,9 +5598,9 @@ } }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.1.tgz", - "integrity": "sha512-o1m1a8eUS3fWERMbDFvN8t8sZUFPgDKNemmlQ5Ot2vKm+Ax84lKP1dhEFgkiOaZ1bDHk4T5h6SjHuTghrJHKww==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.1.tgz", + "integrity": "sha512-zxgA2QkvP3ZDKxTBc9UltNFTrSeFezGXcZtZj6qcsBxiMzowoEMP5mVwXcKjpzldpZVRuY+JCC+RsekEgid4vg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -4184,9 +5625,9 @@ } }, "node_modules/@uiw/codemirror-extensions-hyper-link": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-hyper-link/-/codemirror-extensions-hyper-link-4.24.1.tgz", - "integrity": "sha512-qf3docpmsHHM0OKLO5m2Fc8t4G+pr1+k9QwrhlM2iolku/INbz+B1JzbRcSU0ow1EcxKtHRtCFE4Lnu6DwP7CQ==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-hyper-link/-/codemirror-extensions-hyper-link-4.25.1.tgz", + "integrity": "sha512-BVp+bnPI0LtqYXAPFWBqpLLLICoD8QsTAC/KQVRf7l+MO8FXCP0F/4WoM724eU4/2bcLefBkK1gBgCB1+Ug1CQ==", "license": "MIT", "funding": { "url": "https://jaywcjlove.github.io/#/sponsor" @@ -4197,34 +5638,13 @@ } }, "node_modules/@uiw/codemirror-extensions-langs": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.24.1.tgz", - "integrity": "sha512-8Q33k/UhNni2u5VvAHD+2mxe4hNIqZTNySSUcnJ7urV2lXXau+0fimsQlI+GQLF7gy5F1BUzIi+yvOMrEPK9Ig==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.1.tgz", + "integrity": "sha512-P9Sxk0w8WgxxoOK4hC2yNV2f3shE0CH8gmk8lG5rDrAYYyuUrTsTmJANXh30TuQWCPCkEXwXZZVy+dbTYAgvMQ==", "license": "MIT", "dependencies": { - "@codemirror/lang-angular": "^0.1.0", - "@codemirror/lang-cpp": "^6.0.0", - "@codemirror/lang-css": "^6.2.0", - "@codemirror/lang-html": "^6.4.0", - "@codemirror/lang-java": "^6.0.0", - "@codemirror/lang-javascript": "^6.1.0", - "@codemirror/lang-json": "^6.0.0", - "@codemirror/lang-less": "^6.0.1", - "@codemirror/lang-lezer": "^6.0.0", - "@codemirror/lang-liquid": "^6.0.1", - "@codemirror/lang-markdown": "^6.1.0", - "@codemirror/lang-php": "^6.0.0", - "@codemirror/lang-python": "^6.1.0", - "@codemirror/lang-rust": "^6.0.0", - "@codemirror/lang-sass": "^6.0.1", - "@codemirror/lang-sql": "^6.4.0", - "@codemirror/lang-vue": "^0.1.1", - "@codemirror/lang-wast": "^6.0.0", - "@codemirror/lang-xml": "^6.0.0", - "@codemirror/language-data": ">=6.0.0", - "@codemirror/legacy-modes": ">=6.0.0", - "@nextjournal/lang-clojure": "^1.0.0", - "@replit/codemirror-lang-csharp": "^6.1.0", + "@codemirror/language": "^6.0.0", + "@codemirror/language-data": "^6.5.1", "@replit/codemirror-lang-nix": "^6.0.1", "@replit/codemirror-lang-solidity": "^6.0.1", "@replit/codemirror-lang-svelte": "^6.0.0", @@ -4234,14 +5654,14 @@ "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "@codemirror/language-data": ">=6.0.0", - "@codemirror/legacy-modes": ">=6.0.0" + "@codemirror/language": ">=6.0.0", + "@codemirror/language-data": ">=6.0.0" } }, "node_modules/@uiw/codemirror-themes": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.24.1.tgz", - "integrity": "sha512-hduBbFNiWNW6nYa2/giKQ9YpzhWNw87BGpCjC+cXYMZ7bCD6q5DC6Hw+7z7ZwSzEaOQvV91lmirOjJ8hn9+pkg==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.1.tgz", + "integrity": "sha512-6o8tQ8bdq14RuVFpZ7l9u8KnuPq824uG3U1VV933Uhv8mfaxaoaOQSjv6T2bQUPhjH6ZlEu5+tAMkOfIL21eIQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -4258,16 +5678,16 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.24.1.tgz", - "integrity": "sha512-BivF4NLqbuBQK5gPVhSkOARi9nPXw8X5r25EnInPeY+I9l1dfEX8O9V6+0xHTlGHyUo0cNfGEF9t1KHEicUfJw==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.1.tgz", + "integrity": "sha512-eESBKHndoYkaEGlKCwRO4KrnTw1HkWBxVpEeqntoWTpoFEUYxdLWUYmkPBVk4/u8YzVy9g91nFfIRpqe5LjApg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.24.1", + "@uiw/codemirror-extensions-basic-setup": "4.25.1", "codemirror": "^6.0.0" }, "funding": { @@ -4279,22 +5699,32 @@ "@codemirror/theme-one-dark": ">=6.0.0", "@codemirror/view": ">=6.0.0", "codemirror": ">=6.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react": ">=17.0.0", + "react-dom": ">=17.0.0" } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", - "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.11", - "@swc/core": "^1.11.31" + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0" + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" } }, "node_modules/@xterm/addon-attach": { @@ -4360,6 +5790,33 @@ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -4409,6 +5866,43 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4426,6 +5920,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4450,6 +5954,221 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.2.18", + "@electron/fuses": "^1.8.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.7.0", + "@electron/universal": "2.0.1", + "@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", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.0.11", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "plist": "3.1.0", + "resedit": "^1.7.0", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.0.12", + "electron-builder-squirrel-windows": "26.0.12" + } + }, + "node_modules/app-builder-lib/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -4470,6 +6189,110 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/args": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.3.tgz", + "integrity": "sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/args/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/args/node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/args/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/args/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/args/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/args/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/args/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/args/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -4482,6 +6305,16 @@ "node": ">=10" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -4491,12 +6324,70 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -4535,14 +6426,31 @@ "postcss": "^8.1.0" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -4631,6 +6539,20 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -4651,6 +6573,15 @@ "node": ">=18" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4676,9 +6607,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -4696,8 +6627,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -4732,6 +6663,29 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -4753,6 +6707,84 @@ "node": ">=10.0.0" } }, + "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==", + "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", + "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", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4773,6 +6805,204 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4822,9 +7052,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -4842,6 +7072,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4867,6 +7114,29 @@ "node": ">=18" } }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4879,6 +7149,60 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -4890,6 +7214,39 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4899,6 +7256,16 @@ "node": ">=6" } }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", @@ -4955,6 +7322,26 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4977,6 +7364,177 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/y18n": { + "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" + } + }, + "node_modules/concurrently/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", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -5020,20 +7578,18 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" }, "node_modules/cors": { "version": "2.8.5", @@ -5062,6 +7618,17 @@ "node": ">=10.0.0" } }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -5075,6 +7642,15 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -5119,6 +7695,54 @@ "node": ">= 8" } }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn-windows-exe/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5126,6 +7750,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "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", @@ -5192,6 +7829,90 @@ "dev": true, "license": "MIT" }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5219,6 +7940,14 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -5241,10 +7970,162 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, "node_modules/dotenv": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", - "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5254,9 +8135,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.44.3", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.3.tgz", - "integrity": "sha512-8nIiYQxOpgUicEL04YFojJmvC4DNO4KoyXsEIqN44+g6gNBr6hmVpWk3uyAt4CaTiRGDwoU+alfqNNeonLAFOQ==", + "version": "0.44.5", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz", + "integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -5392,6 +8273,24 @@ "node": ">= 0.4" } }, + "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/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -5407,13 +8306,510 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "38.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-38.0.0.tgz", + "integrity": "sha512-egljptiPJqbL/oamFCEY+g3RNeONWTVxZSGeyLqzK8xq106JhzuxnhJZ3sxt4DzJFaofbGyGJA37Oe9d+gVzYw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "dmg-builder": "26.0.12", + "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" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/y18n": { + "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" + } + }, + "node_modules/electron-builder/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", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-icon-builder": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/electron-icon-builder/-/electron-icon-builder-2.0.1.tgz", + "integrity": "sha512-rg9BxW2kJi3TXsMFFNXWXrwQEd5dzXmeD+w7Pj3k3z7aYRePLxE89qU4lvL/rK1X/NTY5KDn3+Dbgm1TU2dGXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "args": "^5.0.1", + "icon-gen": "^2.0.0", + "jimp": "^0.16.1" + }, + "bin": { + "electron-icon-builder": "index.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz", + "integrity": "sha512-XofXdikjYI7MVBcnXeoOvRR+yFFFHOLs3J7PF5KYQweigtgLshcH4W660PsvHr4lYZ03JBpLyEcUB8DzHZ+BNw==", + "deprecated": "Please use @electron/packager moving forward. There is no API change, just a package name change", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@electron/asar": "^3.2.1", + "@electron/get": "^2.0.0", + "@electron/notarize": "^1.2.3", + "@electron/osx-sign": "^1.0.5", + "@electron/universal": "^1.3.2", + "cross-spawn-windows-exe": "^1.2.0", + "debug": "^4.0.1", + "extract-zip": "^2.0.0", + "filenamify": "^4.1.0", + "fs-extra": "^11.1.0", + "galactus": "^1.0.0", + "get-package-info": "^1.0.0", + "junk": "^3.1.0", + "parse-author": "^2.0.0", + "plist": "^3.0.0", + "rcedit": "^3.0.1", + "resolve": "^1.1.6", + "semver": "^7.1.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "electron-packager": "bin/electron-packager.js" + }, + "engines": { + "node": ">= 14.17.5" + }, + "funding": { + "url": "https://github.com/electron/electron-packager?sponsor=1" + } + }, + "node_modules/electron-packager/node_modules/@electron/notarize": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz", + "integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-packager/node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/electron-packager/node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-packager/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-packager/node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/electron-packager/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/electron-packager/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-packager/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "version": "1.5.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz", + "integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==", "dev": true, "license": "ISC" }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "22.18.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz", + "integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5429,6 +8825,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -5439,9 +8845,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -5451,6 +8857,33 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5496,10 +8929,25 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -5509,32 +8957,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -5567,19 +9015,19 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -5753,6 +9201,32 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "dev": true + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -5762,6 +9236,13 @@ "node": ">=6" } }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -5804,6 +9285,54 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/extend": { + "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": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5865,6 +9394,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -5901,12 +9440,101 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/file-url": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-2.0.2.tgz", + "integrity": "sha512-x3989K8a1jM6vulMigE8VngH7C5nci0Ks5d9kVjUXmNF28gmiZUNujk5HjwaS8dAzN2QmUfX56riJKgN00dNRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5975,10 +9603,62 @@ "dev": true, "license": "ISC" }, + "node_modules/flora-colossus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", + "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/flora-colossus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/flora-colossus/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/flora-colossus/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -5995,6 +9675,46 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -6082,6 +9802,61 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "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/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6105,6 +9880,59 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/galactus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", + "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "flora-colossus": "^2.0.0", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/galactus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/galactus/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/galactus/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6147,6 +9975,39 @@ "node": ">=6" } }, + "node_modules/get-package-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", + "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/get-package-info/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-package-info/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6160,12 +10021,71 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gifwrap": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.4.tgz", + "integrity": "sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6179,10 +10099,40 @@ "node": ">=10.13.0" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -6192,6 +10142,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6204,6 +10193,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6217,6 +10232,31 @@ "dev": true, "license": "MIT" }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6226,6 +10266,20 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -6253,6 +10307,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha512-jZ38TU/EBiGKrmyTNNZgnvCZHNowiRI4+w/I9noMlekHTZH3KyGgvJLmhSgykeAQ9j2SYPDosM0Bg3wHfzibAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6265,6 +10333,19 @@ "node": ">= 0.4" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -6274,6 +10355,13 @@ "void-elements": "3.1.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -6299,10 +10387,78 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/i18next": { - "version": "25.4.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz", - "integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz", + "integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==", "funding": [ { "type": "individual", @@ -6348,6 +10504,78 @@ "cross-fetch": "4.0.0" } }, + "node_modules/icon-gen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icon-gen/-/icon-gen-2.1.0.tgz", + "integrity": "sha512-rqIVvq9MJ8X7wnJW0NO8Eau/+5RWV7AH6L5vEt/U5Ajv5WefdDNDxGwJhGokyHuyBWeX7JqRMQ03tG0gAco4Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.2.0", + "del": "^6.0.0", + "mkdirp": "^1.0.4", + "pngjs": "^6.0.0", + "svg2png": "4.1.1", + "uuid": "^8.3.1" + }, + "bin": { + "icon-gen": "dist/bin/index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/icon-gen/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/icon-gen/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/icon-gen/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6390,6 +10618,23 @@ "node": ">= 4" } }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -6417,6 +10662,35 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6429,6 +10703,26 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6438,6 +10732,58 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "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-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6457,6 +10803,13 @@ "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6470,6 +10823,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6480,12 +10850,102 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6493,15 +10953,84 @@ "dev": true, "license": "ISC" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jimp": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.16.13.tgz", + "integrity": "sha512-Bxz8q7V4rnCky9A0ktTNGA9SkNFVWRHodddI/DaAWZJzF7sVUlFYKQ60y9JGqrKpi48ECA/TnfMzzc5C70VByA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/custom": "^0.16.13", + "@jimp/plugins": "^0.16.13", + "@jimp/types": "^0.16.13", + "regenerator-runtime": "^0.13.3" + } + }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/jose": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", @@ -6511,10 +11040,17 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", "license": "BSD-3-Clause" }, "node_modules/js-yaml": { @@ -6530,6 +11066,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6537,6 +11080,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6551,6 +11101,36 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/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/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -6573,6 +11153,57 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsprim/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/jsprim/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", @@ -6594,6 +11225,13 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha512-IG6nm0+QtAMdXt9KvbgbGdvY50RSrw+U4sGZg+KlrSKPJEwVE5JVoI3d7RWfSMdBQneRheeAOj3lIjX5VL/9RQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6604,6 +11242,46 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6846,6 +11524,75 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/load-bmfont/node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/load-bmfont/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-bmfont/node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6862,6 +11609,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -6911,6 +11673,53 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.525.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", @@ -6921,12 +11730,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/make-error": { @@ -6936,6 +11745,130 @@ "dev": true, "license": "ISC" }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6990,6 +11923,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -7011,6 +11957,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -7023,6 +11979,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dev": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7054,6 +12019,190 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minizlib": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", @@ -7067,18 +12216,15 @@ } }, "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "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": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "mkdirp": "bin/cmd.js" } }, "node_modules/mkdirp-classic": { @@ -7087,6 +12233,16 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7141,18 +12297,6 @@ "node": ">= 0.6" } }, - "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" - } - }, "node_modules/multer/node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -7224,9 +12368,9 @@ } }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -7235,6 +12379,24 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -7274,12 +12436,58 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", @@ -7290,6 +12498,39 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7311,6 +12552,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "dev": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -7332,6 +12591,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7350,6 +12625,53 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7382,6 +12704,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -7391,6 +12729,20 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7404,6 +12756,64 @@ "node": ">=6" } }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7422,6 +12832,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7432,15 +12852,260 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha512-PIiRzBhW85xco2fuj41FmsyuYHKjKuXWmhjy3A/Y+CMpN/63TV+s9uzfVhsUwFe0G77xWtHBG8xmXf5BqEUEuQ==", + "deprecated": "this package is now deprecated", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + }, + "bin": { + "phantomjs": "bin/phantomjs" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt/node_modules/progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/phin": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz", + "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7460,6 +13125,84 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true, + "license": "MIT" + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -7522,6 +13265,36 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -7558,6 +13331,80 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7577,6 +13424,19 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -7650,6 +13510,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7660,18 +13533,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "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/rc": { @@ -7698,31 +13587,44 @@ "node": ">=0.10.0" } }, + "node_modules/rcedit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.1.0.tgz", + "integrity": "sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.1.1" } }, "node_modules/react-hook-form": { - "version": "7.60.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz", - "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -7809,15 +13711,25 @@ } }, "node_modules/react-resizable-panels": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.3.tgz", - "integrity": "sha512-7HA8THVBHTzhDK4ON0tvlGXyMAJN1zBeRpuyyremSikgYh2ku6ltD7tsGQOcXx4NKPrZtYCm/5CBr+dkruTGQw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz", + "integrity": "sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==", "license": "MIT", "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/react-simple-keyboard": { + "version": "3.8.122", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.122.tgz", + "integrity": "sha512-9u8Boglwtoa/SpZO3UyyEhs17z3vYTRFWS93Ihc6E8JFcCpa5kzt11IwWo5qa9KZqutqWuD5ara1mf5+WJYVGQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -7849,6 +13761,121 @@ "@xterm/xterm": "^5.5.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7863,6 +13890,174 @@ "node": ">= 6" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha512-dxdraeZVUNEn9AvLrxkgB2k6buTlym71dJk1fk4v8j3Ou3RKNm07BcgbHdj2lLgYGfqX71F+awb1MR+tWPFJzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7878,6 +14073,52 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7888,6 +14129,43 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7899,10 +14177,46 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rollup": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", - "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -7915,26 +14229,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.45.0", - "@rollup/rollup-android-arm64": "4.45.0", - "@rollup/rollup-darwin-arm64": "4.45.0", - "@rollup/rollup-darwin-x64": "4.45.0", - "@rollup/rollup-freebsd-arm64": "4.45.0", - "@rollup/rollup-freebsd-x64": "4.45.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", - "@rollup/rollup-linux-arm-musleabihf": "4.45.0", - "@rollup/rollup-linux-arm64-gnu": "4.45.0", - "@rollup/rollup-linux-arm64-musl": "4.45.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", - "@rollup/rollup-linux-riscv64-gnu": "4.45.0", - "@rollup/rollup-linux-riscv64-musl": "4.45.0", - "@rollup/rollup-linux-s390x-gnu": "4.45.0", - "@rollup/rollup-linux-x64-gnu": "4.45.0", - "@rollup/rollup-linux-x64-musl": "4.45.0", - "@rollup/rollup-win32-arm64-msvc": "4.45.0", - "@rollup/rollup-win32-ia32-msvc": "4.45.0", - "@rollup/rollup-win32-x64-msvc": "4.45.0", + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" } }, @@ -7978,6 +14293,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "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" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -8004,6 +14329,23 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "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==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -8022,6 +14364,14 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -8044,6 +14394,23 @@ "node": ">= 18" } }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -8094,6 +14461,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -8166,6 +14546,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -8211,6 +14598,99 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -8221,6 +14701,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8230,6 +14720,53 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/speakeasy": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", @@ -8242,10 +14779,26 @@ "node": ">= 0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sql.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz", + "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/ssh2": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", - "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.6", @@ -8256,7 +14809,76 @@ }, "optionalDependencies": { "cpu-features": "~0.0.10", - "nan": "^2.20.0" + "nan": "^2.23.0" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" } }, "node_modules/statuses": { @@ -8299,6 +14921,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -8311,6 +14949,30 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8324,12 +14986,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/style-mod": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "license": "MIT" }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8342,6 +15058,283 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg2png": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/svg2png/-/svg2png-4.1.1.tgz", + "integrity": "sha512-9tOp9Ugjlunuf1ugqkhiYboTmTaTI7p48dz5ZjNA5NQJ5xS1NLTZZ1tF8vkJOIBb/ZwxGJsKZvRWqVpo4q9z9Q==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "file-url": "^2.0.0", + "phantomjs-prebuilt": "^2.1.14", + "pn": "^1.0.0", + "yargs": "^6.5.0" + }, + "bin": { + "svg2png": "bin/svg2png-cli.js" + } + }, + "node_modules/svg2png/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/svg2png/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha512-6/QWTdisjnu5UHUzQGst+UOEuEVwIzFVGBjq3jMTFNs5WJQsH/X6nMURSaScIdF5txylr1Ao9bvbWiKi2yXbwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + } + }, + "node_modules/svg2png/node_modules/yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha512-+QQWqC2xeL0N5/TE+TY6OGEqyNRM+g2/r712PDNYgiCdXYCApXf1vzfmDSLBxfGRwV+moTq/V8FnMI24JCm2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^3.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -8353,18 +15346,22 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -8418,14 +15415,152 @@ "node": ">=6" } }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/timm": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", + "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==", + "dev": true, + "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", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -8435,10 +15570,13 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -8449,9 +15587,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -8460,6 +15598,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8482,12 +15640,87 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -8564,9 +15797,9 @@ } }, "node_modules/tw-animate-css": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz", - "integrity": "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", + "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", "dev": true, "license": "MIT", "funding": { @@ -8592,6 +15825,20 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -8627,16 +15874,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", - "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.43.0.tgz", + "integrity": "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.40.0", - "@typescript-eslint/parser": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0" + "@typescript-eslint/eslint-plugin": "8.43.0", + "@typescript-eslint/parser": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/utils": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8656,6 +15903,42 @@ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "license": "MIT" }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8758,12 +16041,39 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/utif": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", + "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.5" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8771,6 +16081,17 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validator": { "version": "13.15.15", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", @@ -8789,10 +16110,26 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -8800,7 +16137,7 @@ "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -8907,6 +16244,36 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/wait-on": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.4.tgz", + "integrity": "sha512-8f9LugAGo4PSc0aLbpKVCVtzayd36sSCp4WLpVngkYq6PK87H79zt77/tlCU6eKCLqR46iFvcl0PU5f+DmtkwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.11.0", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -8978,6 +16345,25 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -9005,6 +16391,60 @@ } } }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9116,6 +16556,17 @@ "node": ">=8" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -9140,9 +16591,9 @@ } }, "node_modules/zod": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", - "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 81530bff..78bd621d 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,24 @@ { "name": "termix", "private": true, - "version": "0.0.0", + "version": "1.6.0", + "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities", + "author": "Karmaa", + "main": "electron/main.cjs", "type": "module", "scripts": { + "clean": "npx prettier . --write", "dev": "vite", - "build": "vite build", + "build": "vite build && tsc -p tsconfig.node.json", "build:backend": "tsc -p tsconfig.node.json", - "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/starter.js", + "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "electron": "electron .", + "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"", + "build:win-portable": "npm run build && electron-builder --win --dir", + "build:win-installer": "npm run build && electron-builder --win --publish=never", + "build:linux-portable": "npm run build && electron-builder --linux --dir" }, "dependencies": { "@hookform/resolvers": "^5.1.1", @@ -73,6 +82,7 @@ "react-hook-form": "^7.60.0", "react-i18next": "^15.7.3", "react-resizable-panels": "^3.0.3", + "react-simple-keyboard": "^3.8.120", "react-xtermjs": "^1.0.10", "sonner": "^2.0.7", "speakeasy": "^2.0.0", @@ -96,14 +106,21 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", + "concurrently": "^9.2.1", + "electron": "^38.0.0", + "electron-builder": "^26.0.12", + "electron-icon-builder": "^2.0.1", + "electron-packager": "^17.1.2", "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "prettier": "3.6.2", "ts-node": "^10.9.2", "tw-animate-css": "^1.3.5", "typescript": "~5.9.2", "typescript-eslint": "^8.40.0", - "vite": "^7.1.3" + "vite": "^7.1.5", + "wait-on": "^8.0.4" } } diff --git a/public/favicon.ico b/public/favicon.ico index 862e8505c2f40247d01dee7bd57563c18868cc4e..d33a4ae57d4ecad9a1b5177271dbc47aaf6963ed 100644 GIT binary patch literal 171947 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 literal 0 HcmV?d00001 diff --git a/public/icon.svg b/public/icon.svg index 4a51272d..7543df18 100644 --- a/public/icon.svg +++ b/public/icon.svg @@ -1,80 +1 @@ - - - - - - - - - - >_ - - - - - - - - - + \ No newline at end of file diff --git a/public/icons/1024x1024.png b/public/icons/1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..66f1cc013fdb758fea08245f21a4db2dec830091 GIT binary patch literal 56926 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≫a>mH9EsPXo7mHJYlKLp`CH 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 literal 0 HcmV?d00001 diff --git a/public/icons/128x128.png b/public/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..166c95430582c9d9f66a7dc4a6af8463fb3e12b2 GIT binary patch literal 4072 zcmY*cc{o(>+s9XtR5K_u5e6~#WeC~FWXn2C_Oi>8CE2nwG$bMG*uIj95ngL#-`B=6 zp~6hICbAPs^gH^!*Lz*>AI~}0dG6)gRm5rfYF2)Qc`uL& z3(V`xT|}(}4C@xS_HnBBI-B$0?qMdm=wJ%@R?PGD`qz&iWx5z|Qsx|3wIJG|NA>S1&5?VQNQiGs z0u-B)>H>u^jwH?Yu!GGn)3}L=IA0zsJmGnXdqW_Nv@YC8H_uSnIod2+)u#yR zvFVqv>7ij*4e!-bQBw|Zzf2qwi}S4nb?wU;Zc?kAW^RzW{dlHrYj@rj!>y}`7XHbC zdf(Pndvg%~ymF7vNzJu()y{NDA9hI-)SR4eb;99B%qEX=b8Q;9l>_T?75rKT>!(kq zhTw@)2?I#tNK#uHIZtiQm^pfOj|2Iyt>g=rXk<|o&NmbHL7ApDHZk$;F6(*^b594w`xZV3ZkU@L^1(QqV>a^RyVQJu-1 z<#jZiyC_~Fn)vcS zk*0>s$no28ma@uBZB}uy()RI@a_s}rX8$o=+9c>h>%6R*6=`iOio{>Pj9w5#nucxg zguNg6U%9=CiZq6ng4iTeq9e)X3npGNY}qUw2XxjAKMjy=DPV@QgCXF z%$Vcj<0aS3lamM%jMP-mITQ;Z3GwYYY=la1^H8 zySS|E94&qJTZ#M7e78>Uuayfb+dVJ1)g$u8b<)g#N_yiAeo)5Izs!ch!Y!MM}3aCz-4YG zctX0qi?$Z(ut)eLuilflE7aKAaRe{_RNiyrReAYS63IvsQ*GIy)NkcFRmyk4JkCC8 z>b(NBr(bPfcuA<>pkOq7qgLED1M67h35OC)PJDNgp{gxqpaOSYDOhr&4XY-VaswKQ6ZH42dOW$Qq>y&hHlfVUTJ&WNS)rat-@) zWV}^hr5ckXbEmT6L1#DrOICey`a>m$Iv~xA)mx*@UjJ%bC5q+9dtTv3daq5$xr`R5 zhAoRl4!>>t!TZX+?>@)VGcVzeINQkMXP3d-tfCqEc{eBfgSRNPk5`tA3+1=1}dEifW84#8{@M-1l`jtR5LVd;A)TV54G+XHzZk;`V}D}L9B<7Rw4y@Ry)PoWbs`20re5Y~DBuHb z+S@UG#nuP{Oy*0pFFgZTAX~69AU61{LQtXMrOxf1+)E(!P&!p>N5%M*1TEc<|{$gR8hT>QW>-qX`&>wj?Y*a^83TO)|EPz)@uyu|}n zRFOPh^9h{s-5Neuk6lZAT&Lc9O(NP)Gdw)}_mZ2`*0)9K>vB(C3IDHSQ#W|Q^T??R zn^8)u-H2eXV>CPwvhexy6%}#*p?~EJMY7oYUL@Ev(;f;BeH1s&wqP=X$O}6^{Vqan zNZC=I=@8r4qC=A!Q{)U+J~lml9dRgdL-mN;O~R@r+p&5ex5DOaL2GDpwq9Z{gDHnh zPkp^auq7&8(;bhN6iyOm?*x&SlUYrEe}XMYFpali$Iv}jS(5Nk@*JhJWBTsz9cymY zj<>OFprA>+vWR3JWzts(_4!a^wchAhCaW4}8Gn2(w8nexF_4vxLLyhrqfC21(^Cy! z9Kfb$OWgIF@}vt47_}66p}Gk-jnHqm>Bbutzobh`#p^=ltDJ9h!KrO2OJ76&jp2c6 zq;OL?ZW^>Ei<%c*R{T)ju)VNuf9EqJSIPBi}6zPS48P ziDHI#v&2&;Zy#6QU26|CJ<0!>z7c$WrAFKLuVkNvy&WOZ>H4v)fvVDg0zDy!V?5hw zh{9NPewi(}oyKI;UgG1Sw^A>|zB3@rqQ_;cmO2n|jNMj!R`k6nY3%%ACHc^2(|IaL zS&yQYwbRU*`o@c1cu6wf4R68#fgnM>pm>4hq=oZO;BCIRHxfvp!(v)Vo0ufhcO%5$c+1ORHwgCE)8O}6`ybDP0bQQA>kO4MI z7JbJ=RG|+2&It%un`+&`XZdby&{I*`C_WooBkKDQXN=SzU0V(saZ}M%D&A_>!6^g+MG0$fYhY(L61rje{+PEV>uvkGwt0s2 zat=cg32|rR3v|J7j^iyiDwJJP8`ZdwczrgK%G5|Zpy~jPOliy32wi-*E6~Gr!2Up$ zz7uk;Gk8nuVEfZ(7z^h5!5@_@`XJ}1g<2+Dp=wkC9KrUt^4410;7u_P+XoSLCDC2w z{Rs*iueXL|29i=ito2d$OOhnS`)}iH+nKQ(h00ONvUGhoIDNgw#zZh7_5-egS2%)@ z+cn&uBIqQ@s3pM%X&T=)fT>y$4#O^O`!o46{py5(uk*PwYkPaCnDKp{yWR4WF0PPQ zQy(e-gLu$)4m{%><;eaqXQZuv4hM}%<%%qD*Zo@`b1e(pQGfSxxeG-8#xBr?M>)WM z^&%v@v7L+0E@C8no<3PA{M6o_JMT4D{nUV)T@O&htQT?auRi47(YwJ36*CR~eq9D% zbL>3;$XN2wBB*!E{?gvxoAAgOhUCEy(Ft9b9ci5l#d%4-^0svxslsX5|>s7oYW-X}E@PdSz6& z;kHSUbiFBGi&bDoOzy8Me|#6fB~b6sefDu^XpTDy3v z=-u1f+j{2nTMUs(lvVGJ1nm>)y8)^(7L~2`j z*wGAi{`S3JvH)NhK@o@6KGlN2B$mz_149|p;U7c)Tq7FguNIq&nx1btkh^h%4f*9m znHd(8J-E=Q>*OS$H6eHGR7hCSrrBDd5?K=$`2iqmok9=+5cTUjJ$@q~#O)gKQ0t(L zSwA@Aj0UF2qys>NuGQb1khdg!0v*mTYOd}RqkvhhDq%!La-f;R{4D1e@F zAN-Rajok|j++bxz6DQ=tg@$W%%Gkj8++1H0fa;ZK_dPsNC`g*ir}h%}cL~6-u)WxS z-`#!g6)_Ud?++E^@9|3&ECV6=f_`pM$g{Ia{>d;HHj6BGv{h-R%tqvUBPvZ_D6#@ssBxIplYPO5yD| zIcZ%K{K`|5DGKk31q~RgQ$nwDab=&P!7dO&*<4ciAn=h_WQKxYUVpWd&P7rV`Ii8u zNelJWY&oyEvNHK}P_lQ;hBlQg<_RP}{V+Xkc2Av)#!X{^t?8223X+o_Ncr=0Nq?TH zdazG9jy*Ujj#^Zy5f>PiMKS|y3BvrPq9mm}e_M@A!itksX2trqj}bBFL6VNZQ0hmb zuC)Fk`ac%nTOuIN!+sOddg}QupZ=`h(HPQ&)(hu;2q89m~s4Xt&$A^7%Xi z14md{so+{)U&mTYhv)NKxx7TVyo76Uae-Ve$L#DK4q|Xb$$;21{11cU=lP?2TDZv9 z%cqFaNjBbnB(|1rd>nnxw=iFRusd;r&(|+;xAK^8k6LtNa9HCJr$yW^Xj{DR5t8V} z;Mg5M#?IA~B&*vD-Ckwx^fWi7uhET zd)YRYRt!tDYEcnd)-i zPHL^aE+UKN@{LPUD9p@7QFO7kZd-~ZltMF zsZ@yLm`0;+GMW5o>ztcWRkj>I{yry9e#kJ&;=?-=V7f&(fh*t8?z$=k$`vWLQ`@ z!In~KmF49x8HAyWA}GS6V(6j>?TIYTSW*nbvh>h;YntfA#|c($5qxtSQ;SiA-S8$S z_nnHONZ6(b>cBlYPV2P;w2mFb9T+3I7qNQ&EB5Zqvr(zAwzf_`LEz&}PVPGuMUk*g zQ0$-g@xKl6zrO=6C4ON)t+%JS8$^s;yTgcQSX*1CpCIrt^q>eJq!T{rgpURus3NtP zow9UEF|?lBPjmhmk|*|HYB9l;KggDD^Xk*Hyf^N(envLQv~Hd%5GS?2#Sr{2uimfuKma-pDhuFA&Gc& z@-(3rOBX?Tm|DF~JC1ns^_S^%_RzxRDZ-yN>`pQeJ zT)xP$H;!V}Vbx*PVb$?421pn{yWOT*t&$`O8yg!8D3Qa5pZ$4r^RMSsm3~G?M;RX< zCk#XS5rKR@|BDeZALVjmsff@|v)LpJL;4Ycv9Vk#B8$QUg~E|JRXyXJn*|KRNv*Zl fMP#vDzH#Xv*mN8Jg=;H-00000NkvXXu0mjfv#?~t literal 0 HcmV?d00001 diff --git a/public/icons/256x256.png b/public/icons/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..bffdbce7abd13be482152c74307c212c88f297af GIT binary patch literal 8597 zcmb7q2{aV!8*fAniW!Dv34^S~gff;ygGToCZ>eM=Ted0t-o(%tOK51aWGDF|B3r~r zCX_A7l5L_8%98EAqwk#mz31L@&%NhxIP-4L`@GNddzRnCnHuXKWfx#SaNxjE%q6t> zfdfq7U#0_)L*S2tU$M)917Z#sw6KRI33U1cWUhPGy+niz794+-@R!whi5?*I6 zl|`!Nfoe#ke2u(RP1V~P`JfN}OA(u6zG}xaPrh0?X>0RpX%)he-yE@1H&*}h?o*uc zGpO&7nv^Dj`+!u(bsBeW2a}oAD*qj{%6Xx@^Xv_#@b)VgFRHIs9>7h2U#ty@D|0S{JaX=%vs+(N&j-@ z^>t#2ROx(R7Jb}&#=BC!K5TulMPL<8KD@JYke9w~B7xZ;<92mm+(cv%I!%M>Wdu5LenBJFi5K9Jtq+a0YWLCM|S{Y}S1cQ2>lBg7C3{MgyF zQezj}@Q^HevzqdYRaAGRPbN4|f@Nk1@Ae zX%IECsFcr>W_KbM%TkpEVJ)cGb&>=|I?VtNg~E)jiN5k(l_sV0^h1XO3~})qMXqmR zE{{CMHK$2*R;ba{RN0Vvr2_&F9y~BKF-b0z(KX8{H{Qqq43QV-K1hI(mI~p8t1rclWb$EH=-ABGH3+wJ~}0vDk)|amsYZfeyBQ zcNrd@DMJ&RIHVI16@-*^%SNlCqSy8OQU*8M;|}FGJZl(@bR=npmA%=(T6os zn2jB1YvRFHc_*Twp`juQrE7);kFP+zlvxPZmB4VL@wS9@uLG*hWo29T;A9o!*Er^b z2L)xj>&7|7w(Vad@5^_o`g>B8jgVB@V~rk69}&tt7UCi{wsl!Izo{c&Di>=~D@ z8M@cTBRN_kR52-u_b;`|mk!8IP4U2WqmV9(?ffMJ?y6D}m{$4e+Ab_n_vX!^n&;2W z3OGCDb$)sSd&?xGlG}g%QU*)f_`yjcRzO|RB_(c_X~Ad-3Cv2D82+Uahl%jc&i5Xf zG=qq*jrEbXhDJt8C{&azm0ShXiN;H?5g_}MPfq2iR*{5j{jQcss%5h zh4Yh?>~P2?Ag+WcRbFUcZuzca zeY9QUdh@{0In%FJLKO(a9yKK5)SWi|<02wbR07iFYkM&<(FBUkKe4nkgW(P*+4wf- zTpPbI*|^6KEynu!#mjdgsmfsM3^EyQRq`eldj2D0H(sF%Q&~xo+uhgD!fu)7<j1<>vFvHj7Y~?M%D>_7+gQ<%GP0Ow6Kf~}Kz6JHMdA?KPQpEV zq~=~NKqa^5*_G#(($MJid(g2CU(!^HgaZeQiz%GdHvKx$R%1n@NSb}#2jMwc``Wkk z57YdmzEpnkgknqC4}*hx_V$8fAw*kg@F?IWC#9s~kd?ho#C$gcNK}R@-D|eCcUGGoHj5yuFBx3Im2k^)R1UH$1M?G#VW!B}BJ4+D?FTbpKb))D)4eR`U zgwxHT;##{?T2u=Y&Guu>8$+Jsy9Wl`)2hlDxT8uBRDrRLZN0oVHONdlz7K8prYYFI zLcxu<`r&aL3JnLae0OVV&OWP~6*%3z?YG#pbz=7?*B*hM9T8O}L%o!BEY*N?{1T*T zsKRbwRlK{-BSSljZds4`o6OFMicRx6U=NHgyp9`5YW+C>gl zh)j6O#776Cr-h^tyz>MzTZqloAlBxe99oGuPHcKgwa3uPhs518T@MxLo&>WJeY7m6 zv_)*GUrlvQ%}SNlmePxw8Yi0h?Ex#Bm715MZ-;YAX-}pv81(0sQf>`hix3`iz^PE> zt-4X+zw{&UX>vA-A>w(x=+xGwG-7sXBhjNkBkH!>VuJzUAcQ-Mu&MD%#kn z$=@e0n`UiiZ*O~vbjZBKQnAoh0;7{6AupkS@c3~Q9{JwM-8}^uBVS5h6p=)7GOQwJ zW`fV+85{IytjfD(IvFad2KwOh^b;$SgpS?_3?z@%XSa}DeD-!mFny_viBnTk8f%}X z_^bM@%B)TVO~V*c7Rv^F@ja}cc%q$L;5kwg0&J9UZ`M_jv1`QE%kkIb0{75RO7nJ? z0)PNpSJEL1IBUdjn`<|Ge2!7&RdmgM4pfXW3{zL!M>%I_Ykl>5^X?-dwc8Nz9QBKT z&SvjzAA4I#)SGZMHPPBpZr&NmcDa2Uj^(T87aw`)OxauYsm)5ql;5n}w?eShjTVkdB)Y{0>ghlor<-EK+icvR8HjVxFMpo9Cy{?4f zT@dq#$@i4coN2AOwf^z*XX>jvbaUA>MHi&0)!O1{2mSmys_ExFt($&+DOK-L`UU2> zC&v5wLdpmL8_KO2ZhrFQ$xpBD$R%p+s?PXj0FOzL5W`0z6VO%wPH!%ag@tZ*U7PL8 z-aV42u_CQ&_79YTvy|nXXOrhHeTPld>}LF~%yiXGr(9NDYPj?CKhIMZ7f%E8J_JNP z_r*uEyu6%N1pH=sbSeDU!P0kaZ4%%vKwSioZXuOlJOUtaJ4*N4w+~o`laIXAirl0R zKD+Wp@}Xx=b#>b2uWxyYD}v3-7~15lB31cREV9rIMMN5PT_?KPabgYo{P?k%=>Q~O z2vNy}GygwR(huk=QYmU3h$6_nK=x(nXyoiPYdS<%x=mdANqi0@9$DD*nmZ37+j^O$ z-3@I@L{{+rrxl(HO$1|o1mtWsOJ*cOS_dARqnZF_zdzl3a0m#!(%)|2^tD;wJRv`jQYR8-nnJlcuo1asxCHY)2>_?8p8 z$JAQYkz+cWvVXcZ%!;L*tjFid{k*E}*L%8ISeISOsJ@K%@smM*M*@`p=WPtd*_Dsq z;>dqkA=>yOBR2NH~oDiQJjRW73*|&=NkHB+w1RjM~T+UlmvN zMMru&HxwF3Z(Rhbrim1DY{E1M#R)3ri{PD4gSLJ1>HYjv1_$4je148SY+vKqu6&j*lxA=*_Wt1d~xNKNv$)7v*uy3Uz&Hcn|2eLZ}yCZ zy?FVuudj-oE+mERAPIX8SMv6K`U}9znwpI-<1aZL0$CAA<%~>yDMw`B0kwK__1M&< z5tC2nI3-Pf$X@hOKEh;>1w3N? z0^sh~&i?uHXYzZJu==M;XSu@XC_2ay`=|bQnGf z9Iu|D*Is#j{2tP&;1H7!3&@WFw%b`r**VNvxzMx_`n~&ogQ^B?Mo=No;WYAru)6<0 z&Ry1qSX{SRiDj`=@3ce$K5Pp7ST@xq0t*BnhBJRQ4|(lAQ#9vHuVS_$!G>9O@Fx9rc&Pj?5b|? z9j?t+{t%tK-W2iXf9`Haa~67^Pn?cJ+SMH8i6J2dn6bvzuNz{fVS8U#%6p7@{phRH zJ6$EC|6~FScDi{SPZfpMclD1|#{7D~WGcbApb& z#&E%v8xL=PLT6d9Z#;yz$w? z&w|$6_4cnhj%w$8D_0EbQ!Dkpa3ttsOY(BiUU*;DIT-MyoDD*iFTU`A2S~3F zr)tTfOZGUEW)VJdVCFEV>C;FX@KOJl5T@J89KiSk$FFdJQhS>g%Ut3hId-}3##6y~hT z2wj&aSg``SIT&DH`?AE3vCpNI6*s!6w4-jh4r4*Qm-mf_w&y}tF>+BqQj8h)a^`*-ci=fNA+704IhNwOkD}vVhf=r+eIOzxD_h^j122`760qb0UVH{`lYPb zmsDp$*1=?WgsT7?Yi##=Tii&fect+`hjnt(bzTNykYe&CsHGC)f_XDzJNVqZOXZHw z;vfjJj$^Kp+~p{lJjAE+blD;K!`sZt)+VO9i`0D?%Ps5VZCAN zq;n9MiLxp=ptbvQs~^J*vW(ad#5fnwc6SF zvcPe!(N@8V`gFfKyqj^e8smX4JurV<$7XFe(4|zjyZvS~qI= zF6>Qj<>>ApN20di`PNAqVT`+&|LK-Cmm_;1bPCMB&X+x#*jd^VJ9}kMPd1H@xVgtX z`y(!1-679;B>de-%(8-U^cgw@_iJsn(ncykpLRWn>Ny&eX(xC+sp;} zTw>MHn;$=nQ5E(+Hq+A|jq4Vew~&WfXbrv{=I`tovk<%OnAr#7h|=44Uij_Y88QV` z<}8}o#j6BpR|jCKw5D=&Kl|+czo}P|xZ^CIfj{AqE3?NS3Whqlv}5{cOBfv6jebpO z(!ux(7c_v^xp+9aQ^+!dYabJ+w6sW+^Wr6gx#YX4OUF-YoQq;?2~GvY0Pd8dv2iXQ zq}Jmvvsd24`slku(>6sx1}6@ZOm?zuow$m@_;JRrKYys6d8~BpZ}QK8glcw4jS%9> z+QHNl$6G{7fiQ>vP4-cbm}j@Xd`O+{M1qTAVn|{Sj<4#PQuIJle~;VM-;9kBmVF{u z-hiaq!c=-y+muC@aScAZ98BFZ@t;-2QvqB&{=zK@H)+STeFeaf0465`9xsxWzugNd zILyg?l^m(VxZ!@X*+*bDLi{*hG?kbo8Z@uC==v@c1}}nXBMU#UW};UbkwArz`28w{ zj|unr-vWjRN)*)J%7)BJ!~89JTuLBhTNEgiK)ut?Lh}I2CJCUkz)D;HJGbu$z_h;= zfD4bS`QJQSx$FYZ-}TfBIc$u&D@VD$DB#C*9sSybq2^{kC;Yk$G}VRoKTq~=s*T+1 zxfKxlSRrEOYWGbi<>N3LxD6dq=FXv12kLZcxb<^#5Z4 z$6ry19g=4`A^S%W3q-XrqjuP{_mBj z^=tBRLn9j*Nw)g~{kM6o+G(d9X-5{nH1Jma`hTcwSq&|M-dNe>Q0i`Gho$SE2*T@{ zxVba+S$YKzsn^b3S(-2$^&eT#So?XA-2VRmr%EPEO6p1KIX5}m#@KEPxnMG~vI^}U zyEG+`NZ0waVr|Vqb1;KKabkSnwEw}1#!Vleq)lZNmG&&n5aj)Aivi}Ue9zHW381~I zpwRFWp&qc(JakG^Qu4eRXpQ!g=a5@dwZLw+oW3^a2V|~=|HbxVj+&=d7-I={D6S3W~ zcekWOhBhgNcu`~hb4-cHs_94lD{Bg9avu3@X9HRAza#y9oJ^uUeOEd3ZinD>i zbRfX8ZNGpu8?)#3vJQ)cCY4n*WeR$S$_bu*T4e54P*8C1$=ed^G9i>79t;!8?U!ca zq_RNok~TTr*n#RWv{X@upEUkjJJjek1^6!~7e!6Y_Zu55E^~A9^G?H+&Y&nsc18ip zgqR>)XjksU*9@vZ(9Fxgn8CNQLRUiQhe>kr8!AgX6^eHyWWG|wsRUa)PLcDpuWw?J zmlJU$^AfZsl3IbIOsNh8Y9%S%g=%296{+$k!~z@E`^}2y&ysLLHt_ayPQ<)^v(qmf zK@a=g%o#*0P<`yG#-0FZfC~x;Je>_%=AGXso`BOfK{Y@ND>(j1?JZYVpOh&fh5N{{ z+)@>n2?_)J!G!d5_HLBRlTtV0GAC%l4^K?=?Lo1jnQC@+?<6qM$U@(HP**n@3lpNR z552pnxur{}0*c)hw(@fkMZBo4Mj-gH0*Qp)!NGVONiANZN9&E8>QynLs^+-of$3_X z>2>OqGZC3ku&*-?Puz*?O_zNK+f(RX9 zU9;)tdO*0n1Di>9ttd`^B!4su;8I{PHMP%}6nwDd@Bf&=vp+S4D9%_3DM`mW;j-x0uJZx? ztgZc>>gwT<2W-LC)eIhC2XAkry;iylc%y|MKf>Ekd&czqShW-hQN#_~8072^cQCoS zI+|c=Vg_G#v0Yn=i8)k6jw&KOfBpe0XZ`fk93`ZJ@WNC-MIvzNQr{6QZJUQ7N{OW` z0XX;ku6$R7yN^$Y*Zfb~ld38Yg$@*bOcs%)rUSuGbXFb#Jc^PQ1$X=Q3yTsMsxW~AP!+13cF11V2GFO_Nh`ecxF1EB|tb{U4cObqc9j5%XcGK|SlTIwG>s*go_=^K<$@X7iO7#NHc4UmJfiM;>y|EB zmFa!uwuf!{-sHpt2}@z=!3YSdTQ1Do`4&Mm5tU|Wp{k!_eIL!KA&Kr1K-}4BJQQfY zpu9Xmf{lVF(>Jf(v|YOoSZXfc6pbQB&mX4MfcU$Dbnis1AA+&bbkN}3%a=JY+)yS4 zZA;z8b)$fxEiBvu1__$IfN@1z^#L<0Dh&_x`8oH7!rUVxAgq~s=T7-2KCF;$ccU5F z4#fl$!(XU#D0DdeK!OQ&SQ;>{sc4oZ%^APA!9iCre8ZzQ_eSXuu}pp&Da{8v^#X z(Kmye%mgiqtY|a?5N~eSF-sB`^BZ=?=OU$q-Rm*VRTnNhA`wXb!3AYSob0*3R}DNV4TYfHZEA|*TiW@Tnn(PXnm0+G*;AoT_Uzj*wpC;PyWpi1 zi3St&`H9BnVbsebKK$7EIOKew+4Nq{JDdNq{i>J$Yz+kfhE1v!d?~=FyY4ECEz;3& z_2bkE|I{Vovi<8p`7UokfzK$at*CqwgQgE&3WrWR(5DSx0p++#12pr*G^jlhzf?s_ x-B09^im94P)~rq#4AjF=!OMBTVi7A|1rMiRG#NHFf)5E2Lp zE=BR3-JkZtEt$g2X z(OPd4kzT-18?jPq$T_#KSd0g*UcDl!tE*ep)dimy3dOD2Y!+)Rwd8pojg1Qkg5Y4G zP}u6VwJqOLEEYeWnwmmHs5KEGpU>m_ey{KQzZ<34md$3FEtN{q-rkN<%$q2R7#X>O zh%h6Y&5}$ue_%wU$6Cun>2#U{2lf+&A@l9<;Unzaxr-UrT12Er13t5C*zhW07*Y$H zHf_Z7JZAAVk2rJYEY@0T;q>V^kMt*Vpqaaj}AGg#%}(A0vVjNImx#O(>fleY*@-9}I5(KwU{^F>pJnzpzlARz#bK#@E(!=+M{9RYOAqh7vrnh$!@M27h=0*W{C5+s5?z<=Ev5 z$i3G|{7iz}SAS3q%@YWU87>*(mek`G2K=YssFtAwt32ESpZrU$Os`u;+>zj4P6&R^ z5FWotaOocH{ryzKP=cVi|NbfpFSam4Pv!|v-6lM73!Ry!``J!D8u*m->Q!93_8XNl zlq28|PY@ovN!`V9lyxW(G>+#wSCU@6ifS3kX!>jf6~!n7Ee?e+-wY)v1f46Oag;K8 z^;a$p4lzES!J&B^jiVex37%M<`^MF(N`Od7eiikst_y zfnqWKMDajZSGV$gvqfvYO+~^6hO|->^nwT_3Lt-(ldaF$uNli@C3xWwv zZ;gKeg_yYjAMghA49H)$8@H$;l}~2(%;T9OZI}VzHRyxjsKO zcJeRL+O>n-Q4}4_vaIL&^&6NO?aIuk)lv$xy!YQ;zPx9Liz5FMVAbU06c38Y$th+% z?!Eu17vZHeO$i}zH$y{1tXb2Ch_Kl5Jm=`qqg=Uim1ZFX(ljNGW9}{T9bDufP5pB0@7WLqxhA z_fV-+Xcs*_J$UcA8^=8yKYoHFNoWU0jvOV+GVUho?X7H2)4Jq&PO~IQICbh2gM$Nf zcXy+zs4A+8s-mi>DyoXAqN=DWs!9lfD2iwnf{|qzs*0+js;DZeimIZjs4A+8s-mjY z>lwqtAMnN-Z;~Vl&Aj)-ag3~6_oPlvPPtmG#=Us&nTt|s2?q}BquJ=_5w>i3mAMEZ z5JKQ?Iyz#?agR_8Ov=dK@3_&HL!CAl& zXa|l~MMC=ZUd%g!ICAl*L>R*T+(cbXOKimvz&eV52C8{tzJbO$6UH}nZ0}8WxjatiQ|~4x3{uA zP3w~9Irn1XAVBAP7w{iVVsV7_c7YQbtjm)PEF*b-75SPnGMzJd?q3`}{C9Tm`8`jr zUk7IF-TN-{#d}X2$9P&0nG$fA3dLhpipQ#iWijcqk5GGVC0gm?mPn{S^)U6P9wr)3 z=>E+=;CPjZiR(O6F0l|iEph}fFoYluH<8l$&Ohn=<9V{R4^jKpqohywf)g5aMJM@} z%M_1SSxAmKGK6NKTp(XlCSP3wCoDEk3k)HM!w_z9LVf+0s6Drm>}x%kcQlhMP#hU2 z{$QMi=4n9?haqUCi*(~kYR^7GSRQky=*kQo2QLvHx{RC3Xz2gySLj>4it7^-ECf%B ztmv5d)zc6O|0g%%W7p`|f04rJPr<-B$EHo+4j~r#+fj{#1T&^RaI#SF81 z_10Uv7#<#>udk2c;St8i$C*!sLIJQ3qZKPEOT72rsnu$j8S`m;e4KOV&M`eb&3tms zQ7V-%vtPSB&vpS9%HC!UA zVlfFJY&vu1^hd&N0|P_PM3LJKtV+|AEXy$qv;z@Gq2P(*m{}J>2tPi3`s81kB{b>p zA6ODa(aX-c7lHL6(n%XIvjjL1LfE6KJI@~07*qoM6N<$ Ef@rGfsQ>@~ literal 0 HcmV?d00001 diff --git a/public/icons/512x512.png b/public/icons/512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..2c58b82ecd1782f80cc0db09f98dfd7a7ef75bae GIT binary patch literal 20523 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%0AF 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 literal 0 HcmV?d00001 diff --git a/public/icons/64x64.png b/public/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..13da126a1e7c259cef7e76d6af5bf8e919105dba GIT binary patch literal 1989 zcmV;$2RitPP)ad7iwV2`@14TB**zm>J-iU6a8$%s^ zAxuyi2;zos1d(kFU00`*p{Y(LtwPmCn>4X?D{XDsJje9jn|sdr|8_H4h+UtOlq9$P ze1tP-Zf;%bTzIbtcZkSZ5y?@3%xuKW-UnWd2*3Oj}#O zwnSAQ^4=I38O6-FRGAqgBcpilQPp3yw5+PvAP9a4)D#MNy!Tw{y!Yhu144SVd(t%J zY?d!y&Qni4$;y>0sT7@^UEF{FPZ=B>el_1CYX5^dOU3t<>?CT2_!~9UaU>nx-5)c#s!g{0oDFL!3=*Z7pHwa9p6?-d>)5 z`gdFm&ph*c>gwt^-JU&rc<{l8nN7-NPPxn}mpNsM4B=AZm?AIA1TjPLR655r7=nr5 zQ527uAsCg&F+&~&0l^S66i+2lrYUa8QQX*wMFA5G!HbfJ!;4}FmB2AgercAmAFtxX z_rFT?uU?`ZM+gp$F~OrCAQ);M!I-ZcGh_=w3Y%9@Xj?(pJxtVon9RG!k=SDhsRT?= zJYt4m%qNbEMIdg;5x3;fAC#zi>nPEVBLuy9CU_JC1Y>X4KCZiN4bC~{TF}t2Y^#|` zp-|*Pm?$9Ff{=J^JysQPI;#punsOA|RuDJmkWz{}Rse5+uyfbDY}@`apN);N;tR`J zSX0A<{reBl-oAs`kj-WZ0wri@Shm&7q);ewAxsnyg8m}WKMvszjbT}bUz!CGK1aYe zWGUU$KyhOuwjd} zcu@>NVo%ksV^r-rMtVh-@eNl}+|Y=xk2syLts;Nd7s-G7YBGBUh}sX6=@=kMQaZc# zF;^T@Bp89E7=nr5Q6>b(3sk@G3Dw*FO=EgqV^+H?d(O0DRa#+MTYP{BE&5@iW?g#-FO8S1)Pl(Q>u0yBWgd4 zJ21**EzM1Q@4NRfR~*wczP^Fa?z@I_2@Z{sZ9hWQ+eeWT38zF+#OB*?(4A2=?O;Aoy`#}TUD`V{>vW-`H0 z^9aU-FbtV595a!4s&@AiwI3$z90G5gA{b(Z;+fAJ)8zIIQ}@8z=+HQm35J?SFe-s# znu5Lplc{+WPo;6pfMBS31fxQ z7lUISFf-EBbD_NVTr7@xpmX2Hbng3@xu9I;9CPSr58OwwP~?=4_U>idw(U%XV9bPL z4&8ajW+q!u9kG4eHfED@nN#LKk|dl-k|fMF1w6B?qoac}>FD^7*&@OOQ(#b4nN80> z{|EN&-OFS2a9-OkllucW)Xhd17M zlQd144XR3g{SwUV4QFPrh{&cWs^Y|nJhLT^V_tdX@63fLikM(#FKd#d&jDlEY&ELN zrA}3;uFg_6mL$n@L4SW=Vd>HK&N;mI4|jHc^tw_O$MNH4 z_F5Q*LQ1z(AxEF$2&$IPf? zK@d<~on_IYg*fN%-d-z}O5f?}=_*ntOw!cU>YNL<1VQkSh}2N#y~lfx85KnYR25a_ zq_HGPA5GKbvF`4!gp-6bXl`y@>RfoQ2;UTu>qI0+1v0Y{GkYI+HICzFySuykIL&_n Xk)a{pt!&$i00000NkvXXu0mjfy?Vi> literal 0 HcmV?d00001 diff --git a/public/icons/icon.icns b/public/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..8a97f2aed2d3795269c7cdbc853d4797c835e6c8 GIT binary patch literal 125606 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�_A+OZH<{J< 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 literal 0 HcmV?d00001 diff --git a/public/icons/icon.ico b/public/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..567b77b0617dec64e32bbffd17015e870da2aaf8 GIT binary patch literal 361102 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 { - const parts = v.split('='); - return parts[0] === name ? decodeURIComponent(parts[1]) : r; - }, ""); -} - -function setCookie(name: string, value: string, days = 7) { - const expires = new Date(Date.now() + days * 864e5).toUTCString(); - document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`; -} - -function AppContent() { - const [view, setView] = useState("homepage") - const [mountedViews, setMountedViews] = useState>(new Set(["homepage"])) - const [isAuthenticated, setIsAuthenticated] = useState(false) - const [username, setUsername] = useState(null) - const [isAdmin, setIsAdmin] = useState(false) - const [authLoading, setAuthLoading] = useState(true) - const [isTopbarOpen, setIsTopbarOpen] = useState(true) - const {currentTab, tabs} = useTabs(); - - useEffect(() => { - const checkAuth = () => { - const jwt = getCookie("jwt"); - if (jwt) { - setAuthLoading(true); - getUserInfo() - .then((meRes) => { - setIsAuthenticated(true); - setIsAdmin(!!meRes.is_admin); - setUsername(meRes.username || null); - }) - .catch((err) => { - setIsAuthenticated(false); - setIsAdmin(false); - setUsername(null); - document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - }) - .finally(() => setAuthLoading(false)); - } else { - setIsAuthenticated(false); - setIsAdmin(false); - setUsername(null); - setAuthLoading(false); - } - } - - checkAuth() - - const handleStorageChange = () => checkAuth() - window.addEventListener('storage', handleStorageChange) - - return () => window.removeEventListener('storage', handleStorageChange) - }, []) - - const handleSelectView = (nextView: string) => { - setMountedViews((prev) => { - if (prev.has(nextView)) return prev - const next = new Set(prev) - next.add(nextView) - return next - }) - setView(nextView) - } - - const handleAuthSuccess = (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => { - setIsAuthenticated(true) - setIsAdmin(authData.isAdmin) - setUsername(authData.username) - } - - const currentTabData = tabs.find(tab => tab.id === currentTab); - const showTerminalView = currentTabData?.type === 'terminal' || currentTabData?.type === 'server' || currentTabData?.type === 'file_manager'; - const showHome = currentTabData?.type === 'home'; - const showSshManager = currentTabData?.type === 'ssh_manager'; - const showAdmin = currentTabData?.type === 'admin'; - const showProfile = currentTabData?.type === 'profile'; - - return ( -

- {!isAuthenticated && !authLoading && ( -
-
-
- )} - - {!isAuthenticated && !authLoading && ( -
- -
- )} - - {isAuthenticated && ( - -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- - -
- )} - -
- ) -} - -function App() { - return ( - - - - ); -} - -export default App \ No newline at end of file diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index a59f1ffd..50f169db 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -1,249 +1,295 @@ -import express from 'express'; -import bodyParser from 'body-parser'; -import userRoutes from './routes/users.js'; -import sshRoutes from './routes/ssh.js'; -import alertRoutes from './routes/alerts.js'; -import chalk from 'chalk'; -import cors from 'cors'; -import fetch from 'node-fetch'; -import 'dotenv/config'; +import express from "express"; +import bodyParser from "body-parser"; +import userRoutes from "./routes/users.js"; +import sshRoutes from "./routes/ssh.js"; +import alertRoutes from "./routes/alerts.js"; +import credentialsRoutes from "./routes/credentials.js"; +import cors from "cors"; +import fetch from "node-fetch"; +import fs from "fs"; +import path from "path"; +import "dotenv/config"; +import { databaseLogger, apiLogger } from "../utils/logger.js"; const app = express(); -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] -})); - -const dbIconSymbol = '🗄️'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; +app.use( + cors({ + origin: "*", + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "User-Agent", + "X-Electron-App", + ], + }), +); interface CacheEntry { - data: any; - timestamp: number; - expiresAt: number; + data: any; + timestamp: number; + expiresAt: number; } class GitHubCache { - private cache: Map = new Map(); - private readonly CACHE_DURATION = 30 * 60 * 1000; + private cache: Map = new Map(); + private readonly CACHE_DURATION = 30 * 60 * 1000; - set(key: string, data: any): void { - const now = Date.now(); - this.cache.set(key, { - data, - timestamp: now, - expiresAt: now + this.CACHE_DURATION - }); + set(key: string, data: any): void { + const now = Date.now(); + this.cache.set(key, { + data, + timestamp: now, + expiresAt: now + this.CACHE_DURATION, + }); + } + + get(key: string): any | null { + const entry = this.cache.get(key); + if (!entry) { + return null; } - get(key: string): any | null { - const entry = this.cache.get(key); - if (!entry) { - return null; - } - - if (Date.now() > entry.expiresAt) { - this.cache.delete(key); - return null; - } - - return entry.data; + if (Date.now() > entry.expiresAt) { + this.cache.delete(key); + return null; } + + return entry.data; + } } const githubCache = new GitHubCache(); -const GITHUB_API_BASE = 'https://api.github.com'; -const REPO_OWNER = 'LukeGus'; -const REPO_NAME = 'Termix'; +const GITHUB_API_BASE = "https://api.github.com"; +const REPO_OWNER = "LukeGus"; +const REPO_NAME = "Termix"; interface GitHubRelease { + id: number; + tag_name: string; + name: string; + body: string; + published_at: string; + html_url: string; + assets: Array<{ id: number; - tag_name: string; name: string; - body: string; - published_at: string; - html_url: string; - assets: Array<{ - id: number; - name: string; - size: number; - download_count: number; - browser_download_url: string; - }>; - prerelease: boolean; - draft: boolean; + size: number; + download_count: number; + browser_download_url: string; + }>; + prerelease: boolean; + draft: boolean; } -async function fetchGitHubAPI(endpoint: string, cacheKey: string): Promise { - const cachedData = githubCache.get(cacheKey); - if (cachedData) { - return { - data: cachedData, - cached: true, - cache_age: Date.now() - cachedData.timestamp - }; +async function fetchGitHubAPI( + endpoint: string, + cacheKey: string, +): Promise { + const cachedData = githubCache.get(cacheKey); + if (cachedData) { + return { + data: cachedData, + cached: true, + cache_age: Date.now() - cachedData.timestamp, + }; + } + + try { + const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, { + headers: { + Accept: "application/vnd.github+json", + "User-Agent": "TermixUpdateChecker/1.0", + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + + if (!response.ok) { + throw new Error( + `GitHub API error: ${response.status} ${response.statusText}`, + ); } - try { - const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, { - headers: { - 'Accept': 'application/vnd.github+json', - 'User-Agent': 'TermixUpdateChecker/1.0', - 'X-GitHub-Api-Version': '2022-11-28' - } - }); + const data = await response.json(); + githubCache.set(cacheKey, data); - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - - githubCache.set(cacheKey, data); - - return { - data: data, - cached: false - }; - } catch (error) { - logger.error(`Failed to fetch from GitHub API: ${endpoint}`, error); - throw error; - } + return { + data: data, + cached: false, + }; + } catch (error) { + databaseLogger.error(`Failed to fetch from GitHub API`, error, { + operation: "github_api", + endpoint, + }); + throw error; + } } app.use(bodyParser.json()); -app.get('/health', (req, res) => { - res.json({status: 'ok'}); +app.get("/health", (req, res) => { + res.json({ status: "ok" }); }); -app.get('/version', async (req, res) => { - const localVersion = process.env.VERSION; - - if (!localVersion) { - return res.status(401).send('Local Version Not Set'); - } +app.get("/version", async (req, res) => { + let localVersion = process.env.VERSION; + if (!localVersion) { try { - const cacheKey = 'latest_release'; - const releaseData = await fetchGitHubAPI( - `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, - cacheKey - ); - - const rawTag = releaseData.data.tag_name || releaseData.data.name || ''; - const remoteVersionMatch = rawTag.match(/(\d+\.\d+(\.\d+)?)/); - const remoteVersion = remoteVersionMatch ? remoteVersionMatch[1] : null; - - if (!remoteVersion) { - return res.status(401).send('Remote Version Not Found'); - } - - const response = { - status: localVersion === remoteVersion ? 'up_to_date' : 'requires_update', - version: remoteVersion, - latest_release: { - tag_name: releaseData.data.tag_name, - name: releaseData.data.name, - published_at: releaseData.data.published_at, - html_url: releaseData.data.html_url - }, - cached: releaseData.cached, - cache_age: releaseData.cache_age - }; - - res.json(response); - } catch (err) { - logger.error('Version check failed', err); - res.status(500).send('Fetch Error'); - } -}); - -app.get('/releases/rss', async (req, res) => { - try { - const page = parseInt(req.query.page as string) || 1; - const per_page = Math.min(parseInt(req.query.per_page as string) || 20, 100); - const cacheKey = `releases_rss_${page}_${per_page}`; - - const releasesData = await fetchGitHubAPI( - `/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`, - cacheKey - ); - - const rssItems = releasesData.data.map((release: GitHubRelease) => ({ - id: release.id, - title: release.name || release.tag_name, - description: release.body, - link: release.html_url, - pubDate: release.published_at, - version: release.tag_name, - isPrerelease: release.prerelease, - isDraft: release.draft, - assets: release.assets.map(asset => ({ - name: asset.name, - size: asset.size, - download_count: asset.download_count, - download_url: asset.browser_download_url - })) - })); - - const response = { - feed: { - title: `${REPO_NAME} Releases`, - description: `Latest releases from ${REPO_NAME} repository`, - link: `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases`, - updated: new Date().toISOString() - }, - items: rssItems, - total_count: rssItems.length, - cached: releasesData.cached, - cache_age: releasesData.cache_age - }; - - res.json(response); + const packagePath = path.resolve(process.cwd(), "package.json"); + const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); + localVersion = packageJson.version; } catch (error) { - logger.error('Failed to generate RSS format', error) - res.status(500).json({ - error: 'Failed to generate RSS format', - details: error instanceof Error ? error.message : 'Unknown error' - }); + databaseLogger.error("Failed to read version from package.json", error, { + operation: "version_check", + }); } + } + + if (!localVersion) { + databaseLogger.error("No version information available", undefined, { + operation: "version_check", + }); + return res.status(404).send("Local Version Not Set"); + } + + try { + const cacheKey = "latest_release"; + const releaseData = await fetchGitHubAPI( + `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, + cacheKey, + ); + + const rawTag = releaseData.data.tag_name || releaseData.data.name || ""; + const remoteVersionMatch = rawTag.match(/(\d+\.\d+(\.\d+)?)/); + const remoteVersion = remoteVersionMatch ? remoteVersionMatch[1] : null; + + if (!remoteVersion) { + databaseLogger.warn("Remote version not found in GitHub response", { + operation: "version_check", + rawTag, + }); + return res.status(401).send("Remote Version Not Found"); + } + + const isUpToDate = localVersion === remoteVersion; + + const response = { + status: isUpToDate ? "up_to_date" : "requires_update", + localVersion: localVersion, + version: remoteVersion, + latest_release: { + tag_name: releaseData.data.tag_name, + name: releaseData.data.name, + published_at: releaseData.data.published_at, + html_url: releaseData.data.html_url, + }, + cached: releaseData.cached, + cache_age: releaseData.cache_age, + }; + + res.json(response); + } catch (err) { + databaseLogger.error("Version check failed", err, { + operation: "version_check", + }); + res.status(500).send("Fetch Error"); + } }); -app.use('/users', userRoutes); -app.use('/ssh', sshRoutes); -app.use('/alerts', alertRoutes); +app.get("/releases/rss", async (req, res) => { + try { + const page = parseInt(req.query.page as string) || 1; + const per_page = Math.min( + parseInt(req.query.per_page as string) || 20, + 100, + ); + const cacheKey = `releases_rss_${page}_${per_page}`; -app.use((err: unknown, req: express.Request, res: express.Response, next: express.NextFunction) => { - logger.error('Unhandled error:', err); - res.status(500).json({error: 'Internal Server Error'}); + const releasesData = await fetchGitHubAPI( + `/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`, + cacheKey, + ); + + const rssItems = releasesData.data.map((release: GitHubRelease) => ({ + id: release.id, + title: release.name || release.tag_name, + description: release.body, + link: release.html_url, + pubDate: release.published_at, + version: release.tag_name, + isPrerelease: release.prerelease, + isDraft: release.draft, + assets: release.assets.map((asset) => ({ + name: asset.name, + size: asset.size, + download_count: asset.download_count, + download_url: asset.browser_download_url, + })), + })); + + const response = { + feed: { + title: `${REPO_NAME} Releases`, + description: `Latest releases from ${REPO_NAME} repository`, + link: `https://github.com/${REPO_OWNER}/${REPO_NAME}/releases`, + updated: new Date().toISOString(), + }, + items: rssItems, + total_count: rssItems.length, + cached: releasesData.cached, + cache_age: releasesData.cache_age, + }; + + res.json(response); + } catch (error) { + databaseLogger.error("Failed to generate RSS format", error, { + operation: "rss_releases", + }); + res.status(500).json({ + error: "Failed to generate RSS format", + details: error instanceof Error ? error.message : "Unknown error", + }); + } }); +app.use("/users", userRoutes); +app.use("/ssh", sshRoutes); +app.use("/alerts", alertRoutes); +app.use("/credentials", credentialsRoutes); + +app.use( + ( + err: unknown, + req: express.Request, + res: express.Response, + next: express.NextFunction, + ) => { + apiLogger.error("Unhandled error in request", err, { + operation: "error_handler", + method: req.method, + url: req.url, + userAgent: req.get("User-Agent"), + }); + res.status(500).json({ error: "Internal Server Error" }); + }, +); + const PORT = 8081; app.listen(PORT, () => { -}); \ No newline at end of file + databaseLogger.success(`Database API server started on port ${PORT}`, { + operation: "server_start", + port: PORT, + routes: [ + "/users", + "/ssh", + "/alerts", + "/credentials", + "/health", + "/version", + "/releases/rss", + ], + }); +}); diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 2dd60b79..1dd17218 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -1,454 +1,306 @@ -import {drizzle} from 'drizzle-orm/better-sqlite3'; -import Database from 'better-sqlite3'; -import * as schema from './schema.js'; -import chalk from 'chalk'; -import fs from 'fs'; -import path from 'path'; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import Database from "better-sqlite3"; +import * as schema from "./schema.js"; +import fs from "fs"; +import path from "path"; +import { databaseLogger } from "../../utils/logger.js"; -const dbIconSymbol = '🗄️'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; - -const dataDir = process.env.DATA_DIR || './db/data'; +const dataDir = process.env.DATA_DIR || "./db/data"; const dbDir = path.resolve(dataDir); if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, {recursive: true}); + databaseLogger.info(`Creating database directory`, { + operation: "db_init", + path: dbDir, + }); + fs.mkdirSync(dbDir, { recursive: true }); } -const dbPath = path.join(dataDir, 'db.sqlite'); +const dbPath = path.join(dataDir, "db.sqlite"); +databaseLogger.info(`Initializing SQLite database`, { + operation: "db_init", + path: dbPath, +}); const sqlite = new Database(dbPath); sqlite.exec(` - CREATE TABLE IF NOT EXISTS users - ( - id - TEXT - PRIMARY - KEY, - username - TEXT - NOT - NULL, - password_hash - TEXT - NOT - NULL, - is_admin - INTEGER - NOT - NULL - DEFAULT - 0, - - is_oidc - INTEGER - NOT - NULL - DEFAULT - 0, - client_id - TEXT - NOT - NULL, - client_secret - TEXT - NOT - NULL, - issuer_url - TEXT - NOT - NULL, - authorization_url - TEXT - NOT - NULL, - token_url - TEXT - NOT - NULL, - redirect_uri - TEXT, - identifier_path - TEXT - NOT - NULL, - name_path - TEXT - NOT - NULL, - scopes - TEXT - NOT - NULL + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0, + is_oidc INTEGER NOT NULL DEFAULT 0, + client_id TEXT NOT NULL, + client_secret TEXT NOT NULL, + issuer_url TEXT NOT NULL, + authorization_url TEXT NOT NULL, + token_url TEXT NOT NULL, + redirect_uri TEXT, + identifier_path TEXT NOT NULL, + name_path TEXT NOT NULL, + scopes TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS settings - ( - key - TEXT - PRIMARY - KEY, - value - TEXT - NOT - NULL + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS ssh_data - ( - id - INTEGER - PRIMARY - KEY - AUTOINCREMENT, - user_id - TEXT - NOT - NULL, - name - TEXT, - ip - TEXT - NOT - NULL, - port - INTEGER - NOT - NULL, - username - TEXT - NOT - NULL, - folder - TEXT, - tags - TEXT, - pin - INTEGER - NOT - NULL - DEFAULT - 0, - auth_type - TEXT - NOT - NULL, - password - TEXT, - key - TEXT, - key_password - TEXT, - key_type - TEXT, - enable_terminal - INTEGER - NOT - NULL - DEFAULT - 1, - enable_tunnel - INTEGER - NOT - NULL - DEFAULT - 1, - tunnel_connections - TEXT, - enable_file_manager - INTEGER - NOT - NULL - DEFAULT - 1, - default_path - TEXT, - created_at - TEXT - NOT - NULL - DEFAULT - CURRENT_TIMESTAMP, - updated_at - TEXT - NOT - NULL - DEFAULT - CURRENT_TIMESTAMP, - FOREIGN - KEY - ( - user_id - ) REFERENCES users - ( - id - ) - ); + CREATE TABLE IF NOT EXISTS ssh_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT, + ip TEXT NOT NULL, + port INTEGER NOT NULL, + username TEXT NOT NULL, + folder TEXT, + tags TEXT, + pin INTEGER NOT NULL DEFAULT 0, + auth_type TEXT NOT NULL, + password TEXT, + key TEXT, + key_password TEXT, + key_type TEXT, + enable_terminal INTEGER NOT NULL DEFAULT 1, + enable_tunnel INTEGER NOT NULL DEFAULT 1, + tunnel_connections TEXT, + enable_file_manager INTEGER NOT NULL DEFAULT 1, + default_path TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) + ); - CREATE TABLE IF NOT EXISTS file_manager_recent - ( - id - INTEGER - PRIMARY - KEY - AUTOINCREMENT, - user_id - TEXT - NOT - NULL, - host_id - INTEGER - NOT - NULL, - name - TEXT - NOT - NULL, - path - TEXT - NOT - NULL, - last_opened - TEXT - NOT - NULL - DEFAULT - CURRENT_TIMESTAMP, - FOREIGN - KEY - ( - user_id - ) REFERENCES users - ( - id - ), - FOREIGN KEY - ( - host_id - ) REFERENCES ssh_data - ( - id - ) - ); + CREATE TABLE IF NOT EXISTS file_manager_recent ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + last_opened TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id), + FOREIGN KEY (host_id) REFERENCES ssh_data (id) + ); - CREATE TABLE IF NOT EXISTS file_manager_pinned - ( - id - INTEGER - PRIMARY - KEY - AUTOINCREMENT, - user_id - TEXT - NOT - NULL, - host_id - INTEGER - NOT - NULL, - name - TEXT - NOT - NULL, - path - TEXT - NOT - NULL, - pinned_at - TEXT - NOT - NULL - DEFAULT - CURRENT_TIMESTAMP, - FOREIGN - KEY - ( - user_id - ) REFERENCES users - ( - id - ), - FOREIGN KEY - ( - host_id - ) REFERENCES ssh_data - ( - id - ) - ); + CREATE TABLE IF NOT EXISTS file_manager_pinned ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + pinned_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id), + FOREIGN KEY (host_id) REFERENCES ssh_data (id) + ); - CREATE TABLE IF NOT EXISTS file_manager_shortcuts - ( - id - INTEGER - PRIMARY - KEY - AUTOINCREMENT, - user_id - TEXT - NOT - NULL, - host_id - INTEGER - NOT - NULL, - name - TEXT - NOT - NULL, - path - TEXT - NOT - NULL, - created_at - TEXT - NOT - NULL - DEFAULT - CURRENT_TIMESTAMP, - FOREIGN - KEY - ( - user_id - ) REFERENCES users - ( - id - ), - FOREIGN KEY - ( - host_id - ) REFERENCES ssh_data - ( - id - ) - ); + CREATE TABLE IF NOT EXISTS file_manager_shortcuts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + host_id INTEGER NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id), + FOREIGN KEY (host_id) REFERENCES ssh_data (id) + ); - CREATE TABLE IF NOT EXISTS dismissed_alerts - ( - id - INTEGER - PRIMARY - KEY - AUTOINCREMENT, - user_id - TEXT - NOT - NULL, - alert_id - TEXT - NOT - NULL, - dismissed_at - TEXT - NOT - NULL - DEFAULT - CURRENT_TIMESTAMP, - FOREIGN - KEY - ( - user_id - ) REFERENCES users - ( - id - ) - ); + CREATE TABLE IF NOT EXISTS dismissed_alerts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + alert_id TEXT NOT NULL, + dismissed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) + ); + + CREATE TABLE IF NOT EXISTS ssh_credentials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + folder TEXT, + tags TEXT, + auth_type TEXT NOT NULL, + username TEXT NOT NULL, + password TEXT, + key TEXT, + key_password TEXT, + key_type TEXT, + usage_count INTEGER NOT NULL DEFAULT 0, + last_used TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) + ); + + CREATE TABLE IF NOT EXISTS ssh_credential_usage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + credential_id INTEGER NOT NULL, + host_id INTEGER NOT NULL, + user_id TEXT NOT NULL, + used_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (credential_id) REFERENCES ssh_credentials (id), + FOREIGN KEY (host_id) REFERENCES ssh_data (id), + FOREIGN KEY (user_id) REFERENCES users (id) + ); `); -const addColumnIfNotExists = (table: string, column: string, definition: string) => { +const addColumnIfNotExists = ( + table: string, + column: string, + definition: string, +) => { + try { + sqlite + .prepare( + `SELECT ${column} + FROM ${table} LIMIT 1`, + ) + .get(); + } catch (e) { try { - sqlite.prepare(`SELECT ${column} - FROM ${table} LIMIT 1`).get(); - } catch (e) { - try { - sqlite.exec(`ALTER TABLE ${table} + databaseLogger.debug(`Adding column ${column} to ${table}`, { + operation: "schema_migration", + table, + column, + }); + sqlite.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`); - } catch (alterError) { - logger.warn(`Failed to add column ${column} to ${table}: ${alterError}`); - } + databaseLogger.success(`Column ${column} added to ${table}`, { + operation: "schema_migration", + table, + column, + }); + } catch (alterError) { + databaseLogger.warn(`Failed to add column ${column} to ${table}`, { + operation: "schema_migration", + table, + column, + error: alterError, + }); } + } }; const migrateSchema = () => { - logger.info('Checking for schema updates...'); + databaseLogger.info("Checking for schema updates...", { + operation: "schema_migration", + }); - addColumnIfNotExists('users', 'is_admin', 'INTEGER NOT NULL DEFAULT 0'); + addColumnIfNotExists("users", "is_admin", "INTEGER NOT NULL DEFAULT 0"); - addColumnIfNotExists('users', 'is_oidc', 'INTEGER NOT NULL DEFAULT 0'); - addColumnIfNotExists('users', 'oidc_identifier', 'TEXT'); - addColumnIfNotExists('users', 'client_id', 'TEXT'); - addColumnIfNotExists('users', 'client_secret', 'TEXT'); - addColumnIfNotExists('users', 'issuer_url', 'TEXT'); - addColumnIfNotExists('users', 'authorization_url', 'TEXT'); - addColumnIfNotExists('users', 'token_url', 'TEXT'); - try { - sqlite.prepare(`ALTER TABLE users DROP COLUMN redirect_uri`).run(); - } catch (e) { - } + addColumnIfNotExists("users", "is_oidc", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists("users", "oidc_identifier", "TEXT"); + addColumnIfNotExists("users", "client_id", "TEXT"); + addColumnIfNotExists("users", "client_secret", "TEXT"); + addColumnIfNotExists("users", "issuer_url", "TEXT"); + addColumnIfNotExists("users", "authorization_url", "TEXT"); + addColumnIfNotExists("users", "token_url", "TEXT"); - addColumnIfNotExists('users', 'identifier_path', 'TEXT'); - addColumnIfNotExists('users', 'name_path', 'TEXT'); - addColumnIfNotExists('users', 'scopes', 'TEXT'); + addColumnIfNotExists("users", "identifier_path", "TEXT"); + addColumnIfNotExists("users", "name_path", "TEXT"); + addColumnIfNotExists("users", "scopes", "TEXT"); - addColumnIfNotExists('users', 'totp_secret', 'TEXT'); - addColumnIfNotExists('users', 'totp_enabled', 'INTEGER NOT NULL DEFAULT 0'); - addColumnIfNotExists('users', 'totp_backup_codes', 'TEXT'); + addColumnIfNotExists("users", "totp_secret", "TEXT"); + addColumnIfNotExists("users", "totp_enabled", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists("users", "totp_backup_codes", "TEXT"); - addColumnIfNotExists('ssh_data', 'name', 'TEXT'); - addColumnIfNotExists('ssh_data', 'folder', 'TEXT'); - addColumnIfNotExists('ssh_data', 'tags', 'TEXT'); - addColumnIfNotExists('ssh_data', 'pin', 'INTEGER NOT NULL DEFAULT 0'); - addColumnIfNotExists('ssh_data', 'auth_type', 'TEXT NOT NULL DEFAULT "password"'); - addColumnIfNotExists('ssh_data', 'password', 'TEXT'); - addColumnIfNotExists('ssh_data', 'key', 'TEXT'); - addColumnIfNotExists('ssh_data', 'key_password', 'TEXT'); - addColumnIfNotExists('ssh_data', 'key_type', 'TEXT'); - addColumnIfNotExists('ssh_data', 'enable_terminal', 'INTEGER NOT NULL DEFAULT 1'); - addColumnIfNotExists('ssh_data', 'enable_tunnel', 'INTEGER NOT NULL DEFAULT 1'); - addColumnIfNotExists('ssh_data', 'tunnel_connections', 'TEXT'); - addColumnIfNotExists('ssh_data', 'enable_file_manager', 'INTEGER NOT NULL DEFAULT 1'); - addColumnIfNotExists('ssh_data', 'default_path', 'TEXT'); - addColumnIfNotExists('ssh_data', 'created_at', 'TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP'); - addColumnIfNotExists('ssh_data', 'updated_at', 'TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP'); + addColumnIfNotExists("ssh_data", "name", "TEXT"); + addColumnIfNotExists("ssh_data", "folder", "TEXT"); + addColumnIfNotExists("ssh_data", "tags", "TEXT"); + addColumnIfNotExists("ssh_data", "pin", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists( + "ssh_data", + "auth_type", + 'TEXT NOT NULL DEFAULT "password"', + ); + addColumnIfNotExists("ssh_data", "password", "TEXT"); + addColumnIfNotExists("ssh_data", "key", "TEXT"); + addColumnIfNotExists("ssh_data", "key_password", "TEXT"); + addColumnIfNotExists("ssh_data", "key_type", "TEXT"); + addColumnIfNotExists( + "ssh_data", + "enable_terminal", + "INTEGER NOT NULL DEFAULT 1", + ); + addColumnIfNotExists( + "ssh_data", + "enable_tunnel", + "INTEGER NOT NULL DEFAULT 1", + ); + addColumnIfNotExists("ssh_data", "tunnel_connections", "TEXT"); + addColumnIfNotExists( + "ssh_data", + "enable_file_manager", + "INTEGER NOT NULL DEFAULT 1", + ); + addColumnIfNotExists("ssh_data", "default_path", "TEXT"); + addColumnIfNotExists( + "ssh_data", + "created_at", + "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP", + ); + addColumnIfNotExists( + "ssh_data", + "updated_at", + "TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP", + ); - addColumnIfNotExists('file_manager_recent', 'host_id', 'INTEGER NOT NULL'); - addColumnIfNotExists('file_manager_pinned', 'host_id', 'INTEGER NOT NULL'); - addColumnIfNotExists('file_manager_shortcuts', 'host_id', 'INTEGER NOT NULL'); + addColumnIfNotExists( + "ssh_data", + "credential_id", + "INTEGER REFERENCES ssh_credentials(id)", + ); - logger.success('Schema migration completed'); + addColumnIfNotExists("file_manager_recent", "host_id", "INTEGER NOT NULL"); + addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL"); + addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL"); + + databaseLogger.success("Schema migration completed", { + operation: "schema_migration", + }); }; -migrateSchema(); +const initializeDatabase = async () => { + migrateSchema(); -try { - const row = sqlite.prepare("SELECT value FROM settings WHERE key = 'allow_registration'").get(); + try { + const row = sqlite + .prepare("SELECT value FROM settings WHERE key = 'allow_registration'") + .get(); if (!row) { - sqlite.prepare("INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')").run(); + databaseLogger.info("Initializing default settings", { + operation: "db_init", + setting: "allow_registration", + }); + sqlite + .prepare( + "INSERT INTO settings (key, value) VALUES ('allow_registration', 'true')", + ) + .run(); + databaseLogger.success("Default settings initialized", { + operation: "db_init", + }); + } else { + databaseLogger.debug("Default settings already exist", { + operation: "db_init", + }); } -} catch (e) { - logger.warn('Could not initialize default settings'); -} + } catch (e) { + databaseLogger.warn("Could not initialize default settings", { + operation: "db_init", + error: e, + }); + } +}; -export const db = drizzle(sqlite, {schema}); \ No newline at end of file +initializeDatabase().catch((error) => { + databaseLogger.error("Failed to initialize database", error, { + operation: "db_init", + }); + process.exit(1); +}); + +databaseLogger.success("Database connection established", { + operation: "db_init", + path: dbPath, +}); +export const db = drizzle(sqlite, { schema }); diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 81300eea..9e46d73a 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -1,87 +1,167 @@ -import {sqliteTable, text, integer} from 'drizzle-orm/sqlite-core'; -import {sql} from 'drizzle-orm'; +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; +import { sql } from "drizzle-orm"; -export const users = sqliteTable('users', { - id: text('id').primaryKey(), - username: text('username').notNull(), - password_hash: text('password_hash').notNull(), - is_admin: integer('is_admin', {mode: 'boolean'}).notNull().default(false), +export const users = sqliteTable("users", { + id: text("id").primaryKey(), + username: text("username").notNull(), + password_hash: text("password_hash").notNull(), + is_admin: integer("is_admin", { mode: "boolean" }).notNull().default(false), - is_oidc: integer('is_oidc', {mode: 'boolean'}).notNull().default(false), - oidc_identifier: text('oidc_identifier'), - client_id: text('client_id'), - client_secret: text('client_secret'), - issuer_url: text('issuer_url'), - authorization_url: text('authorization_url'), - token_url: text('token_url'), - identifier_path: text('identifier_path'), - name_path: text('name_path'), - scopes: text().default("openid email profile"), - - totp_secret: text('totp_secret'), - totp_enabled: integer('totp_enabled', {mode: 'boolean'}).notNull().default(false), - totp_backup_codes: text('totp_backup_codes'), + is_oidc: integer("is_oidc", { mode: "boolean" }).notNull().default(false), + oidc_identifier: text("oidc_identifier"), + client_id: text("client_id"), + client_secret: text("client_secret"), + issuer_url: text("issuer_url"), + authorization_url: text("authorization_url"), + token_url: text("token_url"), + identifier_path: text("identifier_path"), + name_path: text("name_path"), + scopes: text().default("openid email profile"), + + totp_secret: text("totp_secret"), + totp_enabled: integer("totp_enabled", { mode: "boolean" }) + .notNull() + .default(false), + totp_backup_codes: text("totp_backup_codes"), }); -export const settings = sqliteTable('settings', { - key: text('key').primaryKey(), - value: text('value').notNull(), +export const settings = sqliteTable("settings", { + key: text("key").primaryKey(), + value: text("value").notNull(), }); -export const sshData = sqliteTable('ssh_data', { - id: integer('id').primaryKey({autoIncrement: true}), - userId: text('user_id').notNull().references(() => users.id), - name: text('name'), - ip: text('ip').notNull(), - port: integer('port').notNull(), - username: text('username').notNull(), - folder: text('folder'), - tags: text('tags'), - pin: integer('pin', {mode: 'boolean'}).notNull().default(false), - authType: text('auth_type').notNull(), - password: text('password'), - key: text('key', {length: 8192}), - keyPassword: text('key_password'), - keyType: text('key_type'), - enableTerminal: integer('enable_terminal', {mode: 'boolean'}).notNull().default(true), - enableTunnel: integer('enable_tunnel', {mode: 'boolean'}).notNull().default(true), - tunnelConnections: text('tunnel_connections'), - enableFileManager: integer('enable_file_manager', {mode: 'boolean'}).notNull().default(true), - defaultPath: text('default_path'), - createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`), - updatedAt: text('updated_at').notNull().default(sql`CURRENT_TIMESTAMP`), +export const sshData = sqliteTable("ssh_data", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id), + name: text("name"), + ip: text("ip").notNull(), + port: integer("port").notNull(), + username: text("username").notNull(), + folder: text("folder"), + tags: text("tags"), + pin: integer("pin", { mode: "boolean" }).notNull().default(false), + authType: text("auth_type").notNull(), + + password: text("password"), + key: text("key", { length: 8192 }), + keyPassword: text("key_password"), + keyType: text("key_type"), + + credentialId: integer("credential_id").references(() => sshCredentials.id), + enableTerminal: integer("enable_terminal", { mode: "boolean" }) + .notNull() + .default(true), + enableTunnel: integer("enable_tunnel", { mode: "boolean" }) + .notNull() + .default(true), + tunnelConnections: text("tunnel_connections"), + enableFileManager: integer("enable_file_manager", { mode: "boolean" }) + .notNull() + .default(true), + defaultPath: text("default_path"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), }); -export const fileManagerRecent = sqliteTable('file_manager_recent', { - id: integer('id').primaryKey({autoIncrement: true}), - userId: text('user_id').notNull().references(() => users.id), - hostId: integer('host_id').notNull().references(() => sshData.id), - name: text('name').notNull(), - path: text('path').notNull(), - lastOpened: text('last_opened').notNull().default(sql`CURRENT_TIMESTAMP`), +export const fileManagerRecent = sqliteTable("file_manager_recent", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id), + hostId: integer("host_id") + .notNull() + .references(() => sshData.id), + name: text("name").notNull(), + path: text("path").notNull(), + lastOpened: text("last_opened") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), }); -export const fileManagerPinned = sqliteTable('file_manager_pinned', { - id: integer('id').primaryKey({autoIncrement: true}), - userId: text('user_id').notNull().references(() => users.id), - hostId: integer('host_id').notNull().references(() => sshData.id), - name: text('name').notNull(), - path: text('path').notNull(), - pinnedAt: text('pinned_at').notNull().default(sql`CURRENT_TIMESTAMP`), +export const fileManagerPinned = sqliteTable("file_manager_pinned", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id), + hostId: integer("host_id") + .notNull() + .references(() => sshData.id), + name: text("name").notNull(), + path: text("path").notNull(), + pinnedAt: text("pinned_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), }); -export const fileManagerShortcuts = sqliteTable('file_manager_shortcuts', { - id: integer('id').primaryKey({autoIncrement: true}), - userId: text('user_id').notNull().references(() => users.id), - hostId: integer('host_id').notNull().references(() => sshData.id), - name: text('name').notNull(), - path: text('path').notNull(), - createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`), +export const fileManagerShortcuts = sqliteTable("file_manager_shortcuts", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id), + hostId: integer("host_id") + .notNull() + .references(() => sshData.id), + name: text("name").notNull(), + path: text("path").notNull(), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), }); -export const dismissedAlerts = sqliteTable('dismissed_alerts', { - id: integer('id').primaryKey({autoIncrement: true}), - userId: text('user_id').notNull().references(() => users.id), - alertId: text('alert_id').notNull(), - dismissedAt: text('dismissed_at').notNull().default(sql`CURRENT_TIMESTAMP`), -}); \ No newline at end of file +export const dismissedAlerts = sqliteTable("dismissed_alerts", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id), + alertId: text("alert_id").notNull(), + dismissedAt: text("dismissed_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const sshCredentials = sqliteTable("ssh_credentials", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id), + name: text("name").notNull(), + description: text("description"), + folder: text("folder"), + tags: text("tags"), + authType: text("auth_type").notNull(), + username: text("username").notNull(), + password: text("password"), + key: text("key", { length: 16384 }), + keyPassword: text("key_password"), + keyType: text("key_type"), + usageCount: integer("usage_count").notNull().default(0), + lastUsed: text("last_used"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + +export const sshCredentialUsage = sqliteTable("ssh_credential_usage", { + id: integer("id").primaryKey({ autoIncrement: true }), + credentialId: integer("credential_id") + .notNull() + .references(() => sshCredentials.id), + hostId: integer("host_id") + .notNull() + .references(() => sshData.id), + userId: text("user_id") + .notNull() + .references(() => users.id), + usedAt: text("used_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index 067be4d7..ddfc44c5 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -1,270 +1,261 @@ -import express from 'express'; -import {db} from '../db/index.js'; -import {dismissedAlerts} from '../db/schema.js'; -import {eq, and} from 'drizzle-orm'; -import chalk from 'chalk'; -import fetch from 'node-fetch'; -import type {Request, Response, NextFunction} from 'express'; - -const dbIconSymbol = '🚨'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#dc2626')(`[${dbIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; +import express from "express"; +import { db } from "../db/index.js"; +import { dismissedAlerts } from "../db/schema.js"; +import { eq, and } from "drizzle-orm"; +import fetch from "node-fetch"; +import { authLogger } from "../../utils/logger.js"; interface CacheEntry { - data: any; - timestamp: number; - expiresAt: number; + data: any; + timestamp: number; + expiresAt: number; } class AlertCache { - private cache: Map = new Map(); - private readonly CACHE_DURATION = 5 * 60 * 1000; + private cache: Map = new Map(); + private readonly CACHE_DURATION = 5 * 60 * 1000; - set(key: string, data: any): void { - const now = Date.now(); - this.cache.set(key, { - data, - timestamp: now, - expiresAt: now + this.CACHE_DURATION - }); + set(key: string, data: any): void { + const now = Date.now(); + this.cache.set(key, { + data, + timestamp: now, + expiresAt: now + this.CACHE_DURATION, + }); + } + + get(key: string): any | null { + const entry = this.cache.get(key); + if (!entry) { + return null; } - get(key: string): any | null { - const entry = this.cache.get(key); - if (!entry) { - return null; - } - - if (Date.now() > entry.expiresAt) { - this.cache.delete(key); - return null; - } - - return entry.data; + if (Date.now() > entry.expiresAt) { + this.cache.delete(key); + return null; } + + return entry.data; + } } const alertCache = new AlertCache(); -const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com'; -const REPO_OWNER = 'LukeGus'; -const REPO_NAME = 'Termix-Docs'; -const ALERTS_FILE = 'main/termix-alerts.json'; +const GITHUB_RAW_BASE = "https://raw.githubusercontent.com"; +const REPO_OWNER = "LukeGus"; +const REPO_NAME = "Termix-Docs"; +const ALERTS_FILE = "main/termix-alerts.json"; interface TermixAlert { - id: string; - title: string; - message: string; - expiresAt: string; - priority?: 'low' | 'medium' | 'high' | 'critical'; - type?: 'info' | 'warning' | 'error' | 'success'; - actionUrl?: string; - actionText?: string; + id: string; + title: string; + message: string; + expiresAt: string; + priority?: "low" | "medium" | "high" | "critical"; + type?: "info" | "warning" | "error" | "success"; + actionUrl?: string; + actionText?: string; } async function fetchAlertsFromGitHub(): Promise { - const cacheKey = 'termix_alerts'; - const cachedData = alertCache.get(cacheKey); - if (cachedData) { - return cachedData; + const cacheKey = "termix_alerts"; + const cachedData = alertCache.get(cacheKey); + if (cachedData) { + return cachedData; + } + try { + const url = `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${ALERTS_FILE}`; + + const response = await fetch(url, { + headers: { + Accept: "application/json", + "User-Agent": "TermixAlertChecker/1.0", + }, + }); + + if (!response.ok) { + authLogger.warn("GitHub API returned error status", { + operation: "alerts_fetch", + status: response.status, + statusText: response.statusText, + }); + throw new Error( + `GitHub raw content error: ${response.status} ${response.statusText}`, + ); } - try { - const url = `${GITHUB_RAW_BASE}/${REPO_OWNER}/${REPO_NAME}/${ALERTS_FILE}`; + const alerts: TermixAlert[] = (await response.json()) as TermixAlert[]; - const response = await fetch(url, { - headers: { - 'Accept': 'application/json', - 'User-Agent': 'TermixAlertChecker/1.0' - } - }); + const now = new Date(); - if (!response.ok) { - throw new Error(`GitHub raw content error: ${response.status} ${response.statusText}`); - } + const validAlerts = alerts.filter((alert) => { + const expiryDate = new Date(alert.expiresAt); + const isValid = expiryDate > now; + return isValid; + }); - const alerts: TermixAlert[] = await response.json() as TermixAlert[]; - - const now = new Date(); - - const validAlerts = alerts.filter(alert => { - const expiryDate = new Date(alert.expiresAt); - const isValid = expiryDate > now; - return isValid; - }); - - alertCache.set(cacheKey, validAlerts); - return validAlerts; - } catch (error) { - logger.error('Failed to fetch alerts from GitHub', error); - return []; - } + alertCache.set(cacheKey, validAlerts); + return validAlerts; + } catch (error) { + authLogger.error("Failed to fetch alerts from GitHub", { + operation: "alerts_fetch", + error: error instanceof Error ? error.message : "Unknown error", + }); + return []; + } } const router = express.Router(); // Route: Get all active alerts // GET /alerts -router.get('/', async (req, res) => { - try { - const alerts = await fetchAlertsFromGitHub(); - res.json({ - alerts, - cached: alertCache.get('termix_alerts') !== null, - total_count: alerts.length - }); - } catch (error) { - logger.error('Failed to get alerts', error); - res.status(500).json({error: 'Failed to fetch alerts'}); - } +router.get("/", async (req, res) => { + try { + const alerts = await fetchAlertsFromGitHub(); + res.json({ + alerts, + cached: alertCache.get("termix_alerts") !== null, + total_count: alerts.length, + }); + } catch (error) { + authLogger.error("Failed to get alerts", error); + res.status(500).json({ error: "Failed to fetch alerts" }); + } }); // Route: Get alerts for a specific user (excluding dismissed ones) // GET /alerts/user/:userId -router.get('/user/:userId', async (req, res) => { - try { - const {userId} = req.params; +router.get("/user/:userId", async (req, res) => { + try { + const { userId } = req.params; - if (!userId) { - return res.status(400).json({error: 'User ID is required'}); - } - - const allAlerts = await fetchAlertsFromGitHub(); - - const dismissedAlertRecords = await db - .select({alertId: dismissedAlerts.alertId}) - .from(dismissedAlerts) - .where(eq(dismissedAlerts.userId, userId)); - - const dismissedAlertIds = new Set(dismissedAlertRecords.map(record => record.alertId)); - - const userAlerts = allAlerts.filter(alert => !dismissedAlertIds.has(alert.id)); - - res.json({ - alerts: userAlerts, - total_count: userAlerts.length, - dismissed_count: dismissedAlertIds.size - }); - } catch (error) { - logger.error('Failed to get user alerts', error); - res.status(500).json({error: 'Failed to fetch user alerts'}); + if (!userId) { + return res.status(400).json({ error: "User ID is required" }); } + + const allAlerts = await fetchAlertsFromGitHub(); + + const dismissedAlertRecords = await db + .select({ alertId: dismissedAlerts.alertId }) + .from(dismissedAlerts) + .where(eq(dismissedAlerts.userId, userId)); + + const dismissedAlertIds = new Set( + dismissedAlertRecords.map((record) => record.alertId), + ); + + const userAlerts = allAlerts.filter( + (alert) => !dismissedAlertIds.has(alert.id), + ); + + res.json({ + alerts: userAlerts, + total_count: userAlerts.length, + dismissed_count: dismissedAlertIds.size, + }); + } catch (error) { + authLogger.error("Failed to get user alerts", error); + res.status(500).json({ error: "Failed to fetch user alerts" }); + } }); // Route: Dismiss an alert for a user // POST /alerts/dismiss -router.post('/dismiss', async (req, res) => { - try { - const {userId, alertId} = req.body; +router.post("/dismiss", async (req, res) => { + try { + const { userId, alertId } = req.body; - if (!userId || !alertId) { - logger.warn('Missing userId or alertId in dismiss request'); - return res.status(400).json({error: 'User ID and Alert ID are required'}); - } - - const existingDismissal = await db - .select() - .from(dismissedAlerts) - .where(and( - eq(dismissedAlerts.userId, userId), - eq(dismissedAlerts.alertId, alertId) - )); - - if (existingDismissal.length > 0) { - logger.warn(`Alert ${alertId} already dismissed by user ${userId}`); - return res.status(409).json({error: 'Alert already dismissed'}); - } - - const result = await db.insert(dismissedAlerts).values({ - userId, - alertId - }); - - logger.success(`Alert ${alertId} dismissed by user ${userId}. Insert result: ${JSON.stringify(result)}`); - res.json({message: 'Alert dismissed successfully'}); - } catch (error) { - logger.error('Failed to dismiss alert', error); - res.status(500).json({error: 'Failed to dismiss alert'}); + if (!userId || !alertId) { + authLogger.warn("Missing userId or alertId in dismiss request"); + return res + .status(400) + .json({ error: "User ID and Alert ID are required" }); } + + const existingDismissal = await db + .select() + .from(dismissedAlerts) + .where( + and( + eq(dismissedAlerts.userId, userId), + eq(dismissedAlerts.alertId, alertId), + ), + ); + + if (existingDismissal.length > 0) { + authLogger.warn(`Alert ${alertId} already dismissed by user ${userId}`); + return res.status(409).json({ error: "Alert already dismissed" }); + } + + const result = await db.insert(dismissedAlerts).values({ + userId, + alertId, + }); + + res.json({ message: "Alert dismissed successfully" }); + } catch (error) { + authLogger.error("Failed to dismiss alert", error); + res.status(500).json({ error: "Failed to dismiss alert" }); + } }); // Route: Get dismissed alerts for a user // GET /alerts/dismissed/:userId -router.get('/dismissed/:userId', async (req, res) => { - try { - const {userId} = req.params; +router.get("/dismissed/:userId", async (req, res) => { + try { + const { userId } = req.params; - if (!userId) { - return res.status(400).json({error: 'User ID is required'}); - } - - const dismissedAlertRecords = await db - .select({ - alertId: dismissedAlerts.alertId, - dismissedAt: dismissedAlerts.dismissedAt - }) - .from(dismissedAlerts) - .where(eq(dismissedAlerts.userId, userId)); - - res.json({ - dismissed_alerts: dismissedAlertRecords, - total_count: dismissedAlertRecords.length - }); - } catch (error) { - logger.error('Failed to get dismissed alerts', error); - res.status(500).json({error: 'Failed to fetch dismissed alerts'}); + if (!userId) { + return res.status(400).json({ error: "User ID is required" }); } + + const dismissedAlertRecords = await db + .select({ + alertId: dismissedAlerts.alertId, + dismissedAt: dismissedAlerts.dismissedAt, + }) + .from(dismissedAlerts) + .where(eq(dismissedAlerts.userId, userId)); + + res.json({ + dismissed_alerts: dismissedAlertRecords, + total_count: dismissedAlertRecords.length, + }); + } catch (error) { + authLogger.error("Failed to get dismissed alerts", error); + res.status(500).json({ error: "Failed to fetch dismissed alerts" }); + } }); // Route: Undismiss an alert for a user (remove from dismissed list) // DELETE /alerts/dismiss -router.delete('/dismiss', async (req, res) => { - try { - const {userId, alertId} = req.body; +router.delete("/dismiss", async (req, res) => { + try { + const { userId, alertId } = req.body; - if (!userId || !alertId) { - return res.status(400).json({error: 'User ID and Alert ID are required'}); - } - - const result = await db - .delete(dismissedAlerts) - .where(and( - eq(dismissedAlerts.userId, userId), - eq(dismissedAlerts.alertId, alertId) - )); - - if (result.changes === 0) { - return res.status(404).json({error: 'Dismissed alert not found'}); - } - - logger.success(`Alert ${alertId} undismissed by user ${userId}`); - res.json({message: 'Alert undismissed successfully'}); - } catch (error) { - logger.error('Failed to undismiss alert', error); - res.status(500).json({error: 'Failed to undismiss alert'}); + if (!userId || !alertId) { + return res + .status(400) + .json({ error: "User ID and Alert ID are required" }); } + + const result = await db + .delete(dismissedAlerts) + .where( + and( + eq(dismissedAlerts.userId, userId), + eq(dismissedAlerts.alertId, alertId), + ), + ); + + if (result.changes === 0) { + return res.status(404).json({ error: "Dismissed alert not found" }); + } + res.json({ message: "Alert undismissed successfully" }); + } catch (error) { + authLogger.error("Failed to undismiss alert", error); + res.status(500).json({ error: "Failed to undismiss alert" }); + } }); export default router; diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts new file mode 100644 index 00000000..b6dbb62c --- /dev/null +++ b/src/backend/database/routes/credentials.ts @@ -0,0 +1,664 @@ +import express from "express"; +import { db } from "../db/index.js"; +import { sshCredentials, sshCredentialUsage, sshData } from "../db/schema.js"; +import { eq, and, desc, sql } from "drizzle-orm"; +import type { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import { authLogger } from "../../utils/logger.js"; + +const router = express.Router(); + +interface JWTPayload { + userId: string; + iat?: number; + exp?: number; +} + +function isNonEmptyString(val: any): val is string { + return typeof val === "string" && val.trim().length > 0; +} + +function authenticateJWT(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers["authorization"]; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + authLogger.warn("Missing or invalid Authorization header"); + return res + .status(401) + .json({ error: "Missing or invalid Authorization header" }); + } + const token = authHeader.split(" ")[1]; + const jwtSecret = process.env.JWT_SECRET || "secret"; + try { + const payload = jwt.verify(token, jwtSecret) as JWTPayload; + (req as any).userId = payload.userId; + next(); + } catch (err) { + authLogger.warn("Invalid or expired token"); + return res.status(401).json({ error: "Invalid or expired token" }); + } +} + +// Create a new credential +// POST /credentials +router.post("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { + name, + description, + folder, + tags, + authType, + username, + password, + key, + keyPassword, + keyType, + } = req.body; + + if ( + !isNonEmptyString(userId) || + !isNonEmptyString(name) || + !isNonEmptyString(username) + ) { + authLogger.warn("Invalid credential creation data validation failed", { + operation: "credential_create", + userId, + hasName: !!name, + hasUsername: !!username, + }); + return res.status(400).json({ error: "Name and username are required" }); + } + + if (!["password", "key"].includes(authType)) { + authLogger.warn("Invalid auth type provided", { + operation: "credential_create", + userId, + name, + authType, + }); + return res + .status(400) + .json({ error: 'Auth type must be "password" or "key"' }); + } + + try { + if (authType === "password" && !password) { + authLogger.warn("Password required for password authentication", { + operation: "credential_create", + userId, + name, + authType, + }); + return res + .status(400) + .json({ error: "Password is required for password authentication" }); + } + if (authType === "key" && !key) { + authLogger.warn("SSH key required for key authentication", { + operation: "credential_create", + userId, + name, + authType, + }); + return res + .status(400) + .json({ error: "SSH key is required for key authentication" }); + } + const plainPassword = authType === "password" && password ? password : null; + const plainKey = authType === "key" && key ? key : null; + const plainKeyPassword = + authType === "key" && keyPassword ? keyPassword : null; + + const credentialData = { + userId, + name: name.trim(), + description: description?.trim() || null, + folder: folder?.trim() || null, + tags: Array.isArray(tags) ? tags.join(",") : tags || "", + authType, + username: username.trim(), + password: plainPassword, + key: plainKey, + keyPassword: plainKeyPassword, + keyType: keyType || null, + usageCount: 0, + lastUsed: null, + }; + + const result = await db + .insert(sshCredentials) + .values(credentialData) + .returning(); + const created = result[0]; + + authLogger.success( + `SSH credential created: ${name} (${authType}) by user ${userId}`, + { + operation: "credential_create_success", + userId, + credentialId: created.id, + name, + authType, + username, + }, + ); + + res.status(201).json(formatCredentialOutput(created)); + } catch (err) { + authLogger.error("Failed to create credential in database", err, { + operation: "credential_create", + userId, + name, + authType, + username, + }); + res.status(500).json({ + error: err instanceof Error ? err.message : "Failed to create credential", + }); + } +}); + +// Get all credentials for the authenticated user +// GET /credentials +router.get("/", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + + if (!isNonEmptyString(userId)) { + authLogger.warn("Invalid userId for credential fetch"); + return res.status(400).json({ error: "Invalid userId" }); + } + + try { + const credentials = await db + .select() + .from(sshCredentials) + .where(eq(sshCredentials.userId, userId)) + .orderBy(desc(sshCredentials.updatedAt)); + + res.json(credentials.map((cred) => formatCredentialOutput(cred))); + } catch (err) { + authLogger.error("Failed to fetch credentials", err); + res.status(500).json({ error: "Failed to fetch credentials" }); + } +}); + +// Get all unique credential folders for the authenticated user +// GET /credentials/folders +router.get("/folders", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + + if (!isNonEmptyString(userId)) { + authLogger.warn("Invalid userId for credential folder fetch"); + return res.status(400).json({ error: "Invalid userId" }); + } + + try { + const result = await db + .select({ folder: sshCredentials.folder }) + .from(sshCredentials) + .where(eq(sshCredentials.userId, userId)); + + const folderCounts: Record = {}; + result.forEach((r) => { + if (r.folder && r.folder.trim() !== "") { + folderCounts[r.folder] = (folderCounts[r.folder] || 0) + 1; + } + }); + + const folders = Object.keys(folderCounts).filter( + (folder) => folderCounts[folder] > 0, + ); + res.json(folders); + } catch (err) { + authLogger.error("Failed to fetch credential folders", err); + res.status(500).json({ error: "Failed to fetch credential folders" }); + } +}); + +// Get a specific credential by ID (with plain text secrets) +// GET /credentials/:id +router.get("/:id", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { id } = req.params; + + if (!isNonEmptyString(userId) || !id) { + authLogger.warn("Invalid request for credential fetch"); + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, parseInt(id)), + eq(sshCredentials.userId, userId), + ), + ); + + if (credentials.length === 0) { + return res.status(404).json({ error: "Credential not found" }); + } + + const credential = credentials[0]; + const output = formatCredentialOutput(credential); + + if (credential.password) { + (output as any).password = credential.password; + } + if (credential.key) { + (output as any).key = credential.key; + } + if (credential.keyPassword) { + (output as any).keyPassword = credential.keyPassword; + } + + res.json(output); + } catch (err) { + authLogger.error("Failed to fetch credential", err); + res.status(500).json({ + error: err instanceof Error ? err.message : "Failed to fetch credential", + }); + } +}); + +// Update a credential +// PUT /credentials/:id +router.put("/:id", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { id } = req.params; + const updateData = req.body; + + if (!isNonEmptyString(userId) || !id) { + authLogger.warn("Invalid request for credential update"); + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const existing = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, parseInt(id)), + eq(sshCredentials.userId, userId), + ), + ); + + if (existing.length === 0) { + return res.status(404).json({ error: "Credential not found" }); + } + + const updateFields: any = {}; + + if (updateData.name !== undefined) + updateFields.name = updateData.name.trim(); + if (updateData.description !== undefined) + updateFields.description = updateData.description?.trim() || null; + if (updateData.folder !== undefined) + updateFields.folder = updateData.folder?.trim() || null; + if (updateData.tags !== undefined) { + updateFields.tags = Array.isArray(updateData.tags) + ? updateData.tags.join(",") + : updateData.tags || ""; + } + if (updateData.username !== undefined) + updateFields.username = updateData.username.trim(); + if (updateData.authType !== undefined) + updateFields.authType = updateData.authType; + if (updateData.keyType !== undefined) + updateFields.keyType = updateData.keyType; + + if (updateData.password !== undefined) { + updateFields.password = updateData.password || null; + } + if (updateData.key !== undefined) { + updateFields.key = updateData.key || null; + } + if (updateData.keyPassword !== undefined) { + updateFields.keyPassword = updateData.keyPassword || null; + } + + if (Object.keys(updateFields).length === 0) { + const existing = await db + .select() + .from(sshCredentials) + .where(eq(sshCredentials.id, parseInt(id))); + + return res.json(formatCredentialOutput(existing[0])); + } + + await db + .update(sshCredentials) + .set(updateFields) + .where( + and( + eq(sshCredentials.id, parseInt(id)), + eq(sshCredentials.userId, userId), + ), + ); + + const updated = await db + .select() + .from(sshCredentials) + .where(eq(sshCredentials.id, parseInt(id))); + + const credential = updated[0]; + authLogger.success( + `SSH credential updated: ${credential.name} (${credential.authType}) by user ${userId}`, + { + operation: "credential_update_success", + userId, + credentialId: parseInt(id), + name: credential.name, + authType: credential.authType, + username: credential.username, + }, + ); + + res.json(formatCredentialOutput(updated[0])); + } catch (err) { + authLogger.error("Failed to update credential", err); + res.status(500).json({ + error: err instanceof Error ? err.message : "Failed to update credential", + }); + } +}); + +// Delete a credential +// DELETE /credentials/:id +router.delete("/:id", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { id } = req.params; + + if (!isNonEmptyString(userId) || !id) { + authLogger.warn("Invalid request for credential deletion"); + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const credentialToDelete = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, parseInt(id)), + eq(sshCredentials.userId, userId), + ), + ); + + if (credentialToDelete.length === 0) { + return res.status(404).json({ error: "Credential not found" }); + } + + const hostsUsingCredential = await db + .select() + .from(sshData) + .where( + and(eq(sshData.credentialId, parseInt(id)), eq(sshData.userId, userId)), + ); + + if (hostsUsingCredential.length > 0) { + await db + .update(sshData) + .set({ + credentialId: null, + password: null, + key: null, + keyPassword: null, + authType: "password", + }) + .where( + and( + eq(sshData.credentialId, parseInt(id)), + eq(sshData.userId, userId), + ), + ); + } + + await db + .delete(sshCredentialUsage) + .where( + and( + eq(sshCredentialUsage.credentialId, parseInt(id)), + eq(sshCredentialUsage.userId, userId), + ), + ); + + await db + .delete(sshCredentials) + .where( + and( + eq(sshCredentials.id, parseInt(id)), + eq(sshCredentials.userId, userId), + ), + ); + + const credential = credentialToDelete[0]; + authLogger.success( + `SSH credential deleted: ${credential.name} (${credential.authType}) by user ${userId}`, + { + operation: "credential_delete_success", + userId, + credentialId: parseInt(id), + name: credential.name, + authType: credential.authType, + username: credential.username, + }, + ); + + res.json({ message: "Credential deleted successfully" }); + } catch (err) { + authLogger.error("Failed to delete credential", err); + res.status(500).json({ + error: err instanceof Error ? err.message : "Failed to delete credential", + }); + } +}); + +// Apply a credential to an SSH host (for quick application) +// POST /credentials/:id/apply-to-host/:hostId +router.post( + "/:id/apply-to-host/:hostId", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { id: credentialId, hostId } = req.params; + + if (!isNonEmptyString(userId) || !credentialId || !hostId) { + authLogger.warn("Invalid request for credential application"); + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, parseInt(credentialId)), + eq(sshCredentials.userId, userId), + ), + ); + + if (credentials.length === 0) { + return res.status(404).json({ error: "Credential not found" }); + } + + const credential = credentials[0]; + + await db + .update(sshData) + .set({ + credentialId: parseInt(credentialId), + username: credential.username, + authType: credential.authType, + password: null, + key: null, + keyPassword: null, + keyType: null, + updatedAt: new Date().toISOString(), + }) + .where( + and(eq(sshData.id, parseInt(hostId)), eq(sshData.userId, userId)), + ); + + await db.insert(sshCredentialUsage).values({ + credentialId: parseInt(credentialId), + hostId: parseInt(hostId), + userId, + }); + + await db + .update(sshCredentials) + .set({ + usageCount: sql`${sshCredentials.usageCount} + + 1`, + lastUsed: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }) + .where(eq(sshCredentials.id, parseInt(credentialId))); + res.json({ message: "Credential applied to host successfully" }); + } catch (err) { + authLogger.error("Failed to apply credential to host", err); + res.status(500).json({ + error: + err instanceof Error + ? err.message + : "Failed to apply credential to host", + }); + } + }, +); + +// Get hosts using a specific credential +// GET /credentials/:id/hosts +router.get( + "/:id/hosts", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { id: credentialId } = req.params; + + if (!isNonEmptyString(userId) || !credentialId) { + authLogger.warn("Invalid request for credential hosts fetch"); + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const hosts = await db + .select() + .from(sshData) + .where( + and( + eq(sshData.credentialId, parseInt(credentialId)), + eq(sshData.userId, userId), + ), + ); + + res.json(hosts.map((host) => formatSSHHostOutput(host))); + } catch (err) { + authLogger.error("Failed to fetch hosts using credential", err); + res.status(500).json({ + error: + err instanceof Error + ? err.message + : "Failed to fetch hosts using credential", + }); + } + }, +); + +function formatCredentialOutput(credential: any): any { + return { + id: credential.id, + name: credential.name, + description: credential.description, + folder: credential.folder, + tags: + typeof credential.tags === "string" + ? credential.tags + ? credential.tags.split(",").filter(Boolean) + : [] + : [], + authType: credential.authType, + username: credential.username, + keyType: credential.keyType, + usageCount: credential.usageCount || 0, + lastUsed: credential.lastUsed, + createdAt: credential.createdAt, + updatedAt: credential.updatedAt, + }; +} + +function formatSSHHostOutput(host: any): any { + return { + id: host.id, + userId: host.userId, + name: host.name, + ip: host.ip, + port: host.port, + username: host.username, + folder: host.folder, + tags: + typeof host.tags === "string" + ? host.tags + ? host.tags.split(",").filter(Boolean) + : [] + : [], + pin: !!host.pin, + authType: host.authType, + enableTerminal: !!host.enableTerminal, + enableTunnel: !!host.enableTunnel, + tunnelConnections: host.tunnelConnections + ? JSON.parse(host.tunnelConnections) + : [], + enableFileManager: !!host.enableFileManager, + defaultPath: host.defaultPath, + createdAt: host.createdAt, + updatedAt: host.updatedAt, + }; +} + +// Rename a credential folder +// PUT /credentials/folders/rename +router.put( + "/folders/rename", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { oldName, newName } = req.body; + + if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) { + return res + .status(400) + .json({ error: "Both oldName and newName are required" }); + } + + if (oldName === newName) { + return res + .status(400) + .json({ error: "Old name and new name cannot be the same" }); + } + + try { + await db + .update(sshCredentials) + .set({ folder: newName }) + .where( + and( + eq(sshCredentials.userId, userId), + eq(sshCredentials.folder, oldName), + ), + ); + + res.json({ success: true, message: "Folder renamed successfully" }); + } catch (error) { + authLogger.error("Error renaming credential folder:", error); + res.status(500).json({ error: "Failed to rename folder" }); + } + }, +); + +export default router; diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index e9ad54b8..b08d39dd 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -1,806 +1,1243 @@ -import express from 'express'; -import {db} from '../db/index.js'; -import {sshData, fileManagerRecent, fileManagerPinned, fileManagerShortcuts} from '../db/schema.js'; -import {eq, and, desc} from 'drizzle-orm'; -import chalk from 'chalk'; -import jwt from 'jsonwebtoken'; -import multer from 'multer'; -import type {Request, Response, NextFunction} from 'express'; - -const dbIconSymbol = '🗄️'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; +import express from "express"; +import { db } from "../db/index.js"; +import { + sshData, + sshCredentials, + sshCredentialUsage, + fileManagerRecent, + fileManagerPinned, + fileManagerShortcuts, +} from "../db/schema.js"; +import { eq, and, desc } from "drizzle-orm"; +import type { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import multer from "multer"; +import { sshLogger } from "../../utils/logger.js"; const router = express.Router(); -function isNonEmptyString(val: any): val is string { - return typeof val === 'string' && val.trim().length > 0; -} - -function isValidPort(val: any): val is number { - return typeof val === 'number' && val > 0 && val < 65536; -} +const upload = multer({ storage: multer.memoryStorage() }); interface JWTPayload { - userId: string; - iat?: number; - exp?: number; + userId: string; } -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 10 * 1024 * 1024, - }, - fileFilter: (req, file, cb) => { - if (file.fieldname === 'key') { - cb(null, true); - } else { - cb(new Error('Invalid file type')); - } - } -}); +function isNonEmptyString(value: any): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +function isValidPort(port: any): port is number { + return typeof port === "number" && port > 0 && port <= 65535; +} function authenticateJWT(req: Request, res: Response, next: NextFunction) { - const authHeader = req.headers['authorization']; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - logger.warn('Missing or invalid Authorization header'); - return res.status(401).json({error: 'Missing or invalid Authorization header'}); - } - const token = authHeader.split(' ')[1]; - const jwtSecret = process.env.JWT_SECRET || 'secret'; - try { - const payload = jwt.verify(token, jwtSecret) as JWTPayload; - (req as any).userId = payload.userId; - next(); - } catch (err) { - logger.warn('Invalid or expired token'); - return res.status(401).json({error: 'Invalid or expired token'}); - } + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + sshLogger.warn("Missing or invalid Authorization header"); + return res + .status(401) + .json({ error: "Missing or invalid Authorization header" }); + } + const token = authHeader.split(" ")[1]; + const jwtSecret = process.env.JWT_SECRET || "secret"; + try { + const payload = jwt.verify(token, jwtSecret) as JWTPayload; + (req as any).userId = payload.userId; + next(); + } catch (err) { + sshLogger.warn("Invalid or expired token"); + return res.status(401).json({ error: "Invalid or expired token" }); + } } function isLocalhost(req: Request) { - const ip = req.ip || req.connection?.remoteAddress; - return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1'; + const ip = req.ip || req.connection?.remoteAddress; + return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1"; } // Internal-only endpoint for autostart (no JWT) -router.get('/db/host/internal', async (req: Request, res: Response) => { - if (!isLocalhost(req) && req.headers['x-internal-request'] !== '1') { - logger.warn('Unauthorized attempt to access internal SSH host endpoint'); - return res.status(403).json({error: 'Forbidden'}); - } - try { - const data = await db.select().from(sshData); - const result = data.map((row: any) => ({ - ...row, - tags: typeof row.tags === 'string' ? (row.tags ? row.tags.split(',').filter(Boolean) : []) : [], - pin: !!row.pin, - enableTerminal: !!row.enableTerminal, - enableTunnel: !!row.enableTunnel, - tunnelConnections: row.tunnelConnections ? JSON.parse(row.tunnelConnections) : [], - enableFileManager: !!row.enableFileManager, - })); - res.json(result); - } catch (err) { - logger.error('Failed to fetch SSH data (internal)', err); - res.status(500).json({error: 'Failed to fetch SSH data'}); - } +router.get("/db/host/internal", async (req: Request, res: Response) => { + if (!isLocalhost(req) && req.headers["x-internal-request"] !== "1") { + sshLogger.warn("Unauthorized attempt to access internal SSH host endpoint"); + return res.status(403).json({ error: "Forbidden" }); + } + try { + const data = await db.select().from(sshData); + const result = data.map((row: any) => { + return { + ...row, + tags: + typeof row.tags === "string" + ? row.tags + ? row.tags.split(",").filter(Boolean) + : [] + : [], + pin: !!row.pin, + enableTerminal: !!row.enableTerminal, + enableTunnel: !!row.enableTunnel, + tunnelConnections: row.tunnelConnections + ? JSON.parse(row.tunnelConnections) + : [], + enableFileManager: !!row.enableFileManager, + }; + }); + res.json(result); + } catch (err) { + sshLogger.error("Failed to fetch SSH data (internal)", err); + res.status(500).json({ error: "Failed to fetch SSH data" }); + } }); // Route: Create SSH data (requires JWT) // POST /ssh/host -router.post('/db/host', authenticateJWT, upload.single('key'), async (req: Request, res: Response) => { +router.post( + "/db/host", + authenticateJWT, + upload.single("key"), + async (req: Request, res: Response) => { + const userId = (req as any).userId; let hostData: any; - if (req.headers['content-type']?.includes('multipart/form-data')) { - if (req.body.data) { - try { - hostData = JSON.parse(req.body.data); - } catch (err) { - logger.warn('Invalid JSON data in multipart request'); - return res.status(400).json({error: 'Invalid JSON data'}); - } - } else { - logger.warn('Missing data field in multipart request'); - return res.status(400).json({error: 'Missing data field'}); + if (req.headers["content-type"]?.includes("multipart/form-data")) { + if (req.body.data) { + try { + hostData = JSON.parse(req.body.data); + } catch (err) { + sshLogger.warn("Invalid JSON data in multipart request", { + operation: "host_create", + userId, + error: err, + }); + return res.status(400).json({ error: "Invalid JSON data" }); } + } else { + sshLogger.warn("Missing data field in multipart request", { + operation: "host_create", + userId, + }); + return res.status(400).json({ error: "Missing data field" }); + } - if (req.file) { - hostData.key = req.file.buffer.toString('utf8'); - } + if (req.file) { + hostData.key = req.file.buffer.toString("utf8"); + } } else { - hostData = req.body; + hostData = req.body; } const { - name, - folder, - tags, - ip, - port, - username, - password, - authMethod, - key, - keyPassword, - keyType, - pin, - enableTerminal, - enableTunnel, - enableFileManager, - defaultPath, - tunnelConnections + name, + folder, + tags, + ip, + port, + username, + password, + authMethod, + authType, + credentialId, + key, + keyPassword, + keyType, + pin, + enableTerminal, + enableTunnel, + enableFileManager, + defaultPath, + tunnelConnections, } = hostData; - const userId = (req as any).userId; - if (!isNonEmptyString(userId) || !isNonEmptyString(ip) || !isValidPort(port)) { - logger.warn('Invalid SSH data input'); - return res.status(400).json({error: 'Invalid SSH data'}); + if ( + !isNonEmptyString(userId) || + !isNonEmptyString(ip) || + !isValidPort(port) + ) { + sshLogger.warn("Invalid SSH data input validation failed", { + operation: "host_create", + userId, + hasIp: !!ip, + port, + isValidPort: isValidPort(port), + }); + return res.status(400).json({ error: "Invalid SSH data" }); } + const effectiveAuthType = authType || authMethod; const sshDataObj: any = { - userId: userId, - name, - folder, - tags: Array.isArray(tags) ? tags.join(',') : (tags || ''), - ip, - port, - username, - authType: authMethod, - pin: !!pin ? 1 : 0, - enableTerminal: !!enableTerminal ? 1 : 0, - enableTunnel: !!enableTunnel ? 1 : 0, - tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) : null, - enableFileManager: !!enableFileManager ? 1 : 0, - defaultPath: defaultPath || null, + userId: userId, + name, + folder: folder || null, + tags: Array.isArray(tags) ? tags.join(",") : tags || "", + ip, + port, + username, + authType: effectiveAuthType, + credentialId: credentialId || null, + pin: pin ? 1 : 0, + enableTerminal: enableTerminal ? 1 : 0, + enableTunnel: enableTunnel ? 1 : 0, + tunnelConnections: Array.isArray(tunnelConnections) + ? JSON.stringify(tunnelConnections) + : null, + enableFileManager: enableFileManager ? 1 : 0, + defaultPath: defaultPath || null, }; - if (authMethod === 'password') { - sshDataObj.password = password; - sshDataObj.key = null; - sshDataObj.keyPassword = null; - sshDataObj.keyType = null; - } else if (authMethod === 'key') { - sshDataObj.key = key; - sshDataObj.keyPassword = keyPassword; - sshDataObj.keyType = keyType; - sshDataObj.password = null; + if (effectiveAuthType === "password") { + sshDataObj.password = password || null; + sshDataObj.key = null; + sshDataObj.keyPassword = null; + sshDataObj.keyType = null; + } else if (effectiveAuthType === "key") { + sshDataObj.key = key || null; + sshDataObj.keyPassword = keyPassword || null; + sshDataObj.keyType = keyType; + sshDataObj.password = null; } try { - await db.insert(sshData).values(sshDataObj); - res.json({message: 'SSH data created'}); + const result = await db.insert(sshData).values(sshDataObj).returning(); + + if (result.length === 0) { + sshLogger.warn("No host returned after creation", { + operation: "host_create", + userId, + name, + ip, + port, + }); + return res.status(500).json({ error: "Failed to create host" }); + } + + const createdHost = result[0]; + const baseHost = { + ...createdHost, + tags: + typeof createdHost.tags === "string" + ? createdHost.tags + ? createdHost.tags.split(",").filter(Boolean) + : [] + : [], + pin: !!createdHost.pin, + enableTerminal: !!createdHost.enableTerminal, + enableTunnel: !!createdHost.enableTunnel, + tunnelConnections: createdHost.tunnelConnections + ? JSON.parse(createdHost.tunnelConnections) + : [], + enableFileManager: !!createdHost.enableFileManager, + }; + + const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; + + sshLogger.success( + `SSH host created: ${name} (${ip}:${port}) by user ${userId}`, + { + operation: "host_create_success", + userId, + hostId: createdHost.id, + name, + ip, + port, + authType: effectiveAuthType, + }, + ); + + res.json(resolvedHost); } catch (err) { - logger.error('Failed to save SSH data', err); - res.status(500).json({error: 'Failed to save SSH data'}); + sshLogger.error("Failed to save SSH host to database", err, { + operation: "host_create", + userId, + name, + ip, + port, + authType: effectiveAuthType, + }); + res.status(500).json({ error: "Failed to save SSH data" }); } -}); + }, +); // Route: Update SSH data (requires JWT) // PUT /ssh/host/:id -router.put('/db/host/:id', authenticateJWT, upload.single('key'), async (req: Request, res: Response) => { +router.put( + "/db/host/:id", + authenticateJWT, + upload.single("key"), + async (req: Request, res: Response) => { + const hostId = req.params.id; + const userId = (req as any).userId; let hostData: any; - if (req.headers['content-type']?.includes('multipart/form-data')) { - if (req.body.data) { - try { - hostData = JSON.parse(req.body.data); - } catch (err) { - logger.warn('Invalid JSON data in multipart request'); - return res.status(400).json({error: 'Invalid JSON data'}); - } - } else { - logger.warn('Missing data field in multipart request'); - return res.status(400).json({error: 'Missing data field'}); + if (req.headers["content-type"]?.includes("multipart/form-data")) { + if (req.body.data) { + try { + hostData = JSON.parse(req.body.data); + } catch (err) { + sshLogger.warn("Invalid JSON data in multipart request", { + operation: "host_update", + hostId: parseInt(hostId), + userId, + error: err, + }); + return res.status(400).json({ error: "Invalid JSON data" }); } + } else { + sshLogger.warn("Missing data field in multipart request", { + operation: "host_update", + hostId: parseInt(hostId), + userId, + }); + return res.status(400).json({ error: "Missing data field" }); + } - if (req.file) { - hostData.key = req.file.buffer.toString('utf8'); - } + if (req.file) { + hostData.key = req.file.buffer.toString("utf8"); + } } else { - hostData = req.body; + hostData = req.body; } const { - name, - folder, - tags, - ip, - port, - username, - password, - authMethod, - key, - keyPassword, - keyType, - pin, - enableTerminal, - enableTunnel, - enableFileManager, - defaultPath, - tunnelConnections + name, + folder, + tags, + ip, + port, + username, + password, + authMethod, + authType, + credentialId, + key, + keyPassword, + keyType, + pin, + enableTerminal, + enableTunnel, + enableFileManager, + defaultPath, + tunnelConnections, } = hostData; - const {id} = req.params; - const userId = (req as any).userId; - if (!isNonEmptyString(userId) || !isNonEmptyString(ip) || !isValidPort(port) || !id) { - logger.warn('Invalid SSH data input for update'); - return res.status(400).json({error: 'Invalid SSH data'}); + if ( + !isNonEmptyString(userId) || + !isNonEmptyString(ip) || + !isValidPort(port) || + !hostId + ) { + sshLogger.warn("Invalid SSH data input validation failed for update", { + operation: "host_update", + hostId: parseInt(hostId), + userId, + hasIp: !!ip, + port, + isValidPort: isValidPort(port), + }); + return res.status(400).json({ error: "Invalid SSH data" }); } + const effectiveAuthType = authType || authMethod; const sshDataObj: any = { - name, - folder, - tags: Array.isArray(tags) ? tags.join(',') : (tags || ''), - ip, - port, - username, - authType: authMethod, - pin: !!pin ? 1 : 0, - enableTerminal: !!enableTerminal ? 1 : 0, - enableTunnel: !!enableTunnel ? 1 : 0, - tunnelConnections: Array.isArray(tunnelConnections) ? JSON.stringify(tunnelConnections) : null, - enableFileManager: !!enableFileManager ? 1 : 0, - defaultPath: defaultPath || null, + name, + folder, + tags: Array.isArray(tags) ? tags.join(",") : tags || "", + ip, + port, + username, + authType: effectiveAuthType, + credentialId: credentialId || null, + pin: pin ? 1 : 0, + enableTerminal: enableTerminal ? 1 : 0, + enableTunnel: enableTunnel ? 1 : 0, + tunnelConnections: Array.isArray(tunnelConnections) + ? JSON.stringify(tunnelConnections) + : null, + enableFileManager: enableFileManager ? 1 : 0, + defaultPath: defaultPath || null, }; - if (authMethod === 'password') { + if (effectiveAuthType === "password") { + if (password) { sshDataObj.password = password; - sshDataObj.key = null; - sshDataObj.keyPassword = null; - sshDataObj.keyType = null; - } else if (authMethod === 'key') { + } + sshDataObj.key = null; + sshDataObj.keyPassword = null; + sshDataObj.keyType = null; + } else if (effectiveAuthType === "key") { + if (key) { sshDataObj.key = key; - sshDataObj.keyPassword = keyPassword; + } + if (keyPassword !== undefined) { + sshDataObj.keyPassword = keyPassword || null; + } + if (keyType) { sshDataObj.keyType = keyType; - sshDataObj.password = null; + } + sshDataObj.password = null; } try { - await db.update(sshData) - .set(sshDataObj) - .where(and(eq(sshData.id, Number(id)), eq(sshData.userId, userId))); - res.json({message: 'SSH data updated'}); + await db + .update(sshData) + .set(sshDataObj) + .where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))); + + const updatedHosts = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))); + + if (updatedHosts.length === 0) { + sshLogger.warn("Updated host not found after update", { + operation: "host_update", + hostId: parseInt(hostId), + userId, + }); + return res.status(404).json({ error: "Host not found after update" }); + } + + const updatedHost = updatedHosts[0]; + const baseHost = { + ...updatedHost, + tags: + typeof updatedHost.tags === "string" + ? updatedHost.tags + ? updatedHost.tags.split(",").filter(Boolean) + : [] + : [], + pin: !!updatedHost.pin, + enableTerminal: !!updatedHost.enableTerminal, + enableTunnel: !!updatedHost.enableTunnel, + tunnelConnections: updatedHost.tunnelConnections + ? JSON.parse(updatedHost.tunnelConnections) + : [], + enableFileManager: !!updatedHost.enableFileManager, + }; + + const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; + + sshLogger.success( + `SSH host updated: ${name} (${ip}:${port}) by user ${userId}`, + { + operation: "host_update_success", + userId, + hostId: parseInt(hostId), + name, + ip, + port, + authType: effectiveAuthType, + }, + ); + + res.json(resolvedHost); } catch (err) { - logger.error('Failed to update SSH data', err); - res.status(500).json({error: 'Failed to update SSH data'}); + sshLogger.error("Failed to update SSH host in database", err, { + operation: "host_update", + hostId: parseInt(hostId), + userId, + name, + ip, + port, + authType: effectiveAuthType, + }); + res.status(500).json({ error: "Failed to update SSH data" }); } -}); + }, +); // Route: Get SSH data for the authenticated user (requires JWT) // GET /ssh/host -router.get('/db/host', authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; - if (!isNonEmptyString(userId)) { - logger.warn('Invalid userId for SSH data fetch'); - return res.status(400).json({error: 'Invalid userId'}); - } - try { - const data = await db - .select() - .from(sshData) - .where(eq(sshData.userId, userId)); - const result = data.map((row: any) => ({ - ...row, - tags: typeof row.tags === 'string' ? (row.tags ? row.tags.split(',').filter(Boolean) : []) : [], - pin: !!row.pin, - enableTerminal: !!row.enableTerminal, - enableTunnel: !!row.enableTunnel, - tunnelConnections: row.tunnelConnections ? JSON.parse(row.tunnelConnections) : [], - enableFileManager: !!row.enableFileManager, - })); - res.json(result); - } catch (err) { - logger.error('Failed to fetch SSH data', err); - res.status(500).json({error: 'Failed to fetch SSH data'}); - } +router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + if (!isNonEmptyString(userId)) { + sshLogger.warn("Invalid userId for SSH data fetch", { + operation: "host_fetch", + userId, + }); + return res.status(400).json({ error: "Invalid userId" }); + } + try { + const data = await db + .select() + .from(sshData) + .where(eq(sshData.userId, userId)); + + const result = await Promise.all( + data.map(async (row: any) => { + const baseHost = { + ...row, + tags: + typeof row.tags === "string" + ? row.tags + ? row.tags.split(",").filter(Boolean) + : [] + : [], + pin: !!row.pin, + enableTerminal: !!row.enableTerminal, + enableTunnel: !!row.enableTunnel, + tunnelConnections: row.tunnelConnections + ? JSON.parse(row.tunnelConnections) + : [], + enableFileManager: !!row.enableFileManager, + }; + + return (await resolveHostCredentials(baseHost)) || baseHost; + }), + ); + + res.json(result); + } catch (err) { + sshLogger.error("Failed to fetch SSH hosts from database", err, { + operation: "host_fetch", + userId, + }); + res.status(500).json({ error: "Failed to fetch SSH data" }); + } }); // Route: Get SSH host by ID (requires JWT) // GET /ssh/host/:id -router.get('/db/host/:id', authenticateJWT, async (req: Request, res: Response) => { - const {id} = req.params; +router.get( + "/db/host/:id", + authenticateJWT, + async (req: Request, res: Response) => { + const hostId = req.params.id; const userId = (req as any).userId; - if (!isNonEmptyString(userId) || !id) { - logger.warn('Invalid request for SSH host fetch'); - return res.status(400).json({error: 'Invalid request'}); - } - - try { - const data = await db - .select() - .from(sshData) - .where(and(eq(sshData.id, Number(id)), eq(sshData.userId, userId))); - - if (data.length === 0) { - return res.status(404).json({error: 'SSH host not found'}); - } - - const host = data[0]; - const result = { - ...host, - tags: typeof host.tags === 'string' ? (host.tags ? host.tags.split(',').filter(Boolean) : []) : [], - pin: !!host.pin, - enableTerminal: !!host.enableTerminal, - enableTunnel: !!host.enableTunnel, - tunnelConnections: host.tunnelConnections ? JSON.parse(host.tunnelConnections) : [], - enableFileManager: !!host.enableFileManager, - }; - - res.json(result); - } catch (err) { - logger.error('Failed to fetch SSH host', err); - res.status(500).json({error: 'Failed to fetch SSH host'}); - } -}); - -// Route: Get all unique folders for the authenticated user (requires JWT) -// GET /ssh/folders -router.get('/db/folders', authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; - if (!isNonEmptyString(userId)) { - logger.warn('Invalid userId for SSH folder fetch'); - return res.status(400).json({error: 'Invalid userId'}); + if (!isNonEmptyString(userId) || !hostId) { + sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", { + operation: "host_fetch_by_id", + hostId: parseInt(hostId), + userId, + }); + return res.status(400).json({ error: "Invalid userId or hostId" }); } try { - const data = await db - .select({folder: sshData.folder}) - .from(sshData) - .where(eq(sshData.userId, userId)); + const data = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))); - const folderCounts: Record = {}; - data.forEach(d => { - if (d.folder && d.folder.trim() !== '') { - folderCounts[d.folder] = (folderCounts[d.folder] || 0) + 1; - } + if (data.length === 0) { + sshLogger.warn("SSH host not found", { + operation: "host_fetch_by_id", + hostId: parseInt(hostId), + userId, }); + return res.status(404).json({ error: "SSH host not found" }); + } - const folders = Object.keys(folderCounts).filter(folder => folderCounts[folder] > 0); + const host = data[0]; + const result = { + ...host, + tags: + typeof host.tags === "string" + ? host.tags + ? host.tags.split(",").filter(Boolean) + : [] + : [], + pin: !!host.pin, + enableTerminal: !!host.enableTerminal, + enableTunnel: !!host.enableTunnel, + tunnelConnections: host.tunnelConnections + ? JSON.parse(host.tunnelConnections) + : [], + enableFileManager: !!host.enableFileManager, + }; - res.json(folders); + res.json((await resolveHostCredentials(result)) || result); } catch (err) { - logger.error('Failed to fetch SSH folders', err); - res.status(500).json({error: 'Failed to fetch SSH folders'}); + sshLogger.error("Failed to fetch SSH host by ID from database", err, { + operation: "host_fetch_by_id", + hostId: parseInt(hostId), + userId, + }); + res.status(500).json({ error: "Failed to fetch SSH host" }); } -}); + }, +); // Route: Delete SSH host by id (requires JWT) // DELETE /ssh/host/:id -router.delete('/db/host/:id', authenticateJWT, async (req: Request, res: Response) => { +router.delete( + "/db/host/:id", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {id} = req.params; - if (!isNonEmptyString(userId) || !id) { - logger.warn('Invalid userId or id for SSH host delete'); - return res.status(400).json({error: 'Invalid userId or id'}); + const hostId = req.params.id; + + if (!isNonEmptyString(userId) || !hostId) { + sshLogger.warn("Invalid userId or hostId for SSH host delete", { + operation: "host_delete", + hostId: parseInt(hostId), + userId, + }); + return res.status(400).json({ error: "Invalid userId or id" }); } try { - const result = await db.delete(sshData) - .where(and(eq(sshData.id, Number(id)), eq(sshData.userId, userId))); - res.json({message: 'SSH host deleted'}); + const hostToDelete = await db + .select() + .from(sshData) + .where(and(eq(sshData.id, Number(hostId)), eq(sshData.userId, userId))); + + if (hostToDelete.length === 0) { + sshLogger.warn("SSH host not found for deletion", { + operation: "host_delete", + hostId: parseInt(hostId), + userId, + }); + return res.status(404).json({ error: "SSH host not found" }); + } + + const numericHostId = Number(hostId); + + await db + .delete(fileManagerRecent) + .where( + and( + eq(fileManagerRecent.userId, userId), + eq(fileManagerRecent.hostId, numericHostId), + ), + ); + + await db + .delete(fileManagerPinned) + .where( + and( + eq(fileManagerPinned.userId, userId), + eq(fileManagerPinned.hostId, numericHostId), + ), + ); + + await db + .delete(fileManagerShortcuts) + .where( + and( + eq(fileManagerShortcuts.userId, userId), + eq(fileManagerShortcuts.hostId, numericHostId), + ), + ); + + await db + .delete(sshCredentialUsage) + .where( + and( + eq(sshCredentialUsage.userId, userId), + eq(sshCredentialUsage.hostId, numericHostId), + ), + ); + + const result = await db + .delete(sshData) + .where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId))); + + const host = hostToDelete[0]; + sshLogger.success( + `SSH host deleted: ${host.name} (${host.ip}:${host.port}) by user ${userId}`, + { + operation: "host_delete_success", + userId, + hostId: parseInt(hostId), + name: host.name, + ip: host.ip, + port: host.port, + }, + ); + + res.json({ message: "SSH host deleted" }); } catch (err) { - logger.error('Failed to delete SSH host', err); - res.status(500).json({error: 'Failed to delete SSH host'}); + sshLogger.error("Failed to delete SSH host from database", err, { + operation: "host_delete", + hostId: parseInt(hostId), + userId, + }); + res.status(500).json({ error: "Failed to delete SSH host" }); } -}); + }, +); // Route: Get recent files (requires JWT) // GET /ssh/file_manager/recent -router.get('/file_manager/recent', authenticateJWT, async (req: Request, res: Response) => { +router.get( + "/file_manager/recent", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null; + const hostId = req.query.hostId + ? parseInt(req.query.hostId as string) + : null; if (!isNonEmptyString(userId)) { - logger.warn('Invalid userId for recent files fetch'); - return res.status(400).json({error: 'Invalid userId'}); + sshLogger.warn("Invalid userId for recent files fetch"); + return res.status(400).json({ error: "Invalid userId" }); } if (!hostId) { - logger.warn('Host ID is required for recent files fetch'); - return res.status(400).json({error: 'Host ID is required'}); + sshLogger.warn("Host ID is required for recent files fetch"); + return res.status(400).json({ error: "Host ID is required" }); } try { - const recentFiles = await db - .select() - .from(fileManagerRecent) - .where(and( - eq(fileManagerRecent.userId, userId), - eq(fileManagerRecent.hostId, hostId) - )) - .orderBy(desc(fileManagerRecent.lastOpened)); - res.json(recentFiles); - } catch (err) { - logger.error('Failed to fetch recent files', err); - res.status(500).json({error: 'Failed to fetch recent files'}); - } -}); + const recentFiles = await db + .select() + .from(fileManagerRecent) + .where( + and( + eq(fileManagerRecent.userId, userId), + eq(fileManagerRecent.hostId, hostId), + ), + ) + .orderBy(desc(fileManagerRecent.lastOpened)) + .limit(20); -// Route: Add file to recent (requires JWT) + res.json(recentFiles); + } catch (err) { + sshLogger.error("Failed to fetch recent files", err); + res.status(500).json({ error: "Failed to fetch recent files" }); + } + }, +); + +// Route: Add recent file (requires JWT) // POST /ssh/file_manager/recent -router.post('/file_manager/recent', authenticateJWT, async (req: Request, res: Response) => { +router.post( + "/file_manager/recent", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {name, path, hostId} = req.body; - if (!isNonEmptyString(userId) || !name || !path || !hostId) { - logger.warn('Invalid request for adding recent file'); - return res.status(400).json({error: 'Invalid request - userId, name, path, and hostId are required'}); + const { hostId, path, name } = req.body; + + if (!isNonEmptyString(userId) || !hostId || !path) { + sshLogger.warn("Invalid data for recent file addition"); + return res.status(400).json({ error: "Invalid data" }); } + try { - const conditions = [ + const existing = await db + .select() + .from(fileManagerRecent) + .where( + and( eq(fileManagerRecent.userId, userId), + eq(fileManagerRecent.hostId, hostId), eq(fileManagerRecent.path, path), - eq(fileManagerRecent.hostId, hostId) - ]; + ), + ); - const existing = await db - .select() - .from(fileManagerRecent) - .where(and(...conditions)); + if (existing.length > 0) { + await db + .update(fileManagerRecent) + .set({ lastOpened: new Date().toISOString() }) + .where(eq(fileManagerRecent.id, existing[0].id)); + } else { + await db.insert(fileManagerRecent).values({ + userId, + hostId, + path, + name: name || path.split("/").pop() || "Unknown", + lastOpened: new Date().toISOString(), + }); + } - if (existing.length > 0) { - await db - .update(fileManagerRecent) - .set({lastOpened: new Date().toISOString()}) - .where(and(...conditions)); - } else { - await db.insert(fileManagerRecent).values({ - userId, - hostId, - name, - path, - lastOpened: new Date().toISOString() - }); - } - res.json({message: 'File added to recent'}); + res.json({ message: "Recent file added" }); } catch (err) { - logger.error('Failed to add recent file', err); - res.status(500).json({error: 'Failed to add recent file'}); + sshLogger.error("Failed to add recent file", err); + res.status(500).json({ error: "Failed to add recent file" }); } -}); + }, +); -// Route: Remove file from recent (requires JWT) +// Route: Remove recent file (requires JWT) // DELETE /ssh/file_manager/recent -router.delete('/file_manager/recent', authenticateJWT, async (req: Request, res: Response) => { +router.delete( + "/file_manager/recent", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {name, path, hostId} = req.body; - if (!isNonEmptyString(userId) || !name || !path || !hostId) { - logger.warn('Invalid request for removing recent file'); - return res.status(400).json({error: 'Invalid request - userId, name, path, and hostId are required'}); - } - try { - const conditions = [ - eq(fileManagerRecent.userId, userId), - eq(fileManagerRecent.path, path), - eq(fileManagerRecent.hostId, hostId) - ]; + const { hostId, path, name } = req.body; - const result = await db - .delete(fileManagerRecent) - .where(and(...conditions)); - res.json({message: 'File removed from recent'}); - } catch (err) { - logger.error('Failed to remove recent file', err); - res.status(500).json({error: 'Failed to remove recent file'}); + if (!isNonEmptyString(userId) || !hostId || !path) { + sshLogger.warn("Invalid data for recent file deletion"); + return res.status(400).json({ error: "Invalid data" }); } -}); + + try { + await db + .delete(fileManagerRecent) + .where( + and( + eq(fileManagerRecent.userId, userId), + eq(fileManagerRecent.hostId, hostId), + eq(fileManagerRecent.path, path), + ), + ); + + res.json({ message: "Recent file removed" }); + } catch (err) { + sshLogger.error("Failed to remove recent file", err); + res.status(500).json({ error: "Failed to remove recent file" }); + } + }, +); // Route: Get pinned files (requires JWT) // GET /ssh/file_manager/pinned -router.get('/file_manager/pinned', authenticateJWT, async (req: Request, res: Response) => { +router.get( + "/file_manager/pinned", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null; + const hostId = req.query.hostId + ? parseInt(req.query.hostId as string) + : null; if (!isNonEmptyString(userId)) { - logger.warn('Invalid userId for pinned files fetch'); - return res.status(400).json({error: 'Invalid userId'}); + sshLogger.warn("Invalid userId for pinned files fetch"); + return res.status(400).json({ error: "Invalid userId" }); } if (!hostId) { - logger.warn('Host ID is required for pinned files fetch'); - return res.status(400).json({error: 'Host ID is required'}); + sshLogger.warn("Host ID is required for pinned files fetch"); + return res.status(400).json({ error: "Host ID is required" }); } try { - const pinnedFiles = await db - .select() - .from(fileManagerPinned) - .where(and( - eq(fileManagerPinned.userId, userId), - eq(fileManagerPinned.hostId, hostId) - )) - .orderBy(fileManagerPinned.pinnedAt); - res.json(pinnedFiles); - } catch (err) { - logger.error('Failed to fetch pinned files', err); - res.status(500).json({error: 'Failed to fetch pinned files'}); - } -}); + const pinnedFiles = await db + .select() + .from(fileManagerPinned) + .where( + and( + eq(fileManagerPinned.userId, userId), + eq(fileManagerPinned.hostId, hostId), + ), + ) + .orderBy(desc(fileManagerPinned.pinnedAt)); -// Route: Add file to pinned (requires JWT) + res.json(pinnedFiles); + } catch (err) { + sshLogger.error("Failed to fetch pinned files", err); + res.status(500).json({ error: "Failed to fetch pinned files" }); + } + }, +); + +// Route: Add pinned file (requires JWT) // POST /ssh/file_manager/pinned -router.post('/file_manager/pinned', authenticateJWT, async (req: Request, res: Response) => { +router.post( + "/file_manager/pinned", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {name, path, hostId} = req.body; - if (!isNonEmptyString(userId) || !name || !path || !hostId) { - logger.warn('Invalid request for adding pinned file'); - return res.status(400).json({error: 'Invalid request - userId, name, path, and hostId are required'}); + const { hostId, path, name } = req.body; + + if (!isNonEmptyString(userId) || !hostId || !path) { + sshLogger.warn("Invalid data for pinned file addition"); + return res.status(400).json({ error: "Invalid data" }); } + try { - const conditions = [ + const existing = await db + .select() + .from(fileManagerPinned) + .where( + and( eq(fileManagerPinned.userId, userId), + eq(fileManagerPinned.hostId, hostId), eq(fileManagerPinned.path, path), - eq(fileManagerPinned.hostId, hostId) - ]; + ), + ); - const existing = await db - .select() - .from(fileManagerPinned) - .where(and(...conditions)); + if (existing.length > 0) { + return res.status(409).json({ error: "File already pinned" }); + } - if (existing.length === 0) { - await db.insert(fileManagerPinned).values({ - userId, - hostId, - name, - path, - pinnedAt: new Date().toISOString() - }); - } - res.json({message: 'File pinned successfully'}); + await db.insert(fileManagerPinned).values({ + userId, + hostId, + path, + name: name || path.split("/").pop() || "Unknown", + pinnedAt: new Date().toISOString(), + }); + + res.json({ message: "File pinned" }); } catch (err) { - logger.error('Failed to pin file', err); - res.status(500).json({error: 'Failed to pin file'}); + sshLogger.error("Failed to pin file", err); + res.status(500).json({ error: "Failed to pin file" }); } -}); + }, +); -// Route: Remove file from pinned (requires JWT) +// Route: Remove pinned file (requires JWT) // DELETE /ssh/file_manager/pinned -router.delete('/file_manager/pinned', authenticateJWT, async (req: Request, res: Response) => { +router.delete( + "/file_manager/pinned", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {name, path, hostId} = req.body; - if (!isNonEmptyString(userId) || !name || !path || !hostId) { - logger.warn('Invalid request for removing pinned file'); - return res.status(400).json({error: 'Invalid request - userId, name, path, and hostId are required'}); + const { hostId, path, name } = req.body; + + if (!isNonEmptyString(userId) || !hostId || !path) { + sshLogger.warn("Invalid data for pinned file deletion"); + return res.status(400).json({ error: "Invalid data" }); } + try { - const conditions = [ + await db + .delete(fileManagerPinned) + .where( + and( eq(fileManagerPinned.userId, userId), + eq(fileManagerPinned.hostId, hostId), eq(fileManagerPinned.path, path), - eq(fileManagerPinned.hostId, hostId) - ]; + ), + ); - const result = await db - .delete(fileManagerPinned) - .where(and(...conditions)); - res.json({message: 'File unpinned successfully'}); + res.json({ message: "Pinned file removed" }); } catch (err) { - logger.error('Failed to unpin file', err); - res.status(500).json({error: 'Failed to unpin file'}); + sshLogger.error("Failed to remove pinned file", err); + res.status(500).json({ error: "Failed to remove pinned file" }); } -}); + }, +); -// Route: Get folder shortcuts (requires JWT) +// Route: Get shortcuts (requires JWT) // GET /ssh/file_manager/shortcuts -router.get('/file_manager/shortcuts', authenticateJWT, async (req: Request, res: Response) => { +router.get( + "/file_manager/shortcuts", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const hostId = req.query.hostId ? parseInt(req.query.hostId as string) : null; + const hostId = req.query.hostId + ? parseInt(req.query.hostId as string) + : null; if (!isNonEmptyString(userId)) { - return res.status(400).json({error: 'Invalid userId'}); + sshLogger.warn("Invalid userId for shortcuts fetch"); + return res.status(400).json({ error: "Invalid userId" }); } if (!hostId) { - return res.status(400).json({error: 'Host ID is required'}); + sshLogger.warn("Host ID is required for shortcuts fetch"); + return res.status(400).json({ error: "Host ID is required" }); } try { - const shortcuts = await db - .select() - .from(fileManagerShortcuts) - .where(and( - eq(fileManagerShortcuts.userId, userId), - eq(fileManagerShortcuts.hostId, hostId) - )) - .orderBy(fileManagerShortcuts.createdAt); - res.json(shortcuts); - } catch (err) { - logger.error('Failed to fetch shortcuts', err); - res.status(500).json({error: 'Failed to fetch shortcuts'}); - } -}); + const shortcuts = await db + .select() + .from(fileManagerShortcuts) + .where( + and( + eq(fileManagerShortcuts.userId, userId), + eq(fileManagerShortcuts.hostId, hostId), + ), + ) + .orderBy(desc(fileManagerShortcuts.createdAt)); -// Route: Add folder shortcut (requires JWT) + res.json(shortcuts); + } catch (err) { + sshLogger.error("Failed to fetch shortcuts", err); + res.status(500).json({ error: "Failed to fetch shortcuts" }); + } + }, +); + +// Route: Add shortcut (requires JWT) // POST /ssh/file_manager/shortcuts -router.post('/file_manager/shortcuts', authenticateJWT, async (req: Request, res: Response) => { +router.post( + "/file_manager/shortcuts", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {name, path, hostId} = req.body; - if (!isNonEmptyString(userId) || !name || !path || !hostId) { - return res.status(400).json({error: 'Invalid request - userId, name, path, and hostId are required'}); + const { hostId, path, name } = req.body; + + if (!isNonEmptyString(userId) || !hostId || !path) { + sshLogger.warn("Invalid data for shortcut addition"); + return res.status(400).json({ error: "Invalid data" }); } + try { - const conditions = [ + const existing = await db + .select() + .from(fileManagerShortcuts) + .where( + and( eq(fileManagerShortcuts.userId, userId), + eq(fileManagerShortcuts.hostId, hostId), eq(fileManagerShortcuts.path, path), - eq(fileManagerShortcuts.hostId, hostId) - ]; + ), + ); - const existing = await db - .select() - .from(fileManagerShortcuts) - .where(and(...conditions)); + if (existing.length > 0) { + return res.status(409).json({ error: "Shortcut already exists" }); + } - if (existing.length === 0) { - await db.insert(fileManagerShortcuts).values({ - userId, - hostId, - name, - path, - createdAt: new Date().toISOString() - }); - } - res.json({message: 'Shortcut added successfully'}); + await db.insert(fileManagerShortcuts).values({ + userId, + hostId, + path, + name: name || path.split("/").pop() || "Unknown", + createdAt: new Date().toISOString(), + }); + + res.json({ message: "Shortcut added" }); } catch (err) { - logger.error('Failed to add shortcut', err); - res.status(500).json({error: 'Failed to add shortcut'}); + sshLogger.error("Failed to add shortcut", err); + res.status(500).json({ error: "Failed to add shortcut" }); } -}); + }, +); -// Route: Remove folder shortcut (requires JWT) +// Route: Remove shortcut (requires JWT) // DELETE /ssh/file_manager/shortcuts -router.delete('/file_manager/shortcuts', authenticateJWT, async (req: Request, res: Response) => { +router.delete( + "/file_manager/shortcuts", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {name, path, hostId} = req.body; - if (!isNonEmptyString(userId) || !name || !path || !hostId) { - return res.status(400).json({error: 'Invalid request - userId, name, path, and hostId are required'}); + const { hostId, path, name } = req.body; + + if (!isNonEmptyString(userId) || !hostId || !path) { + sshLogger.warn("Invalid data for shortcut deletion"); + return res.status(400).json({ error: "Invalid data" }); } + try { - const conditions = [ + await db + .delete(fileManagerShortcuts) + .where( + and( eq(fileManagerShortcuts.userId, userId), + eq(fileManagerShortcuts.hostId, hostId), eq(fileManagerShortcuts.path, path), - eq(fileManagerShortcuts.hostId, hostId) - ]; + ), + ); - const result = await db - .delete(fileManagerShortcuts) - .where(and(...conditions)); - res.json({message: 'Shortcut removed successfully'}); + res.json({ message: "Shortcut removed" }); } catch (err) { - logger.error('Failed to remove shortcut', err); - res.status(500).json({error: 'Failed to remove shortcut'}); + sshLogger.error("Failed to remove shortcut", err); + res.status(500).json({ error: "Failed to remove shortcut" }); } -}); + }, +); -// Route: Bulk import SSH hosts from JSON (requires JWT) -// POST /ssh/bulk-import -router.post('/bulk-import', authenticateJWT, async (req: Request, res: Response) => { +async function resolveHostCredentials(host: any): Promise { + try { + if (host.credentialId && host.userId) { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, host.credentialId), + eq(sshCredentials.userId, host.userId), + ), + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + return { + ...host, + username: credential.username, + authType: credential.authType, + password: credential.password, + key: credential.key, + keyPassword: credential.keyPassword, + keyType: credential.keyType, + }; + } + } + return host; + } catch (error) { + sshLogger.warn( + `Failed to resolve credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + return host; + } +} + +// Route: Rename folder (requires JWT) +// PUT /ssh/db/folders/rename +router.put( + "/folders/rename", + authenticateJWT, + async (req: Request, res: Response) => { const userId = (req as any).userId; - const {hosts} = req.body; + const { oldName, newName } = req.body; + + if (!isNonEmptyString(userId) || !oldName || !newName) { + sshLogger.warn("Invalid data for folder rename"); + return res + .status(400) + .json({ error: "Old name and new name are required" }); + } + + if (oldName === newName) { + return res.json({ message: "Folder name unchanged" }); + } + + try { + const updatedHosts = await db + .update(sshData) + .set({ + folder: newName, + updatedAt: new Date().toISOString(), + }) + .where(and(eq(sshData.userId, userId), eq(sshData.folder, oldName))) + .returning(); + + const updatedCredentials = await db + .update(sshCredentials) + .set({ + folder: newName, + updatedAt: new Date().toISOString(), + }) + .where( + and( + eq(sshCredentials.userId, userId), + eq(sshCredentials.folder, oldName), + ), + ) + .returning(); + + res.json({ + message: "Folder renamed successfully", + updatedHosts: updatedHosts.length, + updatedCredentials: updatedCredentials.length, + }); + } catch (err) { + sshLogger.error("Failed to rename folder", err, { + operation: "folder_rename", + userId, + oldName, + newName, + }); + res.status(500).json({ error: "Failed to rename folder" }); + } + }, +); + +// Route: Bulk import SSH hosts (requires JWT) +// POST /ssh/bulk-import +router.post( + "/bulk-import", + authenticateJWT, + async (req: Request, res: Response) => { + const userId = (req as any).userId; + const { hosts } = req.body; if (!Array.isArray(hosts) || hosts.length === 0) { - logger.warn('Invalid bulk import data - hosts array is required and must not be empty'); - return res.status(400).json({error: 'Hosts array is required and must not be empty'}); + return res + .status(400) + .json({ error: "Hosts array is required and must not be empty" }); } if (hosts.length > 100) { - logger.warn(`Bulk import attempted with too many hosts: ${hosts.length}`); - return res.status(400).json({error: 'Maximum 100 hosts allowed per import'}); + return res + .status(400) + .json({ error: "Maximum 100 hosts allowed per import" }); } const results = { - success: 0, - failed: 0, - errors: [] as string[] + success: 0, + failed: 0, + errors: [] as string[], }; for (let i = 0; i < hosts.length; i++) { - const hostData = hosts[i]; + const hostData = hosts[i]; - try { - if (!isNonEmptyString(hostData.ip) || !isValidPort(hostData.port) || !isNonEmptyString(hostData.username)) { - results.failed++; - results.errors.push(`Host ${i + 1}: Missing or invalid required fields (ip, port, username)`); - continue; - } - - if (hostData.authType !== 'password' && hostData.authType !== 'key') { - results.failed++; - results.errors.push(`Host ${i + 1}: Invalid authType. Must be 'password' or 'key'`); - continue; - } - - if (hostData.authType === 'password' && !isNonEmptyString(hostData.password)) { - results.failed++; - results.errors.push(`Host ${i + 1}: Password required for password authentication`); - continue; - } - - if (hostData.authType === 'key' && !isNonEmptyString(hostData.key)) { - results.failed++; - results.errors.push(`Host ${i + 1}: SSH key required for key authentication`); - continue; - } - - if (hostData.enableTunnel && Array.isArray(hostData.tunnelConnections)) { - for (let j = 0; j < hostData.tunnelConnections.length; j++) { - const conn = hostData.tunnelConnections[j]; - if (!isValidPort(conn.sourcePort) || !isValidPort(conn.endpointPort) || !isNonEmptyString(conn.endpointHost)) { - results.failed++; - results.errors.push(`Host ${i + 1}, Tunnel ${j + 1}: Invalid tunnel connection data`); - break; - } - } - } - - const sshDataObj: any = { - userId: userId, - name: hostData.name || '', - folder: hostData.folder || '', - tags: Array.isArray(hostData.tags) ? hostData.tags.join(',') : (hostData.tags || ''), - ip: hostData.ip, - port: hostData.port, - username: hostData.username, - authType: hostData.authType, - pin: !!hostData.pin ? 1 : 0, - enableTerminal: !!hostData.enableTerminal ? 1 : 0, - enableTunnel: !!hostData.enableTunnel ? 1 : 0, - tunnelConnections: Array.isArray(hostData.tunnelConnections) ? JSON.stringify(hostData.tunnelConnections) : null, - enableFileManager: !!hostData.enableFileManager ? 1 : 0, - defaultPath: hostData.defaultPath || null, - }; - - if (hostData.authType === 'password') { - sshDataObj.password = hostData.password; - sshDataObj.key = null; - sshDataObj.keyPassword = null; - sshDataObj.keyType = null; - } else if (hostData.authType === 'key') { - sshDataObj.key = hostData.key; - sshDataObj.keyPassword = hostData.keyPassword || null; - sshDataObj.keyType = hostData.keyType || null; - sshDataObj.password = null; - } - - await db.insert(sshData).values(sshDataObj); - results.success++; - - } catch (err) { - results.failed++; - results.errors.push(`Host ${i + 1}: ${err instanceof Error ? err.message : 'Unknown error'}`); - logger.error(`Failed to import host ${i + 1}:`, err); + try { + if ( + !isNonEmptyString(hostData.ip) || + !isValidPort(hostData.port) || + !isNonEmptyString(hostData.username) + ) { + results.failed++; + results.errors.push( + `Host ${i + 1}: Missing required fields (ip, port, username)`, + ); + continue; } - } - if (results.success > 0) { - logger.success(`Bulk import completed: ${results.success} successful, ${results.failed} failed`); - } else { - logger.warn(`Bulk import failed: ${results.failed} failed`); + if (!["password", "key", "credential"].includes(hostData.authType)) { + results.failed++; + results.errors.push( + `Host ${i + 1}: Invalid authType. Must be 'password', 'key', or 'credential'`, + ); + continue; + } + + if ( + hostData.authType === "password" && + !isNonEmptyString(hostData.password) + ) { + results.failed++; + results.errors.push( + `Host ${i + 1}: Password required for password authentication`, + ); + continue; + } + + if (hostData.authType === "key" && !isNonEmptyString(hostData.key)) { + results.failed++; + results.errors.push( + `Host ${i + 1}: Key required for key authentication`, + ); + continue; + } + + if (hostData.authType === "credential" && !hostData.credentialId) { + results.failed++; + results.errors.push( + `Host ${i + 1}: credentialId required for credential authentication`, + ); + continue; + } + + const sshDataObj: any = { + userId: userId, + name: hostData.name || `${hostData.username}@${hostData.ip}`, + folder: hostData.folder || "Default", + tags: Array.isArray(hostData.tags) ? hostData.tags.join(",") : "", + ip: hostData.ip, + port: hostData.port, + username: hostData.username, + password: hostData.authType === "password" ? hostData.password : null, + authType: hostData.authType, + credentialId: + hostData.authType === "credential" ? hostData.credentialId : null, + key: hostData.authType === "key" ? hostData.key : null, + keyPassword: + hostData.authType === "key" ? hostData.keyPassword : null, + keyType: + hostData.authType === "key" ? hostData.keyType || "auto" : null, + pin: hostData.pin || false, + enableTerminal: hostData.enableTerminal !== false, + enableTunnel: hostData.enableTunnel !== false, + enableFileManager: hostData.enableFileManager !== false, + defaultPath: hostData.defaultPath || "/", + tunnelConnections: hostData.tunnelConnections + ? JSON.stringify(hostData.tunnelConnections) + : "[]", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + await db.insert(sshData).values(sshDataObj); + results.success++; + } catch (error) { + results.failed++; + results.errors.push( + `Host ${i + 1}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } } res.json({ - message: `Import completed: ${results.success} successful, ${results.failed} failed`, - ...results + message: `Import completed: ${results.success} successful, ${results.failed} failed`, + success: results.success, + failed: results.failed, + errors: results.errors, }); -}); + }, +); -export default router; \ No newline at end of file +export default router; diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 11dc46d0..fe4a7a10 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -1,1349 +1,1609 @@ -import express from 'express'; -import {db} from '../db/index.js'; -import {users, settings, sshData, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts} from '../db/schema.js'; -import {eq, and} from 'drizzle-orm'; -import chalk from 'chalk'; -import bcrypt from 'bcryptjs'; -import {nanoid} from 'nanoid'; -import jwt from 'jsonwebtoken'; -import speakeasy from 'speakeasy'; -import QRCode from 'qrcode'; -import type {Request, Response, NextFunction} from 'express'; +import express from "express"; +import { db } from "../db/index.js"; +import { + users, + sshData, + fileManagerRecent, + fileManagerPinned, + fileManagerShortcuts, + dismissedAlerts, +} from "../db/schema.js"; +import { eq, and } from "drizzle-orm"; +import bcrypt from "bcryptjs"; +import { nanoid } from "nanoid"; +import jwt from "jsonwebtoken"; +import speakeasy from "speakeasy"; +import QRCode from "qrcode"; +import type { Request, Response, NextFunction } from "express"; +import { authLogger, apiLogger } from "../../utils/logger.js"; + +async function verifyOIDCToken( + idToken: string, + issuerUrl: string, + clientId: string, +): Promise { + try { + const normalizedIssuerUrl = issuerUrl.endsWith("/") + ? issuerUrl.slice(0, -1) + : issuerUrl; + const possibleIssuers = [ + issuerUrl, + normalizedIssuerUrl, + issuerUrl.replace(/\/application\/o\/[^\/]+$/, ""), + normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, ""), + ]; + + const jwksUrls = [ + `${normalizedIssuerUrl}/.well-known/jwks.json`, + `${normalizedIssuerUrl}/jwks/`, + `${normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, "")}/.well-known/jwks.json`, + ]; -async function verifyOIDCToken(idToken: string, issuerUrl: string, clientId: string): Promise { try { - const normalizedIssuerUrl = issuerUrl.endsWith('/') ? issuerUrl.slice(0, -1) : issuerUrl; - const possibleIssuers = [ - issuerUrl, - normalizedIssuerUrl, - issuerUrl.replace(/\/application\/o\/[^\/]+$/, ''), - normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '') - ]; - - const jwksUrls = [ - `${normalizedIssuerUrl}/.well-known/jwks.json`, - `${normalizedIssuerUrl}/jwks/`, - `${normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, '')}/.well-known/jwks.json` - ]; - - try { - const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; - const discoveryResponse = await fetch(discoveryUrl); - if (discoveryResponse.ok) { - const discovery = await discoveryResponse.json() as any; - if (discovery.jwks_uri) { - jwksUrls.unshift(discovery.jwks_uri); - } - } - } catch (discoveryError) { - logger.error(`OIDC discovery failed: ${discoveryError}`); + const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; + const discoveryResponse = await fetch(discoveryUrl); + if (discoveryResponse.ok) { + const discovery = (await discoveryResponse.json()) as any; + if (discovery.jwks_uri) { + jwksUrls.unshift(discovery.jwks_uri); } - - let jwks: any = null; - let jwksUrl: string | null = null; - - for (const url of jwksUrls) { - try { - const response = await fetch(url); - if (response.ok) { - const jwksData = await response.json() as any; - if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) { - jwks = jwksData; - jwksUrl = url; - break; - } else { - logger.error(`Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`); - } - } else { - logger.error(`JWKS fetch failed from ${url}: ${response.status} ${response.statusText}`); - } - } catch (error) { - logger.error(`JWKS fetch error from ${url}:`, error); - continue; - } - } - - if (!jwks) { - throw new Error('Failed to fetch JWKS from any URL'); - } - - if (!jwks.keys || !Array.isArray(jwks.keys)) { - throw new Error(`Invalid JWKS response structure. Expected 'keys' array, got: ${JSON.stringify(jwks)}`); - } - - const header = JSON.parse(Buffer.from(idToken.split('.')[0], 'base64').toString()); - const keyId = header.kid; - - const publicKey = jwks.keys.find((key: any) => key.kid === keyId); - if (!publicKey) { - throw new Error(`No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: any) => k.kid).join(', ')}`); - } - - const {importJWK, jwtVerify} = await import('jose'); - const key = await importJWK(publicKey); - - const {payload} = await jwtVerify(idToken, key, { - issuer: possibleIssuers, - audience: clientId, - }); - - return payload; - } catch (error) { - logger.error('OIDC token verification failed:', error); - throw error; + } + } catch (discoveryError) { + authLogger.error(`OIDC discovery failed: ${discoveryError}`); } + + let jwks: any = null; + let jwksUrl: string | null = null; + + for (const url of jwksUrls) { + try { + const response = await fetch(url); + if (response.ok) { + const jwksData = (await response.json()) as any; + if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) { + jwks = jwksData; + jwksUrl = url; + break; + } else { + authLogger.error( + `Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`, + ); + } + } else { + authLogger.error( + `JWKS fetch failed from ${url}: ${response.status} ${response.statusText}`, + ); + } + } catch (error) { + authLogger.error(`JWKS fetch error from ${url}:`, error); + continue; + } + } + + if (!jwks) { + throw new Error("Failed to fetch JWKS from any URL"); + } + + if (!jwks.keys || !Array.isArray(jwks.keys)) { + throw new Error( + `Invalid JWKS response structure. Expected 'keys' array, got: ${JSON.stringify(jwks)}`, + ); + } + + const header = JSON.parse( + Buffer.from(idToken.split(".")[0], "base64").toString(), + ); + const keyId = header.kid; + + const publicKey = jwks.keys.find((key: any) => key.kid === keyId); + if (!publicKey) { + throw new Error( + `No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: any) => k.kid).join(", ")}`, + ); + } + + const { importJWK, jwtVerify } = await import("jose"); + const key = await importJWK(publicKey); + + const { payload } = await jwtVerify(idToken, key, { + issuer: possibleIssuers, + audience: clientId, + }); + + return payload; + } catch (error) { + authLogger.error("OIDC token verification failed:", error); + throw error; + } } -const dbIconSymbol = '🗄️'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${dbIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; - const router = express.Router(); function isNonEmptyString(val: any): val is string { - return typeof val === 'string' && val.trim().length > 0; + return typeof val === "string" && val.trim().length > 0; } interface JWTPayload { - userId: string; - iat?: number; - exp?: number; + userId: string; + iat?: number; + exp?: number; } // JWT authentication middleware function authenticateJWT(req: Request, res: Response, next: NextFunction) { - const authHeader = req.headers['authorization']; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - logger.warn('Missing or invalid Authorization header'); - return res.status(401).json({error: 'Missing or invalid Authorization header'}); - } - const token = authHeader.split(' ')[1]; - const jwtSecret = process.env.JWT_SECRET || 'secret'; - try { - const payload = jwt.verify(token, jwtSecret) as JWTPayload; - (req as any).userId = payload.userId; - next(); - } catch (err) { - logger.warn('Invalid or expired token'); - return res.status(401).json({error: 'Invalid or expired token'}); - } + const authHeader = req.headers["authorization"]; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + authLogger.warn("Missing or invalid Authorization header", { + operation: "auth", + method: req.method, + url: req.url, + }); + return res + .status(401) + .json({ error: "Missing or invalid Authorization header" }); + } + const token = authHeader.split(" ")[1]; + const jwtSecret = process.env.JWT_SECRET || "secret"; + try { + const payload = jwt.verify(token, jwtSecret) as JWTPayload; + (req as any).userId = payload.userId; + next(); + } catch (err) { + authLogger.warn("Invalid or expired token", { + operation: "auth", + method: req.method, + url: req.url, + error: err, + }); + return res.status(401).json({ error: "Invalid or expired token" }); + } } // Route: Create traditional user (username/password) // POST /users/create -router.post('/create', async (req, res) => { +router.post("/create", async (req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'allow_registration'") + .get(); + if (row && (row as any).value !== "true") { + return res + .status(403) + .json({ error: "Registration is currently disabled" }); + } + } catch (e) { + authLogger.warn("Failed to check registration status", { + operation: "registration_check", + error: e, + }); + } + + const { username, password } = req.body; + + if (!isNonEmptyString(username) || !isNonEmptyString(password)) { + authLogger.warn( + "Invalid user creation attempt - missing username or password", + { + operation: "user_create", + hasUsername: !!username, + hasPassword: !!password, + }, + ); + return res + .status(400) + .json({ error: "Username and password are required" }); + } + + try { + const existing = await db + .select() + .from(users) + .where(eq(users.username, username)); + if (existing && existing.length > 0) { + authLogger.warn(`Attempt to create duplicate username: ${username}`, { + operation: "user_create", + username, + }); + return res.status(409).json({ error: "Username already exists" }); + } + + let isFirstUser = false; try { - const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'allow_registration'").get(); - if (row && (row as any).value !== 'true') { - return res.status(403).json({error: 'Registration is currently disabled'}); - } + const countResult = db.$client + .prepare("SELECT COUNT(*) as count FROM users") + .get(); + isFirstUser = ((countResult as any)?.count || 0) === 0; } catch (e) { + isFirstUser = true; + authLogger.warn("Failed to check user count, assuming first user", { + operation: "user_create", + username, + error: e, + }); } - const {username, password} = req.body; + const saltRounds = parseInt(process.env.SALT || "10", 10); + const password_hash = await bcrypt.hash(password, saltRounds); + const id = nanoid(); + await db.insert(users).values({ + id, + username, + password_hash, + is_admin: isFirstUser, + is_oidc: false, + client_id: "", + client_secret: "", + issuer_url: "", + authorization_url: "", + token_url: "", + identifier_path: "", + name_path: "", + scopes: "openid email profile", + totp_secret: null, + totp_enabled: false, + totp_backup_codes: null, + }); - if (!isNonEmptyString(username) || !isNonEmptyString(password)) { - logger.warn('Invalid user creation attempt - missing username or password'); - return res.status(400).json({error: 'Username and password are required'}); - } - - try { - const existing = await db - .select() - .from(users) - .where(eq(users.username, username)); - if (existing && existing.length > 0) { - logger.warn(`Attempt to create duplicate username: ${username}`); - return res.status(409).json({error: 'Username already exists'}); - } - - let isFirstUser = false; - try { - const countResult = db.$client.prepare('SELECT COUNT(*) as count FROM users').get(); - isFirstUser = ((countResult as any)?.count || 0) === 0; - } catch (e) { - isFirstUser = true; - } - - const saltRounds = parseInt(process.env.SALT || '10', 10); - const password_hash = await bcrypt.hash(password, saltRounds); - const id = nanoid(); - - await db.insert(users).values({ - id, - username, - password_hash, - is_admin: isFirstUser, - is_oidc: false, - client_id: '', - client_secret: '', - issuer_url: '', - authorization_url: '', - token_url: '', - identifier_path: '', - name_path: '', - scopes: 'openid email profile', - totp_secret: null, - totp_enabled: false, - totp_backup_codes: null, - }); - - logger.success(`Traditional user created: ${username} (is_admin: ${isFirstUser})`); - res.json({message: 'User created', is_admin: isFirstUser}); - } catch (err) { - logger.error('Failed to create user', err); - res.status(500).json({error: 'Failed to create user'}); - } + authLogger.success( + `Traditional user created: ${username} (is_admin: ${isFirstUser})`, + { + operation: "user_create", + username, + isAdmin: isFirstUser, + userId: id, + }, + ); + res.json({ + message: "User created", + is_admin: isFirstUser, + toast: { type: "success", message: `User created: ${username}` }, + }); + } catch (err) { + authLogger.error("Failed to create user", err); + res.status(500).json({ error: "Failed to create user" }); + } }); // Route: Create OIDC provider configuration (admin only) // POST /users/oidc-config -router.post('/oidc-config', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0 || !user[0].is_admin) { - return res.status(403).json({error: 'Not authorized'}); - } - - const { - client_id, - client_secret, - issuer_url, - authorization_url, - token_url, - userinfo_url, - identifier_path, - name_path, - scopes - } = req.body; - - if (!isNonEmptyString(client_id) || !isNonEmptyString(client_secret) || - !isNonEmptyString(issuer_url) || !isNonEmptyString(authorization_url) || - !isNonEmptyString(token_url) || !isNonEmptyString(identifier_path) || - !isNonEmptyString(name_path)) { - return res.status(400).json({error: 'All OIDC configuration fields are required'}); - } - - const config = { - client_id, - client_secret, - issuer_url, - authorization_url, - token_url, - userinfo_url: userinfo_url || '', - identifier_path, - name_path, - scopes: scopes || 'openid email profile' - }; - - db.$client.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('oidc_config', ?)").run(JSON.stringify(config)); - - res.json({message: 'OIDC configuration updated'}); - } catch (err) { - logger.error('Failed to update OIDC config', err); - res.status(500).json({error: 'Failed to update OIDC config'}); +router.post("/oidc-config", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); } + + const { + client_id, + client_secret, + issuer_url, + authorization_url, + token_url, + userinfo_url, + identifier_path, + name_path, + scopes, + } = req.body; + + const isDisableRequest = + (client_id === "" || client_id === null || client_id === undefined) && + (client_secret === "" || + client_secret === null || + client_secret === undefined) && + (issuer_url === "" || issuer_url === null || issuer_url === undefined) && + (authorization_url === "" || + authorization_url === null || + authorization_url === undefined) && + (token_url === "" || token_url === null || token_url === undefined); + + const isEnableRequest = + isNonEmptyString(client_id) && + isNonEmptyString(client_secret) && + isNonEmptyString(issuer_url) && + isNonEmptyString(authorization_url) && + isNonEmptyString(token_url) && + isNonEmptyString(identifier_path) && + isNonEmptyString(name_path); + + if (!isDisableRequest && !isEnableRequest) { + authLogger.warn( + "OIDC validation failed - neither disable nor enable request", + { + operation: "oidc_config_update", + userId, + isDisableRequest, + isEnableRequest, + }, + ); + return res + .status(400) + .json({ error: "All OIDC configuration fields are required" }); + } + + if (isDisableRequest) { + db.$client + .prepare("DELETE FROM settings WHERE key = 'oidc_config'") + .run(); + authLogger.info("OIDC configuration disabled", { + operation: "oidc_disable", + userId, + }); + res.json({ message: "OIDC configuration disabled" }); + } else { + const config = { + client_id, + client_secret, + issuer_url, + authorization_url, + token_url, + userinfo_url: userinfo_url || "", + identifier_path, + name_path, + scopes: scopes || "openid email profile", + }; + + db.$client + .prepare( + "INSERT OR REPLACE INTO settings (key, value) VALUES ('oidc_config', ?)", + ) + .run(JSON.stringify(config)); + authLogger.info("OIDC configuration updated", { + operation: "oidc_update", + userId, + hasUserinfoUrl: !!userinfo_url, + }); + res.json({ message: "OIDC configuration updated" }); + } + } catch (err) { + authLogger.error("Failed to update OIDC config", err); + res.status(500).json({ error: "Failed to update OIDC config" }); + } +}); + +// Route: Disable OIDC configuration (admin only) +// DELETE /users/oidc-config +router.delete("/oidc-config", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); + } + + db.$client.prepare("DELETE FROM settings WHERE key = 'oidc_config'").run(); + authLogger.success("OIDC configuration disabled", { + operation: "oidc_disable", + userId, + }); + res.json({ message: "OIDC configuration disabled" }); + } catch (err) { + authLogger.error("Failed to disable OIDC config", err); + res.status(500).json({ error: "Failed to disable OIDC config" }); + } }); // Route: Get OIDC configuration // GET /users/oidc-config -router.get('/oidc-config', async (req, res) => { - try { - const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'oidc_config'").get(); - if (!row) { - return res.status(404).json({error: 'OIDC not configured'}); - } - res.json(JSON.parse((row as any).value)); - } catch (err) { - logger.error('Failed to get OIDC config', err); - res.status(500).json({error: 'Failed to get OIDC config'}); +router.get("/oidc-config", async (req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") + .get(); + if (!row) { + return res.json(null); } + res.json(JSON.parse((row as any).value)); + } catch (err) { + authLogger.error("Failed to get OIDC config", err); + res.status(500).json({ error: "Failed to get OIDC config" }); + } }); // Route: Get OIDC authorization URL // GET /users/oidc/authorize -router.get('/oidc/authorize', async (req, res) => { - try { - const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'oidc_config'").get(); - if (!row) { - return res.status(404).json({error: 'OIDC not configured'}); - } - - const config = JSON.parse((row as any).value); - const state = nanoid(); - const nonce = nanoid(); - - let origin = req.get('Origin') || req.get('Referer')?.replace(/\/[^\/]*$/, '') || 'http://localhost:5173'; - - if (origin.includes('localhost')) { - origin = 'http://localhost:8081'; - } - - const redirectUri = `${origin}/users/oidc/callback`; - - db.$client.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(`oidc_state_${state}`, nonce); - - db.$client.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(`oidc_redirect_${state}`, redirectUri); - - const authUrl = new URL(config.authorization_url); - authUrl.searchParams.set('client_id', config.client_id); - authUrl.searchParams.set('redirect_uri', redirectUri); - authUrl.searchParams.set('response_type', 'code'); - authUrl.searchParams.set('scope', config.scopes); - authUrl.searchParams.set('state', state); - authUrl.searchParams.set('nonce', nonce); - - res.json({auth_url: authUrl.toString(), state, nonce}); - } catch (err) { - logger.error('Failed to generate OIDC auth URL', err); - res.status(500).json({error: 'Failed to generate authorization URL'}); +router.get("/oidc/authorize", async (req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") + .get(); + if (!row) { + return res.status(404).json({ error: "OIDC not configured" }); } + + const config = JSON.parse((row as any).value); + const state = nanoid(); + const nonce = nanoid(); + + let origin = + req.get("Origin") || + req.get("Referer")?.replace(/\/[^\/]*$/, "") || + "http://localhost:5173"; + + if (origin.includes("localhost")) { + origin = "http://localhost:8081"; + } + + const redirectUri = `${origin}/users/oidc/callback`; + + db.$client + .prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") + .run(`oidc_state_${state}`, nonce); + + db.$client + .prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") + .run(`oidc_redirect_${state}`, redirectUri); + + const authUrl = new URL(config.authorization_url); + authUrl.searchParams.set("client_id", config.client_id); + authUrl.searchParams.set("redirect_uri", redirectUri); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("scope", config.scopes); + authUrl.searchParams.set("state", state); + authUrl.searchParams.set("nonce", nonce); + + res.json({ auth_url: authUrl.toString(), state, nonce }); + } catch (err) { + authLogger.error("Failed to generate OIDC auth URL", err); + res.status(500).json({ error: "Failed to generate authorization URL" }); + } }); // Route: OIDC callback - exchange code for token and create/login user // GET /users/oidc/callback -router.get('/oidc/callback', async (req, res) => { - const {code, state} = req.query; +router.get("/oidc/callback", async (req, res) => { + const { code, state } = req.query; - if (!isNonEmptyString(code) || !isNonEmptyString(state)) { - return res.status(400).json({error: 'Code and state are required'}); + if (!isNonEmptyString(code) || !isNonEmptyString(state)) { + return res.status(400).json({ error: "Code and state are required" }); + } + + const storedRedirectRow = db.$client + .prepare("SELECT value FROM settings WHERE key = ?") + .get(`oidc_redirect_${state}`); + if (!storedRedirectRow) { + return res + .status(400) + .json({ error: "Invalid state parameter - redirect URI not found" }); + } + const redirectUri = (storedRedirectRow as any).value; + + try { + const storedNonce = db.$client + .prepare("SELECT value FROM settings WHERE key = ?") + .get(`oidc_state_${state}`); + if (!storedNonce) { + return res.status(400).json({ error: "Invalid state parameter" }); } - const storedRedirectRow = db.$client.prepare("SELECT value FROM settings WHERE key = ?").get(`oidc_redirect_${state}`); - if (!storedRedirectRow) { - return res.status(400).json({error: 'Invalid state parameter - redirect URI not found'}); + db.$client + .prepare("DELETE FROM settings WHERE key = ?") + .run(`oidc_state_${state}`); + db.$client + .prepare("DELETE FROM settings WHERE key = ?") + .run(`oidc_redirect_${state}`); + + const configRow = db.$client + .prepare("SELECT value FROM settings WHERE key = 'oidc_config'") + .get(); + if (!configRow) { + return res.status(500).json({ error: "OIDC not configured" }); } - const redirectUri = (storedRedirectRow as any).value; + + const config = JSON.parse((configRow as any).value); + + const tokenResponse = await fetch(config.token_url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: config.client_id, + client_secret: config.client_secret, + code: code, + redirect_uri: redirectUri, + }), + }); + + if (!tokenResponse.ok) { + authLogger.error( + "OIDC token exchange failed", + await tokenResponse.text(), + ); + return res + .status(400) + .json({ error: "Failed to exchange authorization code" }); + } + + const tokenData = (await tokenResponse.json()) as any; + + let userInfo: any = null; + let userInfoUrls: string[] = []; + + const normalizedIssuerUrl = config.issuer_url.endsWith("/") + ? config.issuer_url.slice(0, -1) + : config.issuer_url; + const baseUrl = normalizedIssuerUrl.replace( + /\/application\/o\/[^\/]+$/, + "", + ); try { - const storedNonce = db.$client.prepare("SELECT value FROM settings WHERE key = ?").get(`oidc_state_${state}`); - if (!storedNonce) { - return res.status(400).json({error: 'Invalid state parameter'}); + const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; + const discoveryResponse = await fetch(discoveryUrl); + if (discoveryResponse.ok) { + const discovery = (await discoveryResponse.json()) as any; + if (discovery.userinfo_endpoint) { + userInfoUrls.push(discovery.userinfo_endpoint); } - - db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`oidc_state_${state}`); - db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`oidc_redirect_${state}`); - - const configRow = db.$client.prepare("SELECT value FROM settings WHERE key = 'oidc_config'").get(); - if (!configRow) { - return res.status(500).json({error: 'OIDC not configured'}); - } - - const config = JSON.parse((configRow as any).value); - - const tokenResponse = await fetch(config.token_url, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'authorization_code', - client_id: config.client_id, - client_secret: config.client_secret, - code: code, - redirect_uri: redirectUri, - }), - }); - - if (!tokenResponse.ok) { - logger.error('OIDC token exchange failed', await tokenResponse.text()); - return res.status(400).json({error: 'Failed to exchange authorization code'}); - } - - const tokenData = await tokenResponse.json() as any; - - let userInfo: any = null; - let userInfoUrls: string[] = []; - - const normalizedIssuerUrl = config.issuer_url.endsWith('/') ? config.issuer_url.slice(0, -1) : config.issuer_url; - const baseUrl = normalizedIssuerUrl.replace(/\/application\/o\/[^\/]+$/, ''); - - try { - const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`; - const discoveryResponse = await fetch(discoveryUrl); - if (discoveryResponse.ok) { - const discovery = await discoveryResponse.json() as any; - if (discovery.userinfo_endpoint) { - userInfoUrls.push(discovery.userinfo_endpoint); - } - } - } catch (discoveryError) { - logger.error(`OIDC discovery failed: ${discoveryError}`); - } - - if (config.userinfo_url) { - userInfoUrls.unshift(config.userinfo_url); - } - - userInfoUrls.push( - `${baseUrl}/userinfo/`, - `${baseUrl}/userinfo`, - `${normalizedIssuerUrl}/userinfo/`, - `${normalizedIssuerUrl}/userinfo`, - `${baseUrl}/oauth2/userinfo/`, - `${baseUrl}/oauth2/userinfo`, - `${normalizedIssuerUrl}/oauth2/userinfo/`, - `${normalizedIssuerUrl}/oauth2/userinfo` - ); - - if (tokenData.id_token) { - try { - userInfo = await verifyOIDCToken(tokenData.id_token, config.issuer_url, config.client_id); - logger.info('Successfully verified ID token and extracted user info'); - } catch (error) { - logger.error('OIDC token verification failed, trying userinfo endpoints', error); - try { - const parts = tokenData.id_token.split('.'); - if (parts.length === 3) { - const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString()); - userInfo = payload; - logger.info('Successfully decoded ID token payload without verification'); - } - } catch (decodeError) { - logger.error('Failed to decode ID token payload:', decodeError); - } - } - } - - if (!userInfo && tokenData.access_token) { - for (const userInfoUrl of userInfoUrls) { - try { - const userInfoResponse = await fetch(userInfoUrl, { - headers: { - 'Authorization': `Bearer ${tokenData.access_token}`, - } - }); - - if (userInfoResponse.ok) { - userInfo = await userInfoResponse.json(); - break; - } else { - logger.error(`Userinfo endpoint ${userInfoUrl} failed with status: ${userInfoResponse.status}`); - } - } catch (error) { - logger.error(`Userinfo endpoint ${userInfoUrl} failed:`, error); - continue; - } - } - } - - if (!userInfo) { - logger.error('Failed to get user information from all sources'); - logger.error(`Tried userinfo URLs: ${userInfoUrls.join(', ')}`); - logger.error(`Token data keys: ${Object.keys(tokenData).join(', ')}`); - logger.error(`Has id_token: ${!!tokenData.id_token}`); - logger.error(`Has access_token: ${!!tokenData.access_token}`); - return res.status(400).json({error: 'Failed to get user information'}); - } - - const getNestedValue = (obj: any, path: string): any => { - if (!path || !obj) return null; - return path.split('.').reduce((current, key) => current?.[key], obj); - }; - - const identifier = getNestedValue(userInfo, config.identifier_path) || - userInfo[config.identifier_path] || - userInfo.sub || - userInfo.email || - userInfo.preferred_username; - - const name = getNestedValue(userInfo, config.name_path) || - userInfo[config.name_path] || - userInfo.name || - userInfo.given_name || - identifier; - - if (!identifier) { - logger.error(`Identifier not found at path: ${config.identifier_path}`); - logger.error(`Available fields: ${Object.keys(userInfo).join(', ')}`); - return res.status(400).json({error: `User identifier not found at path: ${config.identifier_path}. Available fields: ${Object.keys(userInfo).join(', ')}`}); - } - - let user = await db - .select() - .from(users) - .where(and(eq(users.is_oidc, true), eq(users.oidc_identifier, identifier))); - - let isFirstUser = false; - if (!user || user.length === 0) { - try { - const countResult = db.$client.prepare('SELECT COUNT(*) as count FROM users').get(); - isFirstUser = ((countResult as any)?.count || 0) === 0; - } catch (e) { - isFirstUser = true; - } - - const id = nanoid(); - await db.insert(users).values({ - id, - username: name, - password_hash: '', - is_admin: isFirstUser, - is_oidc: true, - oidc_identifier: identifier, - client_id: config.client_id, - client_secret: config.client_secret, - issuer_url: config.issuer_url, - authorization_url: config.authorization_url, - token_url: config.token_url, - identifier_path: config.identifier_path, - name_path: config.name_path, - scopes: config.scopes, - }); - - user = await db - .select() - .from(users) - .where(eq(users.id, id)); - } else { - await db.update(users) - .set({username: name}) - .where(eq(users.id, user[0].id)); - - user = await db - .select() - .from(users) - .where(eq(users.id, user[0].id)); - } - - const userRecord = user[0]; - - const jwtSecret = process.env.JWT_SECRET || 'secret'; - const token = jwt.sign({userId: userRecord.id}, jwtSecret, { - expiresIn: '50d', - }); - - let frontendUrl = redirectUri.replace('/users/oidc/callback', ''); - - if (frontendUrl.includes('localhost')) { - frontendUrl = 'http://localhost:5173'; - } - - const redirectUrl = new URL(frontendUrl); - redirectUrl.searchParams.set('success', 'true'); - redirectUrl.searchParams.set('token', token); - - res.redirect(redirectUrl.toString()); - - } catch (err) { - logger.error('OIDC callback failed', err); - - let frontendUrl = redirectUri.replace('/users/oidc/callback', ''); - - if (frontendUrl.includes('localhost')) { - frontendUrl = 'http://localhost:5173'; - } - - const redirectUrl = new URL(frontendUrl); - redirectUrl.searchParams.set('error', 'OIDC authentication failed'); - - res.redirect(redirectUrl.toString()); + } + } catch (discoveryError) { + authLogger.error(`OIDC discovery failed: ${discoveryError}`); } + + if (config.userinfo_url) { + userInfoUrls.unshift(config.userinfo_url); + } + + userInfoUrls.push( + `${baseUrl}/userinfo/`, + `${baseUrl}/userinfo`, + `${normalizedIssuerUrl}/userinfo/`, + `${normalizedIssuerUrl}/userinfo`, + `${baseUrl}/oauth2/userinfo/`, + `${baseUrl}/oauth2/userinfo`, + `${normalizedIssuerUrl}/oauth2/userinfo/`, + `${normalizedIssuerUrl}/oauth2/userinfo`, + ); + + if (tokenData.id_token) { + try { + userInfo = await verifyOIDCToken( + tokenData.id_token, + config.issuer_url, + config.client_id, + ); + } catch (error) { + authLogger.error( + "OIDC token verification failed, trying userinfo endpoints", + error, + ); + try { + const parts = tokenData.id_token.split("."); + if (parts.length === 3) { + const payload = JSON.parse( + Buffer.from(parts[1], "base64").toString(), + ); + userInfo = payload; + } + } catch (decodeError) { + authLogger.error("Failed to decode ID token payload:", decodeError); + } + } + } + + if (!userInfo && tokenData.access_token) { + for (const userInfoUrl of userInfoUrls) { + try { + const userInfoResponse = await fetch(userInfoUrl, { + headers: { + Authorization: `Bearer ${tokenData.access_token}`, + }, + }); + + if (userInfoResponse.ok) { + userInfo = await userInfoResponse.json(); + break; + } else { + authLogger.error( + `Userinfo endpoint ${userInfoUrl} failed with status: ${userInfoResponse.status}`, + ); + } + } catch (error) { + authLogger.error(`Userinfo endpoint ${userInfoUrl} failed:`, error); + continue; + } + } + } + + if (!userInfo) { + authLogger.error("Failed to get user information from all sources"); + authLogger.error(`Tried userinfo URLs: ${userInfoUrls.join(", ")}`); + authLogger.error(`Token data keys: ${Object.keys(tokenData).join(", ")}`); + authLogger.error(`Has id_token: ${!!tokenData.id_token}`); + authLogger.error(`Has access_token: ${!!tokenData.access_token}`); + return res.status(400).json({ error: "Failed to get user information" }); + } + + const getNestedValue = (obj: any, path: string): any => { + if (!path || !obj) return null; + return path.split(".").reduce((current, key) => current?.[key], obj); + }; + + const identifier = + getNestedValue(userInfo, config.identifier_path) || + userInfo[config.identifier_path] || + userInfo.sub || + userInfo.email || + userInfo.preferred_username; + + const name = + getNestedValue(userInfo, config.name_path) || + userInfo[config.name_path] || + userInfo.name || + userInfo.given_name || + identifier; + + if (!identifier) { + authLogger.error( + `Identifier not found at path: ${config.identifier_path}`, + ); + authLogger.error(`Available fields: ${Object.keys(userInfo).join(", ")}`); + return res.status(400).json({ + error: `User identifier not found at path: ${config.identifier_path}. Available fields: ${Object.keys(userInfo).join(", ")}`, + }); + } + + let user = await db + .select() + .from(users) + .where( + and(eq(users.is_oidc, true), eq(users.oidc_identifier, identifier)), + ); + + let isFirstUser = false; + if (!user || user.length === 0) { + try { + const countResult = db.$client + .prepare("SELECT COUNT(*) as count FROM users") + .get(); + isFirstUser = ((countResult as any)?.count || 0) === 0; + } catch (e) { + isFirstUser = true; + } + + const id = nanoid(); + await db.insert(users).values({ + id, + username: name, + password_hash: "", + is_admin: isFirstUser, + is_oidc: true, + oidc_identifier: identifier, + client_id: config.client_id, + client_secret: config.client_secret, + issuer_url: config.issuer_url, + authorization_url: config.authorization_url, + token_url: config.token_url, + identifier_path: config.identifier_path, + name_path: config.name_path, + scopes: config.scopes, + }); + + user = await db.select().from(users).where(eq(users.id, id)); + } else { + await db + .update(users) + .set({ username: name }) + .where(eq(users.id, user[0].id)); + + user = await db.select().from(users).where(eq(users.id, user[0].id)); + } + + const userRecord = user[0]; + + const jwtSecret = process.env.JWT_SECRET || "secret"; + const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { + expiresIn: "50d", + }); + + let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); + + if (frontendUrl.includes("localhost")) { + frontendUrl = "http://localhost:5173"; + } + + const redirectUrl = new URL(frontendUrl); + redirectUrl.searchParams.set("success", "true"); + redirectUrl.searchParams.set("token", token); + + res.redirect(redirectUrl.toString()); + } catch (err) { + authLogger.error("OIDC callback failed", err); + + let frontendUrl = redirectUri.replace("/users/oidc/callback", ""); + + if (frontendUrl.includes("localhost")) { + frontendUrl = "http://localhost:5173"; + } + + const redirectUrl = new URL(frontendUrl); + redirectUrl.searchParams.set("error", "OIDC authentication failed"); + + res.redirect(redirectUrl.toString()); + } }); // Route: Get user JWT by username and password (traditional login) // POST /users/login -router.post('/login', async (req, res) => { - const {username, password} = req.body; +router.post("/login", async (req, res) => { + const { username, password } = req.body; - if (!isNonEmptyString(username) || !isNonEmptyString(password)) { - logger.warn('Invalid traditional login attempt'); - return res.status(400).json({error: 'Invalid username or password'}); + if (!isNonEmptyString(username) || !isNonEmptyString(password)) { + authLogger.warn("Invalid traditional login attempt", { + operation: "user_login", + hasUsername: !!username, + hasPassword: !!password, + }); + return res.status(400).json({ error: "Invalid username or password" }); + } + + try { + const user = await db + .select() + .from(users) + .where(eq(users.username, username)); + + if (!user || user.length === 0) { + authLogger.warn(`User not found: ${username}`, { + operation: "user_login", + username, + }); + return res.status(404).json({ error: "User not found" }); } - try { - const user = await db - .select() - .from(users) - .where(eq(users.username, username)); + const userRecord = user[0]; - if (!user || user.length === 0) { - logger.warn(`User not found: ${username}`); - return res.status(404).json({error: 'User not found'}); - } - - const userRecord = user[0]; - - if (userRecord.is_oidc) { - return res.status(403).json({error: 'This user uses external authentication'}); - } - - const isMatch = await bcrypt.compare(password, userRecord.password_hash); - if (!isMatch) { - logger.warn(`Incorrect password for user: ${username}`); - return res.status(401).json({error: 'Incorrect password'}); - } - - const jwtSecret = process.env.JWT_SECRET || 'secret'; - const token = jwt.sign({userId: userRecord.id}, jwtSecret, { - expiresIn: '50d', - }); - - if (userRecord.totp_enabled) { - return res.json({ - requires_totp: true, - temp_token: jwt.sign( - {userId: userRecord.id, pending_totp: true}, - jwtSecret, - {expiresIn: '10m'} - ) - }); - } - - return res.json({ - token, - is_admin: !!userRecord.is_admin, - username: userRecord.username - }); - - } catch (err) { - logger.error('Failed to log in user', err); - return res.status(500).json({error: 'Login failed'}); + if (userRecord.is_oidc) { + authLogger.warn("OIDC user attempted traditional login", { + operation: "user_login", + username, + userId: userRecord.id, + }); + return res + .status(403) + .json({ error: "This user uses external authentication" }); } + + const isMatch = await bcrypt.compare(password, userRecord.password_hash); + if (!isMatch) { + authLogger.warn(`Incorrect password for user: ${username}`, { + operation: "user_login", + username, + userId: userRecord.id, + }); + return res.status(401).json({ error: "Incorrect password" }); + } + const jwtSecret = process.env.JWT_SECRET || "secret"; + const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { + expiresIn: "50d", + }); + + if (userRecord.totp_enabled) { + const tempToken = jwt.sign( + { userId: userRecord.id, pending_totp: true }, + jwtSecret, + { expiresIn: "10m" }, + ); + return res.json({ + requires_totp: true, + temp_token: tempToken, + }); + } + return res.json({ + token, + is_admin: !!userRecord.is_admin, + username: userRecord.username, + }); + } catch (err) { + authLogger.error("Failed to log in user", err); + return res.status(500).json({ error: "Login failed" }); + } }); // Route: Get current user's info using JWT // GET /users/me -router.get('/me', authenticateJWT, async (req: Request, res: Response) => { - const userId = (req as any).userId; - if (!isNonEmptyString(userId)) { - logger.warn('Invalid userId in JWT for /users/me'); - return res.status(401).json({error: 'Invalid userId'}); - } - try { - const user = await db - .select() - .from(users) - .where(eq(users.id, userId)); - if (!user || user.length === 0) { - logger.warn(`User not found for /users/me: ${userId}`); - return res.status(401).json({error: 'User not found'}); - } - res.json({ - userId: user[0].id, - username: user[0].username, - is_admin: !!user[0].is_admin, - is_oidc: !!user[0].is_oidc, - totp_enabled: !!user[0].totp_enabled - }); - } catch (err) { - logger.error('Failed to get username', err); - res.status(500).json({error: 'Failed to get username'}); +router.get("/me", authenticateJWT, async (req: Request, res: Response) => { + const userId = (req as any).userId; + if (!isNonEmptyString(userId)) { + authLogger.warn("Invalid userId in JWT for /users/me"); + return res.status(401).json({ error: "Invalid userId" }); + } + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0) { + authLogger.warn(`User not found for /users/me: ${userId}`); + return res.status(401).json({ error: "User not found" }); } + res.json({ + userId: user[0].id, + username: user[0].username, + is_admin: !!user[0].is_admin, + is_oidc: !!user[0].is_oidc, + totp_enabled: !!user[0].totp_enabled, + }); + } catch (err) { + authLogger.error("Failed to get username", err); + res.status(500).json({ error: "Failed to get username" }); + } }); // Route: Count users // GET /users/count -router.get('/count', async (req, res) => { - try { - const countResult = db.$client.prepare('SELECT COUNT(*) as count FROM users').get(); - const count = (countResult as any)?.count || 0; - res.json({count}); - } catch (err) { - logger.error('Failed to count users', err); - res.status(500).json({error: 'Failed to count users'}); - } +router.get("/count", async (req, res) => { + try { + const countResult = db.$client + .prepare("SELECT COUNT(*) as count FROM users") + .get(); + const count = (countResult as any)?.count || 0; + res.json({ count }); + } catch (err) { + authLogger.error("Failed to count users", err); + res.status(500).json({ error: "Failed to count users" }); + } }); // Route: DB health check (actually queries DB) // GET /users/db-health -router.get('/db-health', async (req, res) => { - try { - db.$client.prepare('SELECT 1').get(); - res.json({status: 'ok'}); - } catch (err) { - logger.error('DB health check failed', err); - res.status(500).json({error: 'Database not accessible'}); - } +router.get("/db-health", async (req, res) => { + try { + db.$client.prepare("SELECT 1").get(); + res.json({ status: "ok" }); + } catch (err) { + authLogger.error("DB health check failed", err); + res.status(500).json({ error: "Database not accessible" }); + } }); // Route: Get registration allowed status // GET /users/registration-allowed -router.get('/registration-allowed', async (req, res) => { - try { - const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'allow_registration'").get(); - res.json({allowed: row ? (row as any).value === 'true' : true}); - } catch (err) { - logger.error('Failed to get registration allowed', err); - res.status(500).json({error: 'Failed to get registration allowed'}); - } +router.get("/registration-allowed", async (req, res) => { + try { + const row = db.$client + .prepare("SELECT value FROM settings WHERE key = 'allow_registration'") + .get(); + res.json({ allowed: row ? (row as any).value === "true" : true }); + } catch (err) { + authLogger.error("Failed to get registration allowed", err); + res.status(500).json({ error: "Failed to get registration allowed" }); + } }); // Route: Set registration allowed status (admin only) // PATCH /users/registration-allowed -router.patch('/registration-allowed', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0 || !user[0].is_admin) { - return res.status(403).json({error: 'Not authorized'}); - } - const {allowed} = req.body; - if (typeof allowed !== 'boolean') { - return res.status(400).json({error: 'Invalid value for allowed'}); - } - db.$client.prepare("UPDATE settings SET value = ? WHERE key = 'allow_registration'").run(allowed ? 'true' : 'false'); - res.json({allowed}); - } catch (err) { - logger.error('Failed to set registration allowed', err); - res.status(500).json({error: 'Failed to set registration allowed'}); +router.patch("/registration-allowed", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); } + const { allowed } = req.body; + if (typeof allowed !== "boolean") { + return res.status(400).json({ error: "Invalid value for allowed" }); + } + db.$client + .prepare("UPDATE settings SET value = ? WHERE key = 'allow_registration'") + .run(allowed ? "true" : "false"); + res.json({ allowed }); + } catch (err) { + authLogger.error("Failed to set registration allowed", err); + res.status(500).json({ error: "Failed to set registration allowed" }); + } }); // Route: Delete user account // DELETE /users/delete-account -router.delete('/delete-account', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - const {password} = req.body; +router.delete("/delete-account", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + const { password } = req.body; - if (!isNonEmptyString(password)) { - return res.status(400).json({error: 'Password is required to delete account'}); + if (!isNonEmptyString(password)) { + return res + .status(400) + .json({ error: "Password is required to delete account" }); + } + + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0) { + return res.status(404).json({ error: "User not found" }); } - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0) { - return res.status(404).json({error: 'User not found'}); - } + const userRecord = user[0]; - const userRecord = user[0]; - - if (userRecord.is_oidc) { - return res.status(403).json({error: 'Cannot delete external authentication accounts through this endpoint'}); - } - - const isMatch = await bcrypt.compare(password, userRecord.password_hash); - if (!isMatch) { - logger.warn(`Incorrect password provided for account deletion: ${userRecord.username}`); - return res.status(401).json({error: 'Incorrect password'}); - } - - if (userRecord.is_admin) { - const adminCount = db.$client.prepare('SELECT COUNT(*) as count FROM users WHERE is_admin = 1').get(); - if ((adminCount as any)?.count <= 1) { - return res.status(403).json({error: 'Cannot delete the last admin user'}); - } - } - - await db.delete(users).where(eq(users.id, userId)); - - logger.success(`User account deleted: ${userRecord.username}`); - res.json({message: 'Account deleted successfully'}); - - } catch (err) { - logger.error('Failed to delete user account', err); - res.status(500).json({error: 'Failed to delete account'}); + if (userRecord.is_oidc) { + return res.status(403).json({ + error: + "Cannot delete external authentication accounts through this endpoint", + }); } + + const isMatch = await bcrypt.compare(password, userRecord.password_hash); + if (!isMatch) { + authLogger.warn( + `Incorrect password provided for account deletion: ${userRecord.username}`, + ); + return res.status(401).json({ error: "Incorrect password" }); + } + + if (userRecord.is_admin) { + const adminCount = db.$client + .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") + .get(); + if ((adminCount as any)?.count <= 1) { + return res + .status(403) + .json({ error: "Cannot delete the last admin user" }); + } + } + + await db.delete(users).where(eq(users.id, userId)); + + authLogger.success(`User account deleted: ${userRecord.username}`); + res.json({ message: "Account deleted successfully" }); + } catch (err) { + authLogger.error("Failed to delete user account", err); + res.status(500).json({ error: "Failed to delete account" }); + } }); // Route: Initiate password reset // POST /users/initiate-reset -router.post('/initiate-reset', async (req, res) => { - const {username} = req.body; +router.post("/initiate-reset", async (req, res) => { + const { username } = req.body; - if (!isNonEmptyString(username)) { - return res.status(400).json({error: 'Username is required'}); + if (!isNonEmptyString(username)) { + return res.status(400).json({ error: "Username is required" }); + } + + try { + const user = await db + .select() + .from(users) + .where(eq(users.username, username)); + + if (!user || user.length === 0) { + authLogger.warn( + `Password reset attempted for non-existent user: ${username}`, + ); + return res.status(404).json({ error: "User not found" }); } - try { - const user = await db - .select() - .from(users) - .where(eq(users.username, username)); - - if (!user || user.length === 0) { - logger.warn(`Password reset attempted for non-existent user: ${username}`); - return res.status(404).json({error: 'User not found'}); - } - - if (user[0].is_oidc) { - return res.status(403).json({error: 'Password reset not available for external authentication users'}); - } - - const resetCode = Math.floor(100000 + Math.random() * 900000).toString(); - const expiresAt = new Date(Date.now() + 15 * 60 * 1000); - - db.$client.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run( - `reset_code_${username}`, - JSON.stringify({code: resetCode, expiresAt: expiresAt.toISOString()}) - ); - - logger.info(`Password reset code for user ${username}: ${resetCode} (expires at ${expiresAt.toLocaleString()})`); - - res.json({message: 'Password reset code has been generated and logged. Check docker logs for the code.'}); - - } catch (err) { - logger.error('Failed to initiate password reset', err); - res.status(500).json({error: 'Failed to initiate password reset'}); + if (user[0].is_oidc) { + return res.status(403).json({ + error: "Password reset not available for external authentication users", + }); } + + const resetCode = Math.floor(100000 + Math.random() * 900000).toString(); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); + + db.$client + .prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") + .run( + `reset_code_${username}`, + JSON.stringify({ code: resetCode, expiresAt: expiresAt.toISOString() }), + ); + + authLogger.info( + `Password reset code for user ${username}: ${resetCode} (expires at ${expiresAt.toLocaleString()})`, + ); + + res.json({ + message: + "Password reset code has been generated and logged. Check docker logs for the code.", + }); + } catch (err) { + authLogger.error("Failed to initiate password reset", err); + res.status(500).json({ error: "Failed to initiate password reset" }); + } }); // Route: Verify reset code // POST /users/verify-reset-code -router.post('/verify-reset-code', async (req, res) => { - const {username, resetCode} = req.body; +router.post("/verify-reset-code", async (req, res) => { + const { username, resetCode } = req.body; - if (!isNonEmptyString(username) || !isNonEmptyString(resetCode)) { - return res.status(400).json({error: 'Username and reset code are required'}); + if (!isNonEmptyString(username) || !isNonEmptyString(resetCode)) { + return res + .status(400) + .json({ error: "Username and reset code are required" }); + } + + try { + const resetDataRow = db.$client + .prepare("SELECT value FROM settings WHERE key = ?") + .get(`reset_code_${username}`); + if (!resetDataRow) { + return res + .status(400) + .json({ error: "No reset code found for this user" }); } - try { - const resetDataRow = db.$client.prepare("SELECT value FROM settings WHERE key = ?").get(`reset_code_${username}`); - if (!resetDataRow) { - return res.status(400).json({error: 'No reset code found for this user'}); - } + const resetData = JSON.parse((resetDataRow as any).value); + const now = new Date(); + const expiresAt = new Date(resetData.expiresAt); - const resetData = JSON.parse((resetDataRow as any).value); - const now = new Date(); - const expiresAt = new Date(resetData.expiresAt); - - if (now > expiresAt) { - db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`reset_code_${username}`); - return res.status(400).json({error: 'Reset code has expired'}); - } - - if (resetData.code !== resetCode) { - return res.status(400).json({error: 'Invalid reset code'}); - } - - const tempToken = nanoid(); - const tempTokenExpiry = new Date(Date.now() + 10 * 60 * 1000); - - db.$client.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run( - `temp_reset_token_${username}`, - JSON.stringify({token: tempToken, expiresAt: tempTokenExpiry.toISOString()}) - ); - - res.json({message: 'Reset code verified', tempToken}); - - } catch (err) { - logger.error('Failed to verify reset code', err); - res.status(500).json({error: 'Failed to verify reset code'}); + if (now > expiresAt) { + db.$client + .prepare("DELETE FROM settings WHERE key = ?") + .run(`reset_code_${username}`); + return res.status(400).json({ error: "Reset code has expired" }); } + + if (resetData.code !== resetCode) { + return res.status(400).json({ error: "Invalid reset code" }); + } + + const tempToken = nanoid(); + const tempTokenExpiry = new Date(Date.now() + 10 * 60 * 1000); + + db.$client + .prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)") + .run( + `temp_reset_token_${username}`, + JSON.stringify({ + token: tempToken, + expiresAt: tempTokenExpiry.toISOString(), + }), + ); + + res.json({ message: "Reset code verified", tempToken }); + } catch (err) { + authLogger.error("Failed to verify reset code", err); + res.status(500).json({ error: "Failed to verify reset code" }); + } }); // Route: Complete password reset // POST /users/complete-reset -router.post('/complete-reset', async (req, res) => { - const {username, tempToken, newPassword} = req.body; +router.post("/complete-reset", async (req, res) => { + const { username, tempToken, newPassword } = req.body; - if (!isNonEmptyString(username) || !isNonEmptyString(tempToken) || !isNonEmptyString(newPassword)) { - return res.status(400).json({error: 'Username, temporary token, and new password are required'}); + if ( + !isNonEmptyString(username) || + !isNonEmptyString(tempToken) || + !isNonEmptyString(newPassword) + ) { + return res.status(400).json({ + error: "Username, temporary token, and new password are required", + }); + } + + try { + const tempTokenRow = db.$client + .prepare("SELECT value FROM settings WHERE key = ?") + .get(`temp_reset_token_${username}`); + if (!tempTokenRow) { + return res.status(400).json({ error: "No temporary token found" }); } - try { - const tempTokenRow = db.$client.prepare("SELECT value FROM settings WHERE key = ?").get(`temp_reset_token_${username}`); - if (!tempTokenRow) { - return res.status(400).json({error: 'No temporary token found'}); - } + const tempTokenData = JSON.parse((tempTokenRow as any).value); + const now = new Date(); + const expiresAt = new Date(tempTokenData.expiresAt); - const tempTokenData = JSON.parse((tempTokenRow as any).value); - const now = new Date(); - const expiresAt = new Date(tempTokenData.expiresAt); - - if (now > expiresAt) { - // Clean up expired token - db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`temp_reset_token_${username}`); - return res.status(400).json({error: 'Temporary token has expired'}); - } - - if (tempTokenData.token !== tempToken) { - return res.status(400).json({error: 'Invalid temporary token'}); - } - - const saltRounds = parseInt(process.env.SALT || '10', 10); - const password_hash = await bcrypt.hash(newPassword, saltRounds); - - await db.update(users) - .set({password_hash}) - .where(eq(users.username, username)); - - db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`reset_code_${username}`); - db.$client.prepare("DELETE FROM settings WHERE key = ?").run(`temp_reset_token_${username}`); - - logger.success(`Password successfully reset for user: ${username}`); - res.json({message: 'Password has been successfully reset'}); - - } catch (err) { - logger.error('Failed to complete password reset', err); - res.status(500).json({error: 'Failed to complete password reset'}); + if (now > expiresAt) { + db.$client + .prepare("DELETE FROM settings WHERE key = ?") + .run(`temp_reset_token_${username}`); + return res.status(400).json({ error: "Temporary token has expired" }); } + + if (tempTokenData.token !== tempToken) { + return res.status(400).json({ error: "Invalid temporary token" }); + } + + const saltRounds = parseInt(process.env.SALT || "10", 10); + const password_hash = await bcrypt.hash(newPassword, saltRounds); + + await db + .update(users) + .set({ password_hash }) + .where(eq(users.username, username)); + + db.$client + .prepare("DELETE FROM settings WHERE key = ?") + .run(`reset_code_${username}`); + db.$client + .prepare("DELETE FROM settings WHERE key = ?") + .run(`temp_reset_token_${username}`); + + authLogger.success(`Password successfully reset for user: ${username}`); + res.json({ message: "Password has been successfully reset" }); + } catch (err) { + authLogger.error("Failed to complete password reset", err); + res.status(500).json({ error: "Failed to complete password reset" }); + } }); // Route: List all users (admin only) // GET /users/list -router.get('/list', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0 || !user[0].is_admin) { - return res.status(403).json({error: 'Not authorized'}); - } - - const allUsers = await db.select({ - id: users.id, - username: users.username, - is_admin: users.is_admin, - is_oidc: users.is_oidc - }).from(users); - - res.json({users: allUsers}); - } catch (err) { - logger.error('Failed to list users', err); - res.status(500).json({error: 'Failed to list users'}); +router.get("/list", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0 || !user[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); } + + const allUsers = await db + .select({ + id: users.id, + username: users.username, + is_admin: users.is_admin, + is_oidc: users.is_oidc, + }) + .from(users); + + res.json({ users: allUsers }); + } catch (err) { + authLogger.error("Failed to list users", err); + res.status(500).json({ error: "Failed to list users" }); + } }); // Route: Make user admin (admin only) // POST /users/make-admin -router.post('/make-admin', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - const {username} = req.body; +router.post("/make-admin", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + const { username } = req.body; - if (!isNonEmptyString(username)) { - return res.status(400).json({error: 'Username is required'}); + if (!isNonEmptyString(username)) { + return res.status(400).json({ error: "Username is required" }); + } + + try { + const adminUser = await db.select().from(users).where(eq(users.id, userId)); + if (!adminUser || adminUser.length === 0 || !adminUser[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); } - try { - const adminUser = await db.select().from(users).where(eq(users.id, userId)); - if (!adminUser || adminUser.length === 0 || !adminUser[0].is_admin) { - return res.status(403).json({error: 'Not authorized'}); - } - - const targetUser = await db.select().from(users).where(eq(users.username, username)); - if (!targetUser || targetUser.length === 0) { - return res.status(404).json({error: 'User not found'}); - } - - if (targetUser[0].is_admin) { - return res.status(400).json({error: 'User is already an admin'}); - } - - await db.update(users) - .set({is_admin: true}) - .where(eq(users.username, username)); - - logger.success(`User ${username} made admin by ${adminUser[0].username}`); - res.json({message: `User ${username} is now an admin`}); - - } catch (err) { - logger.error('Failed to make user admin', err); - res.status(500).json({error: 'Failed to make user admin'}); + const targetUser = await db + .select() + .from(users) + .where(eq(users.username, username)); + if (!targetUser || targetUser.length === 0) { + return res.status(404).json({ error: "User not found" }); } + + if (targetUser[0].is_admin) { + return res.status(400).json({ error: "User is already an admin" }); + } + + await db + .update(users) + .set({ is_admin: true }) + .where(eq(users.username, username)); + + authLogger.success( + `User ${username} made admin by ${adminUser[0].username}`, + ); + res.json({ message: `User ${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" }); + } }); // Route: Remove admin status (admin only) // POST /users/remove-admin -router.post('/remove-admin', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - const {username} = req.body; +router.post("/remove-admin", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + const { username } = req.body; - if (!isNonEmptyString(username)) { - return res.status(400).json({error: 'Username is required'}); + if (!isNonEmptyString(username)) { + return res.status(400).json({ error: "Username is required" }); + } + + try { + const adminUser = await db.select().from(users).where(eq(users.id, userId)); + if (!adminUser || adminUser.length === 0 || !adminUser[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); } - try { - const adminUser = await db.select().from(users).where(eq(users.id, userId)); - if (!adminUser || adminUser.length === 0 || !adminUser[0].is_admin) { - return res.status(403).json({error: 'Not authorized'}); - } - - if (adminUser[0].username === username) { - return res.status(400).json({error: 'Cannot remove your own admin status'}); - } - - const targetUser = await db.select().from(users).where(eq(users.username, username)); - if (!targetUser || targetUser.length === 0) { - return res.status(404).json({error: 'User not found'}); - } - - if (!targetUser[0].is_admin) { - return res.status(400).json({error: 'User is not an admin'}); - } - - await db.update(users) - .set({is_admin: false}) - .where(eq(users.username, username)); - - logger.success(`Admin status removed from ${username} by ${adminUser[0].username}`); - res.json({message: `Admin status removed from ${username}`}); - - } catch (err) { - logger.error('Failed to remove admin status', err); - res.status(500).json({error: 'Failed to remove admin status'}); + if (adminUser[0].username === username) { + return res + .status(400) + .json({ error: "Cannot remove your own admin status" }); } + + const targetUser = await db + .select() + .from(users) + .where(eq(users.username, username)); + if (!targetUser || targetUser.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + if (!targetUser[0].is_admin) { + return res.status(400).json({ error: "User is not an admin" }); + } + + await db + .update(users) + .set({ is_admin: false }) + .where(eq(users.username, username)); + + authLogger.success( + `Admin status removed from ${username} by ${adminUser[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" }); + } }); // Route: Verify TOTP during login // POST /users/totp/verify-login -router.post('/totp/verify-login', async (req, res) => { - const {temp_token, totp_code} = req.body; +router.post("/totp/verify-login", async (req, res) => { + const { temp_token, totp_code } = req.body; - if (!temp_token || !totp_code) { - return res.status(400).json({error: 'Token and TOTP code are required'}); + if (!temp_token || !totp_code) { + return res.status(400).json({ error: "Token and TOTP code are required" }); + } + + const jwtSecret = process.env.JWT_SECRET || "secret"; + + try { + const decoded = jwt.verify(temp_token, jwtSecret) as any; + if (!decoded.pending_totp) { + return res.status(401).json({ error: "Invalid temporary token" }); } - const jwtSecret = process.env.JWT_SECRET || 'secret'; - - try { - const decoded = jwt.verify(temp_token, jwtSecret) as any; - if (!decoded.pending_totp) { - return res.status(401).json({error: 'Invalid temporary token'}); - } - - const user = await db.select().from(users).where(eq(users.id, decoded.userId)); - if (!user || user.length === 0) { - return res.status(404).json({error: 'User not found'}); - } - - const userRecord = user[0]; - - if (!userRecord.totp_enabled || !userRecord.totp_secret) { - return res.status(400).json({error: 'TOTP not enabled for this user'}); - } - - const verified = speakeasy.totp.verify({ - secret: userRecord.totp_secret, - encoding: 'base32', - token: totp_code, - window: 2 - }); - - if (!verified) { - const backupCodes = userRecord.totp_backup_codes ? JSON.parse(userRecord.totp_backup_codes) : []; - const backupIndex = backupCodes.indexOf(totp_code); - - if (backupIndex === -1) { - return res.status(401).json({error: 'Invalid TOTP code'}); - } - - backupCodes.splice(backupIndex, 1); - await db.update(users) - .set({totp_backup_codes: JSON.stringify(backupCodes)}) - .where(eq(users.id, userRecord.id)); - } - - const token = jwt.sign({userId: userRecord.id}, jwtSecret, { - expiresIn: '50d', - }); - - return res.json({ - token, - is_admin: !!userRecord.is_admin, - username: userRecord.username - }); - - } catch (err) { - logger.error('TOTP verification failed', err); - return res.status(500).json({error: 'TOTP verification failed'}); + const user = await db + .select() + .from(users) + .where(eq(users.id, decoded.userId)); + if (!user || user.length === 0) { + return res.status(404).json({ error: "User not found" }); } + + const userRecord = user[0]; + + if (!userRecord.totp_enabled || !userRecord.totp_secret) { + return res.status(400).json({ error: "TOTP not enabled for this user" }); + } + + const verified = speakeasy.totp.verify({ + secret: userRecord.totp_secret, + encoding: "base32", + token: totp_code, + window: 2, + }); + + if (!verified) { + const backupCodes = userRecord.totp_backup_codes + ? JSON.parse(userRecord.totp_backup_codes) + : []; + const backupIndex = backupCodes.indexOf(totp_code); + + if (backupIndex === -1) { + return res.status(401).json({ error: "Invalid TOTP code" }); + } + + backupCodes.splice(backupIndex, 1); + await db + .update(users) + .set({ totp_backup_codes: JSON.stringify(backupCodes) }) + .where(eq(users.id, userRecord.id)); + } + + const token = jwt.sign({ userId: userRecord.id }, jwtSecret, { + expiresIn: "50d", + }); + + return res.json({ + token, + is_admin: !!userRecord.is_admin, + username: userRecord.username, + }); + } catch (err) { + authLogger.error("TOTP verification failed", err); + return res.status(500).json({ error: "TOTP verification failed" }); + } }); // Route: Setup TOTP // POST /users/totp/setup -router.post('/totp/setup', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; +router.post("/totp/setup", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0) { - return res.status(404).json({error: 'User not found'}); - } - - const userRecord = user[0]; - - if (userRecord.totp_enabled) { - return res.status(400).json({error: 'TOTP is already enabled'}); - } - - const secret = speakeasy.generateSecret({ - name: `Termix (${userRecord.username})`, - length: 32 - }); - - await db.update(users) - .set({totp_secret: secret.base32}) - .where(eq(users.id, userId)); - - const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url || ''); - - res.json({ - secret: secret.base32, - qr_code: qrCodeUrl - }); - - } catch (err) { - logger.error('Failed to setup TOTP', err); - res.status(500).json({error: 'Failed to setup TOTP'}); + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0) { + return res.status(404).json({ error: "User not found" }); } + + const userRecord = user[0]; + + if (userRecord.totp_enabled) { + return res.status(400).json({ error: "TOTP is already enabled" }); + } + + const secret = speakeasy.generateSecret({ + name: `Termix (${userRecord.username})`, + length: 32, + }); + + await db + .update(users) + .set({ totp_secret: secret.base32 }) + .where(eq(users.id, userId)); + + const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url || ""); + + res.json({ + secret: secret.base32, + qr_code: qrCodeUrl, + }); + } catch (err) { + authLogger.error("Failed to setup TOTP", err); + res.status(500).json({ error: "Failed to setup TOTP" }); + } }); // Route: Enable TOTP // POST /users/totp/enable -router.post('/totp/enable', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - const {totp_code} = req.body; +router.post("/totp/enable", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + const { totp_code } = req.body; - if (!totp_code) { - return res.status(400).json({error: 'TOTP code is required'}); + if (!totp_code) { + return res.status(400).json({ error: "TOTP code is required" }); + } + + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0) { + return res.status(404).json({ error: "User not found" }); } - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0) { - return res.status(404).json({error: 'User not found'}); - } + const userRecord = user[0]; - const userRecord = user[0]; - - if (userRecord.totp_enabled) { - return res.status(400).json({error: 'TOTP is already enabled'}); - } - - if (!userRecord.totp_secret) { - return res.status(400).json({error: 'TOTP setup not initiated'}); - } - - const verified = speakeasy.totp.verify({ - secret: userRecord.totp_secret, - encoding: 'base32', - token: totp_code, - window: 2 - }); - - if (!verified) { - return res.status(401).json({error: 'Invalid TOTP code'}); - } - - const backupCodes = Array.from({length: 8}, () => - Math.random().toString(36).substring(2, 10).toUpperCase() - ); - - await db.update(users) - .set({ - totp_enabled: true, - totp_backup_codes: JSON.stringify(backupCodes) - }) - .where(eq(users.id, userId)); - - res.json({ - message: 'TOTP enabled successfully', - backup_codes: backupCodes - }); - - } catch (err) { - logger.error('Failed to enable TOTP', err); - res.status(500).json({error: 'Failed to enable TOTP'}); + if (userRecord.totp_enabled) { + return res.status(400).json({ error: "TOTP is already enabled" }); } + + if (!userRecord.totp_secret) { + return res.status(400).json({ error: "TOTP setup not initiated" }); + } + + const verified = speakeasy.totp.verify({ + secret: userRecord.totp_secret, + encoding: "base32", + token: totp_code, + window: 2, + }); + + if (!verified) { + return res.status(401).json({ error: "Invalid TOTP code" }); + } + + const backupCodes = Array.from({ length: 8 }, () => + Math.random().toString(36).substring(2, 10).toUpperCase(), + ); + + await db + .update(users) + .set({ + totp_enabled: true, + totp_backup_codes: JSON.stringify(backupCodes), + }) + .where(eq(users.id, userId)); + + res.json({ + message: "TOTP enabled successfully", + backup_codes: backupCodes, + }); + } catch (err) { + authLogger.error("Failed to enable TOTP", err); + res.status(500).json({ error: "Failed to enable TOTP" }); + } }); // Route: Disable TOTP // POST /users/totp/disable -router.post('/totp/disable', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - const {password, totp_code} = req.body; +router.post("/totp/disable", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + const { password, totp_code } = req.body; - if (!password && !totp_code) { - return res.status(400).json({error: 'Password or TOTP code is required'}); + if (!password && !totp_code) { + return res.status(400).json({ error: "Password or TOTP code is required" }); + } + + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0) { + return res.status(404).json({ error: "User not found" }); } - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0) { - return res.status(404).json({error: 'User not found'}); - } + const userRecord = user[0]; - const userRecord = user[0]; - - if (!userRecord.totp_enabled) { - return res.status(400).json({error: 'TOTP is not enabled'}); - } - - if (password && !userRecord.is_oidc) { - const isMatch = await bcrypt.compare(password, userRecord.password_hash); - if (!isMatch) { - return res.status(401).json({error: 'Incorrect password'}); - } - } else if (totp_code) { - const verified = speakeasy.totp.verify({ - secret: userRecord.totp_secret!, - encoding: 'base32', - token: totp_code, - window: 2 - }); - - if (!verified) { - return res.status(401).json({error: 'Invalid TOTP code'}); - } - } else { - return res.status(400).json({error: 'Authentication required'}); - } - - await db.update(users) - .set({ - totp_enabled: false, - totp_secret: null, - totp_backup_codes: null - }) - .where(eq(users.id, userId)); - - res.json({message: 'TOTP disabled successfully'}); - - } catch (err) { - logger.error('Failed to disable TOTP', err); - res.status(500).json({error: 'Failed to disable TOTP'}); + if (!userRecord.totp_enabled) { + return res.status(400).json({ error: "TOTP is not enabled" }); } + + if (password && !userRecord.is_oidc) { + const isMatch = await bcrypt.compare(password, userRecord.password_hash); + if (!isMatch) { + return res.status(401).json({ error: "Incorrect password" }); + } + } else if (totp_code) { + const verified = speakeasy.totp.verify({ + secret: userRecord.totp_secret!, + encoding: "base32", + token: totp_code, + window: 2, + }); + + if (!verified) { + return res.status(401).json({ error: "Invalid TOTP code" }); + } + } else { + return res.status(400).json({ error: "Authentication required" }); + } + + await db + .update(users) + .set({ + totp_enabled: false, + totp_secret: null, + totp_backup_codes: null, + }) + .where(eq(users.id, userId)); + + res.json({ message: "TOTP disabled successfully" }); + } catch (err) { + authLogger.error("Failed to disable TOTP", err); + res.status(500).json({ error: "Failed to disable TOTP" }); + } }); // Route: Generate new backup codes // POST /users/totp/backup-codes -router.post('/totp/backup-codes', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - const {password, totp_code} = req.body; +router.post("/totp/backup-codes", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + const { password, totp_code } = req.body; - if (!password && !totp_code) { - return res.status(400).json({error: 'Password or TOTP code is required'}); + if (!password && !totp_code) { + return res.status(400).json({ error: "Password or TOTP code is required" }); + } + + try { + const user = await db.select().from(users).where(eq(users.id, userId)); + if (!user || user.length === 0) { + return res.status(404).json({ error: "User not found" }); } - try { - const user = await db.select().from(users).where(eq(users.id, userId)); - if (!user || user.length === 0) { - return res.status(404).json({error: 'User not found'}); - } + const userRecord = user[0]; - const userRecord = user[0]; - - if (!userRecord.totp_enabled) { - return res.status(400).json({error: 'TOTP is not enabled'}); - } - - if (password && !userRecord.is_oidc) { - const isMatch = await bcrypt.compare(password, userRecord.password_hash); - if (!isMatch) { - return res.status(401).json({error: 'Incorrect password'}); - } - } else if (totp_code) { - const verified = speakeasy.totp.verify({ - secret: userRecord.totp_secret!, - encoding: 'base32', - token: totp_code, - window: 2 - }); - - if (!verified) { - return res.status(401).json({error: 'Invalid TOTP code'}); - } - } else { - return res.status(400).json({error: 'Authentication required'}); - } - - const backupCodes = Array.from({length: 8}, () => - Math.random().toString(36).substring(2, 10).toUpperCase() - ); - - await db.update(users) - .set({totp_backup_codes: JSON.stringify(backupCodes)}) - .where(eq(users.id, userId)); - - res.json({backup_codes: backupCodes}); - - } catch (err) { - logger.error('Failed to generate backup codes', err); - res.status(500).json({error: 'Failed to generate backup codes'}); + if (!userRecord.totp_enabled) { + return res.status(400).json({ error: "TOTP is not enabled" }); } + + if (password && !userRecord.is_oidc) { + const isMatch = await bcrypt.compare(password, userRecord.password_hash); + if (!isMatch) { + return res.status(401).json({ error: "Incorrect password" }); + } + } else if (totp_code) { + const verified = speakeasy.totp.verify({ + secret: userRecord.totp_secret!, + encoding: "base32", + token: totp_code, + window: 2, + }); + + if (!verified) { + return res.status(401).json({ error: "Invalid TOTP code" }); + } + } else { + return res.status(400).json({ error: "Authentication required" }); + } + + const backupCodes = Array.from({ length: 8 }, () => + Math.random().toString(36).substring(2, 10).toUpperCase(), + ); + + await db + .update(users) + .set({ totp_backup_codes: JSON.stringify(backupCodes) }) + .where(eq(users.id, userId)); + + res.json({ backup_codes: backupCodes }); + } catch (err) { + authLogger.error("Failed to generate backup codes", err); + res.status(500).json({ error: "Failed to generate backup codes" }); + } }); // Route: Delete user (admin only) // DELETE /users/delete-user -router.delete('/delete-user', authenticateJWT, async (req, res) => { - const userId = (req as any).userId; - const {username} = req.body; +router.delete("/delete-user", authenticateJWT, async (req, res) => { + const userId = (req as any).userId; + const { username } = req.body; - if (!isNonEmptyString(username)) { - return res.status(400).json({error: 'Username is required'}); + if (!isNonEmptyString(username)) { + return res.status(400).json({ error: "Username is required" }); + } + + try { + const adminUser = await db.select().from(users).where(eq(users.id, userId)); + if (!adminUser || adminUser.length === 0 || !adminUser[0].is_admin) { + return res.status(403).json({ error: "Not authorized" }); } + if (adminUser[0].username === username) { + return res.status(400).json({ error: "Cannot delete your own account" }); + } + + const targetUser = await db + .select() + .from(users) + .where(eq(users.username, username)); + if (!targetUser || targetUser.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + if (targetUser[0].is_admin) { + const adminCount = db.$client + .prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1") + .get(); + if ((adminCount as any)?.count <= 1) { + return res + .status(403) + .json({ error: "Cannot delete the last admin user" }); + } + } + + const targetUserId = targetUser[0].id; + try { - const adminUser = await db.select().from(users).where(eq(users.id, userId)); - if (!adminUser || adminUser.length === 0 || !adminUser[0].is_admin) { - return res.status(403).json({error: 'Not authorized'}); - } + await db + .delete(fileManagerRecent) + .where(eq(fileManagerRecent.userId, targetUserId)); + await db + .delete(fileManagerPinned) + .where(eq(fileManagerPinned.userId, targetUserId)); + await db + .delete(fileManagerShortcuts) + .where(eq(fileManagerShortcuts.userId, targetUserId)); - if (adminUser[0].username === username) { - return res.status(400).json({error: 'Cannot delete your own account'}); - } + await db + .delete(dismissedAlerts) + .where(eq(dismissedAlerts.userId, targetUserId)); - const targetUser = await db.select().from(users).where(eq(users.username, username)); - if (!targetUser || targetUser.length === 0) { - return res.status(404).json({error: 'User not found'}); - } - - if (targetUser[0].is_admin) { - const adminCount = db.$client.prepare('SELECT COUNT(*) as count FROM users WHERE is_admin = 1').get(); - if ((adminCount as any)?.count <= 1) { - return res.status(403).json({error: 'Cannot delete the last admin user'}); - } - } - - const targetUserId = targetUser[0].id; - - try { - await db.delete(fileManagerRecent).where(eq(fileManagerRecent.userId, targetUserId)); - await db.delete(fileManagerPinned).where(eq(fileManagerPinned.userId, targetUserId)); - await db.delete(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, targetUserId)); - - await db.delete(dismissedAlerts).where(eq(dismissedAlerts.userId, targetUserId)); - - await db.delete(sshData).where(eq(sshData.userId, targetUserId)); - - // Note: All user-related data has been deleted above - // The tables config_editor_* and shared_hosts don't exist in the current schema - } catch (cleanupError) { - logger.error(`Cleanup failed for user ${username}:`, cleanupError); - throw cleanupError; - } - - await db.delete(users).where(eq(users.id, targetUserId)); - - logger.success(`User ${username} deleted by admin ${adminUser[0].username}`); - res.json({message: `User ${username} deleted successfully`}); - - } catch (err) { - logger.error('Failed to delete user', err); - - if (err && typeof err === 'object' && 'code' in err) { - if (err.code === 'SQLITE_CONSTRAINT_FOREIGNKEY') { - res.status(400).json({error: 'Cannot delete user: User has associated data that cannot be removed'}); - } else { - res.status(500).json({error: `Database error: ${err.code}`}); - } - } else { - res.status(500).json({error: 'Failed to delete account'}); - } + await db.delete(sshData).where(eq(sshData.userId, targetUserId)); + } catch (cleanupError) { + authLogger.error(`Cleanup failed for user ${username}:`, cleanupError); + throw cleanupError; } + + await db.delete(users).where(eq(users.id, targetUserId)); + + authLogger.success( + `User ${username} deleted by admin ${adminUser[0].username}`, + ); + res.json({ message: `User ${username} deleted successfully` }); + } catch (err) { + authLogger.error("Failed to delete user", err); + + if (err && typeof err === "object" && "code" in err) { + if (err.code === "SQLITE_CONSTRAINT_FOREIGNKEY") { + res.status(400).json({ + error: + "Cannot delete user: User has associated data that cannot be removed", + }); + } else { + res.status(500).json({ error: `Database error: ${err.code}` }); + } + } else { + res.status(500).json({ error: "Failed to delete account" }); + } + } }); -export default router; \ No newline at end of file +export default router; diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index ee8bcc9e..20c8f816 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -1,1032 +1,1288 @@ -import express from 'express'; -import cors from 'cors'; -import {Client as SSHClient} from 'ssh2'; -import chalk from "chalk"; +import express from "express"; +import cors from "cors"; +import { Client as SSHClient } from "ssh2"; +import { db } from "../database/db/index.js"; +import { sshCredentials } from "../database/db/schema.js"; +import { eq, and } from "drizzle-orm"; +import { fileLogger } from "../utils/logger.js"; const app = express(); -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] -})); -app.use(express.json({limit: '100mb'})); -app.use(express.urlencoded({limit: '100mb', extended: true})); -app.use(express.raw({limit: '200mb', type: 'application/octet-stream'})); - -const sshIconSymbol = '📁'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${sshIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; +app.use( + cors({ + origin: "*", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "User-Agent", + "X-Electron-App", + ], + }), +); +app.use(express.json({ limit: "100mb" })); +app.use(express.urlencoded({ limit: "100mb", extended: true })); +app.use(express.raw({ limit: "200mb", type: "application/octet-stream" })); interface SSHSession { - client: SSHClient; - isConnected: boolean; - lastActive: number; - timeout?: NodeJS.Timeout; + client: SSHClient; + isConnected: boolean; + lastActive: number; + timeout?: NodeJS.Timeout; } const sshSessions: Record = {}; function cleanupSession(sessionId: string) { - const session = sshSessions[sessionId]; - if (session) { - try { - session.client.end(); - } catch { - } - clearTimeout(session.timeout); - delete sshSessions[sessionId]; - } + const session = sshSessions[sessionId]; + if (session) { + try { + session.client.end(); + } catch {} + clearTimeout(session.timeout); + delete sshSessions[sessionId]; + } } function scheduleSessionCleanup(sessionId: string) { - const session = sshSessions[sessionId]; - if (session) { - if (session.timeout) clearTimeout(session.timeout); - } + const session = sshSessions[sessionId]; + if (session) { + if (session.timeout) clearTimeout(session.timeout); + } } -app.post('/ssh/file_manager/ssh/connect', async (req, res) => { - const {sessionId, ip, port, username, password, sshKey, keyPassword} = req.body; - if (!sessionId || !ip || !username || !port) { - return res.status(400).json({error: 'Missing SSH connection parameters'}); - } +app.post("/ssh/file_manager/ssh/connect", async (req, res) => { + const { + sessionId, + hostId, + ip, + port, + username, + password, + sshKey, + keyPassword, + authType, + credentialId, + userId, + } = req.body; - if (sshSessions[sessionId]?.isConnected) { - cleanupSession(sessionId); - } - const client = new SSHClient(); - const config: any = { - host: ip, - port: port || 22, - username, - readyTimeout: 0, - keepaliveInterval: 30000, - keepaliveCountMax: 0, - algorithms: { - kex: [ - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group1-sha1', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group-exchange-sha1', - 'ecdh-sha2-nistp256', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp521' - ], - cipher: [ - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - 'aes128-gcm@openssh.com', - 'aes256-gcm@openssh.com', - 'aes128-cbc', - 'aes192-cbc', - 'aes256-cbc', - '3des-cbc' - ], - hmac: [ - 'hmac-sha2-256', - 'hmac-sha2-512', - 'hmac-sha1', - 'hmac-md5' - ], - compress: [ - 'none', - 'zlib@openssh.com', - 'zlib' - ] - } - }; - - if (sshKey && sshKey.trim()) { - try { - if (!sshKey.includes('-----BEGIN') || !sshKey.includes('-----END')) { - throw new Error('Invalid private key format'); - } - - const cleanKey = sshKey.trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - config.privateKey = Buffer.from(cleanKey, 'utf8'); - - if (keyPassword) config.passphrase = keyPassword; - - logger.info('SSH key authentication configured successfully for file manager'); - } catch (keyError) { - logger.error('SSH key format error: ' + keyError.message); - return res.status(400).json({error: 'Invalid SSH key format'}); - } - } else if (password && password.trim()) { - config.password = password; - } else { - return res.status(400).json({error: 'Either password or SSH key must be provided'}); - } - - let responseSent = false; - - client.on('ready', () => { - if (responseSent) return; - responseSent = true; - sshSessions[sessionId] = {client, isConnected: true, lastActive: Date.now()}; - res.json({status: 'success', message: 'SSH connection established'}); + if (!sessionId || !ip || !username || !port) { + fileLogger.warn("Missing SSH connection parameters for file manager", { + operation: "file_connect", + sessionId, + hasIp: !!ip, + hasUsername: !!username, + hasPort: !!port, }); + return res.status(400).json({ error: "Missing SSH connection parameters" }); + } - client.on('error', (err) => { - if (responseSent) return; - responseSent = true; - logger.error(`SSH connection error for session ${sessionId}:`, err.message); - res.status(500).json({status: 'error', message: err.message}); - }); - - client.on('close', () => { - if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false; - cleanupSession(sessionId); - }); - - client.connect(config); -}); - -app.post('/ssh/file_manager/ssh/disconnect', (req, res) => { - const {sessionId} = req.body; + if (sshSessions[sessionId]?.isConnected) { cleanupSession(sessionId); - res.json({status: 'success', message: 'SSH connection disconnected'}); -}); + } + const client = new SSHClient(); -app.get('/ssh/file_manager/ssh/status', (req, res) => { - const sessionId = req.query.sessionId as string; - const isConnected = !!sshSessions[sessionId]?.isConnected; - res.json({status: 'success', connected: isConnected}); -}); + let resolvedCredentials = { password, sshKey, keyPassword, authType }; + if (credentialId && hostId && userId) { + try { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, credentialId), + eq(sshCredentials.userId, userId), + ), + ); -app.get('/ssh/file_manager/ssh/listFiles', (req, res) => { - const sessionId = req.query.sessionId as string; - const sshConn = sshSessions[sessionId]; - const sshPath = decodeURIComponent((req.query.path as string) || '/'); - - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); + if (credentials.length > 0) { + const credential = credentials[0]; + resolvedCredentials = { + password: credential.password, + sshKey: credential.key, + keyPassword: credential.keyPassword, + authType: credential.authType, + }; + } else { + fileLogger.warn("No credentials found in database for file manager", { + operation: "file_connect", + sessionId, + hostId, + credentialId, + userId, + }); + } + } catch (error) { + fileLogger.warn( + "Failed to resolve credentials from database for file manager", + { + operation: "file_connect", + sessionId, + hostId, + credentialId, + error: error instanceof Error ? error.message : "Unknown error", + }, + ); } + } else if (credentialId && hostId) { + fileLogger.warn( + "Missing userId for credential resolution in file manager", + { + operation: "file_connect", + sessionId, + hostId, + credentialId, + hasUserId: !!userId, + }, + ); + } - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); + const config: any = { + host: ip, + port: port || 22, + username, + readyTimeout: 0, + keepaliveInterval: 30000, + keepaliveCountMax: 0, + algorithms: { + kex: [ + "diffie-hellman-group14-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group1-sha1", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group-exchange-sha1", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + ], + cipher: [ + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-gcm@openssh.com", + "aes256-gcm@openssh.com", + "aes128-cbc", + "aes192-cbc", + "aes256-cbc", + "3des-cbc", + ], + hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-md5"], + compress: ["none", "zlib@openssh.com", "zlib"], + }, + }; + + if (resolvedCredentials.sshKey && resolvedCredentials.sshKey.trim()) { + try { + if ( + !resolvedCredentials.sshKey.includes("-----BEGIN") || + !resolvedCredentials.sshKey.includes("-----END") + ) { + throw new Error("Invalid private key format"); + } + + const cleanKey = resolvedCredentials.sshKey + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + + config.privateKey = Buffer.from(cleanKey, "utf8"); + + if (resolvedCredentials.keyPassword) + config.passphrase = resolvedCredentials.keyPassword; + } catch (keyError) { + fileLogger.error("SSH key format error for file manager", { + operation: "file_connect", + sessionId, + hostId, + error: keyError.message, + }); + return res.status(400).json({ error: "Invalid SSH key format" }); } - - sshConn.lastActive = Date.now(); - - - const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); - sshConn.client.exec(`ls -la '${escapedPath}'`, (err, stream) => { - if (err) { - logger.error('SSH listFiles error:', err); - return res.status(500).json({error: err.message}); - } - - let data = ''; - let errorData = ''; - - stream.on('data', (chunk: Buffer) => { - data += chunk.toString(); - }); - - stream.stderr.on('data', (chunk: Buffer) => { - errorData += chunk.toString(); - }); - - stream.on('close', (code) => { - if (code !== 0) { - logger.error(`SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`); - return res.status(500).json({error: `Command failed: ${errorData}`}); - } - - const lines = data.split('\n').filter(line => line.trim()); - const files = []; - - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - const parts = line.split(/\s+/); - if (parts.length >= 9) { - const permissions = parts[0]; - const name = parts.slice(8).join(' '); - const isDirectory = permissions.startsWith('d'); - const isLink = permissions.startsWith('l'); - - if (name === '.' || name === '..') continue; - - files.push({ - name, - type: isDirectory ? 'directory' : (isLink ? 'link' : 'file') - }); - } - } - - res.json(files); - }); + } else if ( + resolvedCredentials.password && + resolvedCredentials.password.trim() + ) { + config.password = resolvedCredentials.password; + } else { + fileLogger.warn("No authentication method provided for file manager", { + operation: "file_connect", + sessionId, + hostId, }); -}); + return res + .status(400) + .json({ error: "Either password or SSH key must be provided" }); + } -app.get('/ssh/file_manager/ssh/readFile', (req, res) => { - const sessionId = req.query.sessionId as string; - const sshConn = sshSessions[sessionId]; - const filePath = decodeURIComponent(req.query.path as string); + let responseSent = false; - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); - } - - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); - } - - if (!filePath) { - return res.status(400).json({error: 'File path is required'}); - } - - sshConn.lastActive = Date.now(); - - - const escapedPath = filePath.replace(/'/g, "'\"'\"'"); - sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => { - if (err) { - logger.error('SSH readFile error:', err); - return res.status(500).json({error: err.message}); - } - - let data = ''; - let errorData = ''; - - stream.on('data', (chunk: Buffer) => { - data += chunk.toString(); - }); - - stream.stderr.on('data', (chunk: Buffer) => { - errorData += chunk.toString(); - }); - - stream.on('close', (code) => { - if (code !== 0) { - logger.error(`SSH readFile command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`); - return res.status(500).json({error: `Command failed: ${errorData}`}); - } - - res.json({content: data, path: filePath}); - }); - }); -}); - -app.post('/ssh/file_manager/ssh/writeFile', (req, res) => { - const {sessionId, path: filePath, content} = req.body; - const sshConn = sshSessions[sessionId]; - - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); - } - - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); - } - - if (!filePath) { - return res.status(400).json({error: 'File path is required'}); - } - - if (content === undefined) { - return res.status(400).json({error: 'File content is required'}); - } - - sshConn.lastActive = Date.now(); - - const trySFTP = () => { - try { - sshConn.client.sftp((err, sftp) => { - if (err) { - logger.warn(`SFTP failed, trying fallback method: ${err.message}`); - tryFallbackMethod(); - return; - } - - let fileBuffer; - try { - if (typeof content === 'string') { - fileBuffer = Buffer.from(content, 'utf8'); - } else if (Buffer.isBuffer(content)) { - fileBuffer = content; - } else { - fileBuffer = Buffer.from(content); - } - } catch (bufferErr) { - logger.error('Buffer conversion error:', bufferErr); - if (!res.headersSent) { - return res.status(500).json({error: 'Invalid file content format'}); - } - return; - } - - const writeStream = sftp.createWriteStream(filePath); - - let hasError = false; - let hasFinished = false; - - writeStream.on('error', (streamErr) => { - if (hasError || hasFinished) return; - hasError = true; - logger.warn(`SFTP write failed, trying fallback method: ${streamErr.message}`); - tryFallbackMethod(); - }); - - writeStream.on('finish', () => { - if (hasError || hasFinished) return; - hasFinished = true; - logger.success(`File written successfully via SFTP: ${filePath}`); - if (!res.headersSent) { - res.json({message: 'File written successfully', path: filePath}); - } - }); - - writeStream.on('close', () => { - if (hasError || hasFinished) return; - hasFinished = true; - logger.success(`File written successfully via SFTP: ${filePath}`); - if (!res.headersSent) { - res.json({message: 'File written successfully', path: filePath}); - } - }); - - try { - writeStream.write(fileBuffer); - writeStream.end(); - } catch (writeErr) { - if (hasError || hasFinished) return; - hasError = true; - logger.warn(`SFTP write operation failed, trying fallback method: ${writeErr.message}`); - tryFallbackMethod(); - } - }); - } catch (sftpErr) { - logger.warn(`SFTP connection error, trying fallback method: ${sftpErr.message}`); - tryFallbackMethod(); - } + client.on("ready", () => { + if (responseSent) return; + responseSent = true; + sshSessions[sessionId] = { + client, + isConnected: true, + lastActive: Date.now(), }; + res.json({ status: "success", message: "SSH connection established" }); + }); - const tryFallbackMethod = () => { + client.on("error", (err) => { + if (responseSent) return; + responseSent = true; + fileLogger.error("SSH connection failed for file manager", { + operation: "file_connect", + sessionId, + hostId, + ip, + port, + username, + error: err.message, + }); + res.status(500).json({ status: "error", message: err.message }); + }); + + client.on("close", () => { + if (sshSessions[sessionId]) sshSessions[sessionId].isConnected = false; + cleanupSession(sessionId); + }); + + client.connect(config); +}); + +app.post("/ssh/file_manager/ssh/disconnect", (req, res) => { + const { sessionId } = req.body; + cleanupSession(sessionId); + res.json({ status: "success", message: "SSH connection disconnected" }); +}); + +app.get("/ssh/file_manager/ssh/status", (req, res) => { + const sessionId = req.query.sessionId as string; + const isConnected = !!sshSessions[sessionId]?.isConnected; + res.json({ status: "success", connected: isConnected }); +}); + +app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { + const sessionId = req.query.sessionId as string; + const sshConn = sshSessions[sessionId]; + const sshPath = decodeURIComponent((req.query.path as string) || "/"); + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + sshConn.lastActive = Date.now(); + + const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); + sshConn.client.exec(`ls -la '${escapedPath}'`, (err, stream) => { + if (err) { + fileLogger.error("SSH listFiles error:", err); + return res.status(500).json({ error: err.message }); + } + + let data = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + data += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); + + stream.on("close", (code) => { + if (code !== 0) { + fileLogger.error( + `SSH listFiles command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + return res.status(500).json({ error: `Command failed: ${errorData}` }); + } + + const lines = data.split("\n").filter((line) => line.trim()); + const files = []; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(/\s+/); + if (parts.length >= 9) { + const permissions = parts[0]; + const name = parts.slice(8).join(" "); + const isDirectory = permissions.startsWith("d"); + const isLink = permissions.startsWith("l"); + + if (name === "." || name === "..") continue; + + files.push({ + name, + type: isDirectory ? "directory" : isLink ? "link" : "file", + }); + } + } + + res.json(files); + }); + }); +}); + +app.get("/ssh/file_manager/ssh/readFile", (req, res) => { + const sessionId = req.query.sessionId as string; + const sshConn = sshSessions[sessionId]; + const filePath = decodeURIComponent(req.query.path as string); + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + if (!filePath) { + return res.status(400).json({ error: "File path is required" }); + } + + sshConn.lastActive = Date.now(); + + const escapedPath = filePath.replace(/'/g, "'\"'\"'"); + sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => { + if (err) { + fileLogger.error("SSH readFile error:", err); + return res.status(500).json({ error: err.message }); + } + + let data = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + data += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); + + stream.on("close", (code) => { + if (code !== 0) { + fileLogger.error( + `SSH readFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + return res.status(500).json({ error: `Command failed: ${errorData}` }); + } + + res.json({ content: data, path: filePath }); + }); + }); +}); + +app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { + const { sessionId, path: filePath, content, hostId, userId } = req.body; + const sshConn = sshSessions[sessionId]; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + if (!filePath) { + return res.status(400).json({ error: "File path is required" }); + } + + if (content === undefined) { + return res.status(400).json({ error: "File content is required" }); + } + + sshConn.lastActive = Date.now(); + + const trySFTP = () => { + try { + sshConn.client.sftp((err, sftp) => { + if (err) { + fileLogger.warn( + `SFTP failed, trying fallback method: ${err.message}`, + ); + tryFallbackMethod(); + return; + } + + let fileBuffer; try { - const base64Content = Buffer.from(content, 'utf8').toString('base64'); - const escapedPath = filePath.replace(/'/g, "'\"'\"'"); + if (typeof content === "string") { + fileBuffer = Buffer.from(content, "utf8"); + } else if (Buffer.isBuffer(content)) { + fileBuffer = content; + } else { + fileBuffer = Buffer.from(content); + } + } catch (bufferErr) { + fileLogger.error("Buffer conversion error:", bufferErr); + if (!res.headersSent) { + return res + .status(500) + .json({ error: "Invalid file content format" }); + } + return; + } - const writeCommand = `echo '${base64Content}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`; + const writeStream = sftp.createWriteStream(filePath); - sshConn.client.exec(writeCommand, (err, stream) => { - if (err) { + let hasError = false; + let hasFinished = false; - logger.error('Fallback write command failed:', err); - if (!res.headersSent) { - return res.status(500).json({error: `Write failed: ${err.message}`}); - } - return; - } + writeStream.on("error", (streamErr) => { + if (hasError || hasFinished) return; + hasError = true; + fileLogger.warn( + `SFTP write failed, trying fallback method: ${streamErr.message}`, + ); + tryFallbackMethod(); + }); - 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')) { - logger.success(`File written successfully via fallback: ${filePath}`); - if (!res.headersSent) { - res.json({message: 'File written successfully', path: filePath}); - } - } else { - logger.error(`Fallback write failed with code ${code}: ${errorData}`); - if (!res.headersSent) { - res.status(500).json({error: `Write failed: ${errorData}`}); - } - } - }); - - stream.on('error', (streamErr) => { - - logger.error('Fallback write stream error:', streamErr); - if (!res.headersSent) { - res.status(500).json({error: `Write stream error: ${streamErr.message}`}); - } - }); + writeStream.on("finish", () => { + if (hasError || hasFinished) return; + hasFinished = true; + if (!res.headersSent) { + res.json({ + message: "File written successfully", + path: filePath, + toast: { type: "success", message: `File written: ${filePath}` }, }); - } catch (fallbackErr) { + } + }); - logger.error('Fallback method failed:', fallbackErr); + writeStream.on("close", () => { + if (hasError || hasFinished) return; + hasFinished = true; + 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 (sftpErr) { + fileLogger.warn( + `SFTP connection error, trying fallback method: ${sftpErr.message}`, + ); + tryFallbackMethod(); + } + }; + + const tryFallbackMethod = () => { + try { + const base64Content = Buffer.from(content, "utf8").toString("base64"); + const escapedPath = filePath.replace(/'/g, "'\"'\"'"); + + const writeCommand = `echo '${base64Content}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`; + + sshConn.client.exec(writeCommand, (err, stream) => { + if (err) { + fileLogger.error("Fallback write command failed:", err); + if (!res.headersSent) { + return res.status(500).json({ + error: `Write failed: ${err.message}`, + toast: { type: "error", message: `Write failed: ${err.message}` }, + }); + } + 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")) { if (!res.headersSent) { - res.status(500).json({error: `All write methods failed: ${fallbackErr.message}`}); + 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}`, + ); + if (!res.headersSent) { + res.status(500).json({ + error: `Write failed: ${errorData}`, + toast: { type: "error", message: `Write failed: ${errorData}` }, + }); + } + } + }); - trySFTP(); + stream.on("error", (streamErr) => { + fileLogger.error("Fallback write stream error:", streamErr); + if (!res.headersSent) { + res + .status(500) + .json({ error: `Write stream error: ${streamErr.message}` }); + } + }); + }); + } catch (fallbackErr) { + fileLogger.error("Fallback method failed:", fallbackErr); + if (!res.headersSent) { + res + .status(500) + .json({ error: `All write methods failed: ${fallbackErr.message}` }); + } + } + }; + + trySFTP(); }); -app.post('/ssh/file_manager/ssh/uploadFile', (req, res) => { - const {sessionId, path: filePath, content, fileName} = req.body; - const sshConn = sshSessions[sessionId]; +app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { + const { + sessionId, + path: filePath, + content, + fileName, + hostId, + userId, + } = req.body; + const sshConn = sshSessions[sessionId]; - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); - } + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); - } + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } - if (!filePath || !fileName || content === undefined) { - return res.status(400).json({error: 'File path, name, and content are required'}); - } + if (!filePath || !fileName || content === undefined) { + return res + .status(400) + .json({ error: "File path, name, and content are required" }); + } - sshConn.lastActive = Date.now(); - + sshConn.lastActive = Date.now(); - const fullPath = filePath.endsWith('/') ? filePath + fileName : filePath + '/' + fileName; + const fullPath = filePath.endsWith("/") + ? filePath + fileName + : filePath + "/" + fileName; - - - const trySFTP = () => { - try { - sshConn.client.sftp((err, sftp) => { - if (err) { - logger.warn(`SFTP failed, trying fallback method: ${err.message}`); - tryFallbackMethod(); - return; - } - - let fileBuffer; - try { - if (typeof content === 'string') { - fileBuffer = Buffer.from(content, 'utf8'); - } else if (Buffer.isBuffer(content)) { - fileBuffer = content; - } else { - fileBuffer = Buffer.from(content); - } - } catch (bufferErr) { - - logger.error('Buffer conversion error:', bufferErr); - if (!res.headersSent) { - return res.status(500).json({error: 'Invalid file content format'}); - } - return; - } - - const writeStream = sftp.createWriteStream(fullPath); - - let hasError = false; - let hasFinished = false; - - writeStream.on('error', (streamErr) => { - if (hasError || hasFinished) return; - hasError = true; - logger.warn(`SFTP write failed, trying fallback method: ${streamErr.message}`); - tryFallbackMethod(); - }); - - writeStream.on('finish', () => { - if (hasError || hasFinished) return; - hasFinished = true; - - logger.success(`File uploaded successfully via SFTP: ${fullPath}`); - if (!res.headersSent) { - res.json({message: 'File uploaded successfully', path: fullPath}); - } - }); - - writeStream.on('close', () => { - if (hasError || hasFinished) return; - hasFinished = true; - - logger.success(`File uploaded successfully via SFTP: ${fullPath}`); - if (!res.headersSent) { - res.json({message: 'File uploaded successfully', path: fullPath}); - } - }); - - try { - writeStream.write(fileBuffer); - writeStream.end(); - } catch (writeErr) { - if (hasError || hasFinished) return; - hasError = true; - logger.warn(`SFTP write operation failed, trying fallback method: ${writeErr.message}`); - tryFallbackMethod(); - } - }); - } catch (sftpErr) { - logger.warn(`SFTP connection error, trying fallback method: ${sftpErr.message}`); - tryFallbackMethod(); + const trySFTP = () => { + try { + sshConn.client.sftp((err, sftp) => { + if (err) { + fileLogger.warn( + `SFTP failed, trying fallback method: ${err.message}`, + ); + tryFallbackMethod(); + return; } - }; - const tryFallbackMethod = () => { + let fileBuffer; try { - const base64Content = Buffer.from(content, 'utf8').toString('base64'); - const chunkSize = 1000000; - const chunks = []; + if (typeof content === "string") { + fileBuffer = Buffer.from(content, "utf8"); + } else if (Buffer.isBuffer(content)) { + fileBuffer = content; + } else { + fileBuffer = Buffer.from(content); + } + } catch (bufferErr) { + fileLogger.error("Buffer conversion error:", bufferErr); + if (!res.headersSent) { + return res + .status(500) + .json({ error: "Invalid file content format" }); + } + return; + } - for (let i = 0; i < base64Content.length; i += chunkSize) { - chunks.push(base64Content.slice(i, i + chunkSize)); + const writeStream = sftp.createWriteStream(fullPath); + + 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; + if (!res.headersSent) { + res.json({ + message: "File uploaded successfully", + path: fullPath, + toast: { type: "success", message: `File uploaded: ${fullPath}` }, + }); + } + }); + + writeStream.on("close", () => { + if (hasError || hasFinished) return; + hasFinished = true; + if (!res.headersSent) { + res.json({ + message: "File uploaded successfully", + path: fullPath, + toast: { type: "success", message: `File uploaded: ${fullPath}` }, + }); + } + }); + + 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 (sftpErr) { + fileLogger.warn( + `SFTP connection error, trying fallback method: ${sftpErr.message}`, + ); + tryFallbackMethod(); + } + }; + + const tryFallbackMethod = () => { + try { + const base64Content = Buffer.from(content, "utf8").toString("base64"); + const chunkSize = 1000000; + const chunks = []; + + for (let i = 0; i < base64Content.length; i += chunkSize) { + chunks.push(base64Content.slice(i, i + chunkSize)); + } + + if (chunks.length === 1) { + const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'"); + const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); + + const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`; + + sshConn.client.exec(writeCommand, (err, stream) => { + if (err) { + fileLogger.error("Fallback upload command failed:", err); + if (!res.headersSent) { + return res + .status(500) + .json({ error: `Upload failed: ${err.message}` }); } + return; + } - if (chunks.length === 1) { - const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'"); - const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); + let outputData = ""; + let errorData = ""; - const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`; + stream.on("data", (chunk: Buffer) => { + outputData += chunk.toString(); + }); - sshConn.client.exec(writeCommand, (err, stream) => { - if (err) { - - logger.error('Fallback upload command failed:', err); - if (!res.headersSent) { - return res.status(500).json({error: `Upload failed: ${err.message}`}); - } - return; - } + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); - 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')) { - logger.success(`File uploaded successfully via fallback: ${fullPath}`); - if (!res.headersSent) { - res.json({message: 'File uploaded successfully', path: fullPath}); - } - } else { - logger.error(`Fallback upload failed with code ${code}: ${errorData}`); - if (!res.headersSent) { - res.status(500).json({error: `Upload failed: ${errorData}`}); - } - } - }); - - stream.on('error', (streamErr) => { - - logger.error('Fallback upload stream error:', streamErr); - if (!res.headersSent) { - res.status(500).json({error: `Upload stream error: ${streamErr.message}`}); - } - }); + stream.on("close", (code) => { + if (outputData.includes("SUCCESS")) { + if (!res.headersSent) { + res.json({ + message: "File uploaded successfully", + path: fullPath, + toast: { + type: "success", + message: `File uploaded: ${fullPath}`, + }, }); + } } else { - const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; - const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'"); - const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); - - let writeCommand = `> '${escapedPath}'`; - - chunks.forEach((chunk, index) => { - writeCommand += ` && echo '${chunk}' | base64 -d >> '${escapedPath}'`; + fileLogger.error( + `Fallback upload failed with code ${code}: ${errorData}`, + ); + if (!res.headersSent) { + res.status(500).json({ + error: `Upload failed: ${errorData}`, + toast: { + type: "error", + message: `Upload failed: ${errorData}`, + }, }); + } + } + }); - writeCommand += ` && echo "SUCCESS"`; + stream.on("error", (streamErr) => { + fileLogger.error("Fallback upload stream error:", streamErr); + if (!res.headersSent) { + res + .status(500) + .json({ error: `Upload stream error: ${streamErr.message}` }); + } + }); + }); + } else { + const tempFile = `/tmp/upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const escapedTempFile = tempFile.replace(/'/g, "'\"'\"'"); + const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); - sshConn.client.exec(writeCommand, (err, stream) => { - if (err) { - - logger.error('Chunked fallback upload failed:', err); - if (!res.headersSent) { - return res.status(500).json({error: `Chunked upload failed: ${err.message}`}); - } - return; - } + let writeCommand = `> '${escapedPath}'`; - let outputData = ''; - let errorData = ''; + chunks.forEach((chunk, index) => { + writeCommand += ` && echo '${chunk}' | base64 -d >> '${escapedPath}'`; + }); - stream.on('data', (chunk: Buffer) => { - outputData += chunk.toString(); - }); + writeCommand += ` && echo "SUCCESS"`; - stream.stderr.on('data', (chunk: Buffer) => { - errorData += chunk.toString(); - }); + sshConn.client.exec(writeCommand, (err, stream) => { + if (err) { + fileLogger.error("Chunked fallback upload failed:", err); + if (!res.headersSent) { + return res + .status(500) + .json({ error: `Chunked upload failed: ${err.message}` }); + } + return; + } - stream.on('close', (code) => { - + let outputData = ""; + let errorData = ""; - if (outputData.includes('SUCCESS')) { - logger.success(`File uploaded successfully via chunked fallback: ${fullPath}`); - if (!res.headersSent) { - res.json({message: 'File uploaded successfully', path: fullPath}); - } - } else { - logger.error(`Chunked fallback upload failed with code ${code}: ${errorData}`); - if (!res.headersSent) { - res.status(500).json({error: `Chunked upload failed: ${errorData}`}); - } - } - }); + stream.on("data", (chunk: Buffer) => { + outputData += chunk.toString(); + }); - stream.on('error', (streamErr) => { - logger.error('Chunked fallback upload stream error:', streamErr); - if (!res.headersSent) { - res.status(500).json({error: `Chunked upload stream error: ${streamErr.message}`}); - } - }); + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + }); + + stream.on("close", (code) => { + if (outputData.includes("SUCCESS")) { + if (!res.headersSent) { + res.json({ + message: "File uploaded successfully", + path: fullPath, + toast: { + type: "success", + message: `File uploaded: ${fullPath}`, + }, }); + } + } else { + fileLogger.error( + `Chunked fallback upload failed with code ${code}: ${errorData}`, + ); + if (!res.headersSent) { + res.status(500).json({ + error: `Chunked upload failed: ${errorData}`, + toast: { + type: "error", + message: `Chunked upload failed: ${errorData}`, + }, + }); + } } - } catch (fallbackErr) { - logger.error('Fallback method failed:', fallbackErr); - if (!res.headersSent) { - res.status(500).json({error: `All upload methods failed: ${fallbackErr.message}`}); - } - } - }; + }); - trySFTP(); + stream.on("error", (streamErr) => { + fileLogger.error( + "Chunked fallback upload stream error:", + streamErr, + ); + if (!res.headersSent) { + res.status(500).json({ + error: `Chunked upload stream error: ${streamErr.message}`, + }); + } + }); + }); + } + } catch (fallbackErr) { + fileLogger.error("Fallback method failed:", fallbackErr); + if (!res.headersSent) { + res + .status(500) + .json({ error: `All upload methods failed: ${fallbackErr.message}` }); + } + } + }; + + trySFTP(); }); -app.post('/ssh/file_manager/ssh/createFile', (req, res) => { - const {sessionId, path: filePath, fileName, content = ''} = req.body; - const sshConn = sshSessions[sessionId]; +app.post("/ssh/file_manager/ssh/createFile", async (req, res) => { + const { + sessionId, + path: filePath, + fileName, + content = "", + hostId, + userId, + } = req.body; + const sshConn = sshSessions[sessionId]; - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + if (!filePath || !fileName) { + return res.status(400).json({ error: "File path and name are required" }); + } + + sshConn.lastActive = Date.now(); + + const fullPath = filePath.endsWith("/") + ? filePath + fileName + : filePath + "/" + fileName; + const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); + + const createCommand = `touch '${escapedPath}' && echo "SUCCESS" && exit 0`; + + sshConn.client.exec(createCommand, (err, stream) => { + if (err) { + fileLogger.error("SSH createFile error:", err); + if (!res.headersSent) { + return res.status(500).json({ error: err.message }); + } + return; } - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); - } + let outputData = ""; + let errorData = ""; - if (!filePath || !fileName) { - return res.status(400).json({error: 'File path and name are required'}); - } - - sshConn.lastActive = Date.now(); - - const fullPath = filePath.endsWith('/') ? filePath + fileName : filePath + '/' + fileName; - const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); - - const createCommand = `touch '${escapedPath}' && echo "SUCCESS" && exit 0`; - - sshConn.client.exec(createCommand, (err, stream) => { - if (err) { - logger.error('SSH createFile error:', err); - if (!res.headersSent) { - return res.status(500).json({error: err.message}); - } - return; - } - - let outputData = ''; - let errorData = ''; - - stream.on('data', (chunk: Buffer) => { - outputData += chunk.toString(); - }); - - stream.stderr.on('data', (chunk: Buffer) => { - errorData += chunk.toString(); - - if (chunk.toString().includes('Permission denied')) { - logger.error(`Permission denied creating file: ${fullPath}`); - if (!res.headersSent) { - return res.status(403).json({ - error: `Permission denied: Cannot create file ${fullPath}. Check directory permissions.` - }); - } - return; - } - }); - - stream.on('close', (code) => { - if (outputData.includes('SUCCESS')) { - if (!res.headersSent) { - res.json({message: 'File created successfully', path: fullPath}); - } - return; - } - - if (code !== 0) { - logger.error(`SSH createFile command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`); - if (!res.headersSent) { - return res.status(500).json({error: `Command failed: ${errorData}`}); - } - return; - } - - if (!res.headersSent) { - res.json({message: 'File created successfully', path: fullPath}); - } - }); - - stream.on('error', (streamErr) => { - logger.error('SSH createFile stream error:', streamErr); - if (!res.headersSent) { - res.status(500).json({error: `Stream error: ${streamErr.message}`}); - } - }); + stream.on("data", (chunk: Buffer) => { + outputData += chunk.toString(); }); -}); -app.post('/ssh/file_manager/ssh/createFolder', (req, res) => { - const {sessionId, path: folderPath, folderName} = req.body; - const sshConn = sshSessions[sessionId]; + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); - } - - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); - } - - if (!folderPath || !folderName) { - return res.status(400).json({error: 'Folder path and name are required'}); - } - - sshConn.lastActive = Date.now(); - - const fullPath = folderPath.endsWith('/') ? folderPath + folderName : folderPath + '/' + folderName; - const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); - - const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`; - - sshConn.client.exec(createCommand, (err, stream) => { - if (err) { - - logger.error('SSH createFolder error:', err); - if (!res.headersSent) { - return res.status(500).json({error: err.message}); - } - return; + if (chunk.toString().includes("Permission denied")) { + fileLogger.error(`Permission denied creating file: ${fullPath}`); + if (!res.headersSent) { + return res.status(403).json({ + error: `Permission denied: Cannot create file ${fullPath}. Check directory permissions.`, + }); } - - let outputData = ''; - let errorData = ''; - - stream.on('data', (chunk: Buffer) => { - outputData += chunk.toString(); - }); - - stream.stderr.on('data', (chunk: Buffer) => { - errorData += chunk.toString(); - - if (chunk.toString().includes('Permission denied')) { - logger.error(`Permission denied creating folder: ${fullPath}`); - if (!res.headersSent) { - return res.status(403).json({ - error: `Permission denied: Cannot create folder ${fullPath}. Check directory permissions.` - }); - } - return; - } - }); - - stream.on('close', (code) => { - if (outputData.includes('SUCCESS')) { - if (!res.headersSent) { - res.json({message: 'Folder created successfully', path: fullPath}); - } - return; - } - - if (code !== 0) { - logger.error(`SSH createFolder command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`); - if (!res.headersSent) { - return res.status(500).json({error: `Command failed: ${errorData}`}); - } - return; - } - - if (!res.headersSent) { - res.json({message: 'Folder created successfully', path: fullPath}); - } - }); - - stream.on('error', (streamErr) => { - logger.error('SSH createFolder stream error:', streamErr); - if (!res.headersSent) { - res.status(500).json({error: `Stream error: ${streamErr.message}`}); - } - }); + return; + } }); -}); -app.delete('/ssh/file_manager/ssh/deleteItem', (req, res) => { - const {sessionId, path: itemPath, isDirectory} = req.body; - const sshConn = sshSessions[sessionId]; - - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); - } - - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); - } - - if (!itemPath) { - return res.status(400).json({error: 'Item path is required'}); - } - - sshConn.lastActive = Date.now(); - const escapedPath = itemPath.replace(/'/g, "'\"'\"'"); - - const deleteCommand = isDirectory - ? `rm -rf '${escapedPath}' && echo "SUCCESS" && exit 0` - : `rm -f '${escapedPath}' && echo "SUCCESS" && exit 0`; - - sshConn.client.exec(deleteCommand, (err, stream) => { - if (err) { - logger.error('SSH deleteItem error:', err); - if (!res.headersSent) { - return res.status(500).json({error: err.message}); - } - return; + stream.on("close", (code) => { + if (outputData.includes("SUCCESS")) { + if (!res.headersSent) { + res.json({ + message: "File created successfully", + path: fullPath, + toast: { type: "success", message: `File created: ${fullPath}` }, + }); } + return; + } - let outputData = ''; - let errorData = ''; - - stream.on('data', (chunk: Buffer) => { - outputData += chunk.toString(); - }); - - stream.stderr.on('data', (chunk: Buffer) => { - errorData += chunk.toString(); - - if (chunk.toString().includes('Permission denied')) { - logger.error(`Permission denied deleting: ${itemPath}`); - if (!res.headersSent) { - return res.status(403).json({ - error: `Permission denied: Cannot delete ${itemPath}. Check file permissions.` - }); - } - return; - } - }); - - stream.on('close', (code) => { - if (outputData.includes('SUCCESS')) { - if (!res.headersSent) { - res.json({message: 'Item deleted successfully', path: itemPath}); - } - return; - } - - if (code !== 0) { - logger.error(`SSH deleteItem command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`); - if (!res.headersSent) { - return res.status(500).json({error: `Command failed: ${errorData}`}); - } - return; - } - - if (!res.headersSent) { - res.json({message: 'Item deleted successfully', path: itemPath}); - } - }); - - stream.on('error', (streamErr) => { - logger.error('SSH deleteItem stream error:', streamErr); - if (!res.headersSent) { - res.status(500).json({error: `Stream error: ${streamErr.message}`}); - } - }); - }); -}); - -app.put('/ssh/file_manager/ssh/renameItem', (req, res) => { - const {sessionId, oldPath, newName} = req.body; - const sshConn = sshSessions[sessionId]; - - if (!sessionId) { - return res.status(400).json({error: 'Session ID is required'}); - } - - if (!sshConn?.isConnected) { - return res.status(400).json({error: 'SSH connection not established'}); - } - - if (!oldPath || !newName) { - return res.status(400).json({error: 'Old path and new name are required'}); - } - - sshConn.lastActive = Date.now(); - - const oldDir = oldPath.substring(0, oldPath.lastIndexOf('/') + 1); - const newPath = oldDir + newName; - const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'"); - const escapedNewPath = newPath.replace(/'/g, "'\"'\"'"); - - const renameCommand = `mv '${escapedOldPath}' '${escapedNewPath}' && echo "SUCCESS" && exit 0`; - - sshConn.client.exec(renameCommand, (err, stream) => { - if (err) { - logger.error('SSH renameItem error:', err); - if (!res.headersSent) { - return res.status(500).json({error: err.message}); - } - return; + if (code !== 0) { + fileLogger.error( + `SSH createFile command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + if (!res.headersSent) { + return res.status(500).json({ + error: `Command failed: ${errorData}`, + toast: { + type: "error", + message: `File creation failed: ${errorData}`, + }, + }); } + return; + } - let outputData = ''; - let errorData = ''; - - stream.on('data', (chunk: Buffer) => { - outputData += chunk.toString(); - }); - - stream.stderr.on('data', (chunk: Buffer) => { - errorData += chunk.toString(); - - if (chunk.toString().includes('Permission denied')) { - logger.error(`Permission denied renaming: ${oldPath}`); - if (!res.headersSent) { - return res.status(403).json({ - error: `Permission denied: Cannot rename ${oldPath}. Check file permissions.` - }); - } - return; - } - }); - - stream.on('close', (code) => { - if (outputData.includes('SUCCESS')) { - if (!res.headersSent) { - res.json({message: 'Item renamed successfully', oldPath, newPath}); - } - return; - } - - if (code !== 0) { - logger.error(`SSH renameItem command failed with code ${code}: ${errorData.replace(/\n/g, ' ').trim()}`); - if (!res.headersSent) { - return res.status(500).json({error: `Command failed: ${errorData}`}); - } - return; - } - - if (!res.headersSent) { - res.json({message: 'Item renamed successfully', oldPath, newPath}); - } - }); - - stream.on('error', (streamErr) => { - logger.error('SSH renameItem stream error:', streamErr); - if (!res.headersSent) { - res.status(500).json({error: `Stream error: ${streamErr.message}`}); - } + if (!res.headersSent) { + res.json({ + message: "File created successfully", + path: fullPath, + toast: { type: "success", message: `File created: ${fullPath}` }, }); + } }); + + stream.on("error", (streamErr) => { + fileLogger.error("SSH createFile stream error:", streamErr); + if (!res.headersSent) { + res.status(500).json({ error: `Stream error: ${streamErr.message}` }); + } + }); + }); }); -process.on('SIGINT', () => { - Object.keys(sshSessions).forEach(cleanupSession); - process.exit(0); +app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { + const { sessionId, path: folderPath, folderName, hostId, userId } = req.body; + const sshConn = sshSessions[sessionId]; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + if (!folderPath || !folderName) { + return res.status(400).json({ error: "Folder path and name are required" }); + } + + sshConn.lastActive = Date.now(); + + const fullPath = folderPath.endsWith("/") + ? folderPath + folderName + : folderPath + "/" + folderName; + const escapedPath = fullPath.replace(/'/g, "'\"'\"'"); + + const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`; + + sshConn.client.exec(createCommand, (err, stream) => { + if (err) { + fileLogger.error("SSH createFolder error:", err); + if (!res.headersSent) { + return res.status(500).json({ error: err.message }); + } + return; + } + + let outputData = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + outputData += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + + if (chunk.toString().includes("Permission denied")) { + fileLogger.error(`Permission denied creating folder: ${fullPath}`); + if (!res.headersSent) { + return res.status(403).json({ + error: `Permission denied: Cannot create folder ${fullPath}. Check directory permissions.`, + }); + } + return; + } + }); + + stream.on("close", (code) => { + if (outputData.includes("SUCCESS")) { + if (!res.headersSent) { + res.json({ + message: "Folder created successfully", + path: fullPath, + toast: { type: "success", message: `Folder created: ${fullPath}` }, + }); + } + return; + } + + if (code !== 0) { + fileLogger.error( + `SSH createFolder command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + if (!res.headersSent) { + return res.status(500).json({ + error: `Command failed: ${errorData}`, + toast: { + type: "error", + message: `Folder creation failed: ${errorData}`, + }, + }); + } + return; + } + + if (!res.headersSent) { + res.json({ + message: "Folder created successfully", + path: fullPath, + toast: { type: "success", message: `Folder created: ${fullPath}` }, + }); + } + }); + + stream.on("error", (streamErr) => { + fileLogger.error("SSH createFolder stream error:", streamErr); + if (!res.headersSent) { + res.status(500).json({ error: `Stream error: ${streamErr.message}` }); + } + }); + }); }); -process.on('SIGTERM', () => { - Object.keys(sshSessions).forEach(cleanupSession); - process.exit(0); +app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { + const { sessionId, path: itemPath, isDirectory, hostId, userId } = req.body; + const sshConn = sshSessions[sessionId]; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + if (!itemPath) { + return res.status(400).json({ error: "Item path is required" }); + } + + sshConn.lastActive = Date.now(); + const escapedPath = itemPath.replace(/'/g, "'\"'\"'"); + + const deleteCommand = isDirectory + ? `rm -rf '${escapedPath}' && echo "SUCCESS" && exit 0` + : `rm -f '${escapedPath}' && echo "SUCCESS" && exit 0`; + + sshConn.client.exec(deleteCommand, (err, stream) => { + if (err) { + fileLogger.error("SSH deleteItem error:", err); + if (!res.headersSent) { + return res.status(500).json({ error: err.message }); + } + return; + } + + let outputData = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + outputData += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + + if (chunk.toString().includes("Permission denied")) { + fileLogger.error(`Permission denied deleting: ${itemPath}`); + if (!res.headersSent) { + return res.status(403).json({ + error: `Permission denied: Cannot delete ${itemPath}. Check file permissions.`, + }); + } + return; + } + }); + + stream.on("close", (code) => { + if (outputData.includes("SUCCESS")) { + if (!res.headersSent) { + res.json({ + message: "Item deleted successfully", + path: itemPath, + toast: { + type: "success", + message: `${isDirectory ? "Directory" : "File"} deleted: ${itemPath}`, + }, + }); + } + return; + } + + if (code !== 0) { + fileLogger.error( + `SSH deleteItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + if (!res.headersSent) { + return res.status(500).json({ + error: `Command failed: ${errorData}`, + toast: { type: "error", message: `Delete failed: ${errorData}` }, + }); + } + return; + } + + if (!res.headersSent) { + res.json({ + message: "Item deleted successfully", + path: itemPath, + toast: { + type: "success", + message: `${isDirectory ? "Directory" : "File"} deleted: ${itemPath}`, + }, + }); + } + }); + + stream.on("error", (streamErr) => { + fileLogger.error("SSH deleteItem stream error:", streamErr); + if (!res.headersSent) { + res.status(500).json({ error: `Stream error: ${streamErr.message}` }); + } + }); + }); +}); + +app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { + const { sessionId, oldPath, newName, hostId, userId } = req.body; + const sshConn = sshSessions[sessionId]; + + if (!sessionId) { + return res.status(400).json({ error: "Session ID is required" }); + } + + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH connection not established" }); + } + + if (!oldPath || !newName) { + return res + .status(400) + .json({ error: "Old path and new name are required" }); + } + + sshConn.lastActive = Date.now(); + + const oldDir = oldPath.substring(0, oldPath.lastIndexOf("/") + 1); + const newPath = oldDir + newName; + const escapedOldPath = oldPath.replace(/'/g, "'\"'\"'"); + const escapedNewPath = newPath.replace(/'/g, "'\"'\"'"); + + const renameCommand = `mv '${escapedOldPath}' '${escapedNewPath}' && echo "SUCCESS" && exit 0`; + + sshConn.client.exec(renameCommand, (err, stream) => { + if (err) { + fileLogger.error("SSH renameItem error:", err); + if (!res.headersSent) { + return res.status(500).json({ error: err.message }); + } + return; + } + + let outputData = ""; + let errorData = ""; + + stream.on("data", (chunk: Buffer) => { + outputData += chunk.toString(); + }); + + stream.stderr.on("data", (chunk: Buffer) => { + errorData += chunk.toString(); + + if (chunk.toString().includes("Permission denied")) { + fileLogger.error(`Permission denied renaming: ${oldPath}`); + if (!res.headersSent) { + return res.status(403).json({ + error: `Permission denied: Cannot rename ${oldPath}. Check file permissions.`, + }); + } + return; + } + }); + + stream.on("close", (code) => { + if (outputData.includes("SUCCESS")) { + if (!res.headersSent) { + res.json({ + message: "Item renamed successfully", + oldPath, + newPath, + toast: { + type: "success", + message: `Item renamed: ${oldPath} -> ${newPath}`, + }, + }); + } + return; + } + + if (code !== 0) { + fileLogger.error( + `SSH renameItem command failed with code ${code}: ${errorData.replace(/\n/g, " ").trim()}`, + ); + if (!res.headersSent) { + return res.status(500).json({ + error: `Command failed: ${errorData}`, + toast: { type: "error", message: `Rename failed: ${errorData}` }, + }); + } + return; + } + + if (!res.headersSent) { + res.json({ + message: "Item renamed successfully", + oldPath, + newPath, + toast: { + type: "success", + message: `Item renamed: ${oldPath} -> ${newPath}`, + }, + }); + } + }); + + stream.on("error", (streamErr) => { + fileLogger.error("SSH renameItem stream error:", streamErr); + if (!res.headersSent) { + res.status(500).json({ error: `Stream error: ${streamErr.message}` }); + } + }); + }); +}); + +process.on("SIGINT", () => { + Object.keys(sshSessions).forEach(cleanupSession); + process.exit(0); +}); + +process.on("SIGTERM", () => { + Object.keys(sshSessions).forEach(cleanupSession); + process.exit(0); }); const PORT = 8084; app.listen(PORT, () => { -}); \ No newline at end of file + fileLogger.success("File Manager API server started", { + operation: "server_start", + port: PORT, + }); +}); diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 8823fd56..bdc8ec50 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -1,454 +1,909 @@ -import express from 'express'; -import chalk from 'chalk'; -import fetch from 'node-fetch'; -import net from 'net'; -import cors from 'cors'; -import {Client, type ConnectConfig} from 'ssh2'; +import express from "express"; +import net from "net"; +import cors from "cors"; +import { Client, type ConnectConfig } from "ssh2"; +import { db } from "../database/db/index.js"; +import { sshData, sshCredentials } from "../database/db/schema.js"; +import { eq, and } from "drizzle-orm"; +import { statsLogger } from "../utils/logger.js"; -type HostRecord = { - id: number; - ip: string; - port: number; - username?: string; - authType?: 'password' | 'key' | string; - password?: string | null; - key?: string | null; - keyPassword?: string | null; - keyType?: string | null; -}; +interface PooledConnection { + client: Client; + lastUsed: number; + inUse: boolean; + hostKey: string; +} -type HostStatus = 'online' | 'offline'; +class SSHConnectionPool { + private connections = new Map(); + private maxConnectionsPerHost = 3; + private connectionTimeout = 30000; + private cleanupInterval: NodeJS.Timeout; + + constructor() { + this.cleanupInterval = setInterval( + () => { + this.cleanup(); + }, + 5 * 60 * 1000, + ); + } + + private getHostKey(host: SSHHostWithCredentials): string { + return `${host.ip}:${host.port}:${host.username}`; + } + + async getConnection(host: SSHHostWithCredentials): Promise { + const hostKey = this.getHostKey(host); + const connections = this.connections.get(hostKey) || []; + + const available = connections.find((conn) => !conn.inUse); + if (available) { + available.inUse = true; + available.lastUsed = Date.now(); + return available.client; + } + + if (connections.length < this.maxConnectionsPerHost) { + const client = await this.createConnection(host); + const pooled: PooledConnection = { + client, + lastUsed: Date.now(), + inUse: true, + hostKey, + }; + connections.push(pooled); + this.connections.set(hostKey, connections); + return client; + } + + return new Promise((resolve, reject) => { + const checkAvailable = () => { + const available = connections.find((conn) => !conn.inUse); + if (available) { + available.inUse = true; + available.lastUsed = Date.now(); + resolve(available.client); + } else { + setTimeout(checkAvailable, 100); + } + }; + checkAvailable(); + }); + } + + private async createConnection( + host: SSHHostWithCredentials, + ): Promise { + return new Promise((resolve, reject) => { + const client = new Client(); + const timeout = setTimeout(() => { + client.end(); + reject(new Error("SSH connection timeout")); + }, this.connectionTimeout); + + client.on("ready", () => { + clearTimeout(timeout); + resolve(client); + }); + + client.on("error", (err) => { + clearTimeout(timeout); + reject(err); + }); + + try { + client.connect(buildSshConfig(host)); + } catch (err) { + clearTimeout(timeout); + reject(err); + } + }); + } + + releaseConnection(host: SSHHostWithCredentials, client: Client): void { + const hostKey = this.getHostKey(host); + const connections = this.connections.get(hostKey) || []; + const pooled = connections.find((conn) => conn.client === client); + if (pooled) { + pooled.inUse = false; + pooled.lastUsed = Date.now(); + } + } + + private cleanup(): void { + const now = Date.now(); + const maxAge = 10 * 60 * 1000; + + for (const [hostKey, connections] of this.connections.entries()) { + const activeConnections = connections.filter((conn) => { + if (!conn.inUse && now - conn.lastUsed > maxAge) { + try { + conn.client.end(); + } catch {} + return false; + } + return true; + }); + + if (activeConnections.length === 0) { + this.connections.delete(hostKey); + } else { + this.connections.set(hostKey, activeConnections); + } + } + } + + destroy(): void { + clearInterval(this.cleanupInterval); + for (const connections of this.connections.values()) { + for (const conn of connections) { + try { + conn.client.end(); + } catch {} + } + } + this.connections.clear(); + } +} + +class RequestQueue { + private queues = new Map Promise>>(); + private processing = new Set(); + + async queueRequest(hostId: number, request: () => Promise): Promise { + return new Promise((resolve, reject) => { + const queue = this.queues.get(hostId) || []; + queue.push(async () => { + try { + const result = await request(); + resolve(result); + } catch (error) { + reject(error); + } + }); + this.queues.set(hostId, queue); + this.processQueue(hostId); + }); + } + + private async processQueue(hostId: number): Promise { + if (this.processing.has(hostId)) return; + + this.processing.add(hostId); + const queue = this.queues.get(hostId) || []; + + while (queue.length > 0) { + const request = queue.shift(); + if (request) { + try { + await request(); + } catch (error) {} + } + } + + this.processing.delete(hostId); + if (queue.length > 0) { + this.processQueue(hostId); + } + } +} + +interface CachedMetrics { + data: any; + timestamp: number; + hostId: number; +} + +class MetricsCache { + private cache = new Map(); + private ttl = 30000; + + get(hostId: number): any | null { + const cached = this.cache.get(hostId); + if (cached && Date.now() - cached.timestamp < this.ttl) { + return cached.data; + } + return null; + } + + set(hostId: number, data: any): void { + this.cache.set(hostId, { + data, + timestamp: Date.now(), + hostId, + }); + } + + clear(hostId?: number): void { + if (hostId) { + this.cache.delete(hostId); + } else { + this.cache.clear(); + } + } +} + +const connectionPool = new SSHConnectionPool(); +const requestQueue = new RequestQueue(); +const metricsCache = new MetricsCache(); + +type HostStatus = "online" | "offline"; + +interface SSHHostWithCredentials { + id: number; + name: string; + ip: string; + port: number; + username: string; + folder: string; + tags: string[]; + pin: boolean; + authType: string; + password?: string; + key?: string; + keyPassword?: string; + keyType?: string; + credentialId?: number; + enableTerminal: boolean; + enableTunnel: boolean; + enableFileManager: boolean; + defaultPath: string; + tunnelConnections: any[]; + createdAt: string; + updatedAt: string; + userId: string; +} type StatusEntry = { - status: HostStatus; - lastChecked: string; + status: HostStatus; + lastChecked: string; }; +function validateHostId( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) { + const id = Number(req.params.id); + if (!id || !Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: "Invalid host ID" }); + } + next(); +} + const app = express(); -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] -})); +app.use( + cors({ + origin: "*", + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Content-Type", + "Authorization", + "User-Agent", + "X-Electron-App", + ], + }), +); app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS'); - if (req.method === 'OPTIONS') { - return res.sendStatus(204); - } - next(); + res.header("Access-Control-Allow-Origin", "*"); + res.header( + "Access-Control-Allow-Headers", + "Content-Type, Authorization, User-Agent, X-Electron-App", + ); + res.header( + "Access-Control-Allow-Methods", + "GET, POST, PUT, PATCH, DELETE, OPTIONS", + ); + if (req.method === "OPTIONS") { + return res.sendStatus(204); + } + next(); }); -app.use(express.json()); - -const statsIconSymbol = '📡'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#22c55e')(`[${statsIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; +app.use(express.json({ limit: "1mb" })); const hostStatuses: Map = new Map(); -async function fetchAllHosts(): Promise { - const url = 'http://localhost:8081/ssh/db/host/internal'; +async function fetchAllHosts(): Promise { + try { + const hosts = await db.select().from(sshData); + + const hostsWithCredentials: SSHHostWithCredentials[] = []; + for (const host of hosts) { + try { + const hostWithCreds = await resolveHostCredentials(host); + if (hostWithCreds) { + hostsWithCredentials.push(hostWithCreds); + } + } catch (err) { + statsLogger.warn( + `Failed to resolve credentials for host ${host.id}: ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } + } + + return hostsWithCredentials.filter((h) => !!h.id && !!h.ip && !!h.port); + } catch (err) { + statsLogger.error("Failed to fetch hosts from database", err); + return []; + } +} + +async function fetchHostById( + id: number, +): Promise { + try { + const hosts = await db.select().from(sshData).where(eq(sshData.id, id)); + + if (hosts.length === 0) { + return undefined; + } + + const host = hosts[0]; + return await resolveHostCredentials(host); + } catch (err) { + statsLogger.error(`Failed to fetch host ${id}`, err); + return undefined; + } +} + +async function resolveHostCredentials( + host: any, +): Promise { + try { + const baseHost: any = { + id: host.id, + name: host.name, + ip: host.ip, + port: host.port, + username: host.username, + folder: host.folder || "", + tags: + typeof host.tags === "string" + ? host.tags + ? host.tags.split(",").filter(Boolean) + : [] + : [], + pin: !!host.pin, + authType: host.authType, + enableTerminal: !!host.enableTerminal, + enableTunnel: !!host.enableTunnel, + enableFileManager: !!host.enableFileManager, + defaultPath: host.defaultPath || "/", + tunnelConnections: host.tunnelConnections + ? JSON.parse(host.tunnelConnections) + : [], + createdAt: host.createdAt, + updatedAt: host.updatedAt, + userId: host.userId, + }; + + if (host.credentialId) { + try { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, host.credentialId), + eq(sshCredentials.userId, host.userId), + ), + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + baseHost.credentialId = credential.id; + baseHost.username = credential.username; + baseHost.authType = credential.authType; + + if (credential.password) { + baseHost.password = credential.password; + } + if (credential.key) { + baseHost.key = credential.key; + } + if (credential.keyPassword) { + baseHost.keyPassword = credential.keyPassword; + } + if (credential.keyType) { + baseHost.keyType = credential.keyType; + } + } else { + statsLogger.warn( + `Credential ${host.credentialId} not found for host ${host.id}, using legacy data`, + ); + addLegacyCredentials(baseHost, host); + } + } catch (error) { + statsLogger.warn( + `Failed to resolve credential ${host.credentialId} for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + addLegacyCredentials(baseHost, host); + } + } else { + addLegacyCredentials(baseHost, host); + } + + return baseHost; + } catch (error) { + statsLogger.error( + `Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + return undefined; + } +} + +function addLegacyCredentials(baseHost: any, host: any): void { + baseHost.password = host.password || null; + baseHost.key = host.key || null; + baseHost.keyPassword = host.keyPassword || null; + baseHost.keyType = host.keyType; +} + +function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig { + const base: ConnectConfig = { + host: host.ip, + port: host.port || 22, + username: host.username || "root", + readyTimeout: 10_000, + algorithms: {}, + } as ConnectConfig; + + if (host.authType === "password") { + if (!host.password) { + throw new Error(`No password available for host ${host.ip}`); + } + (base as any).password = host.password; + } else if (host.authType === "key") { + if (!host.key) { + throw new Error(`No SSH key available for host ${host.ip}`); + } + try { - const resp = await fetch(url, { - headers: {'x-internal-request': '1'} - }); - if (!resp.ok) { - throw new Error(`DB service error: ${resp.status} ${resp.statusText}`); - } - const data = await resp.json(); - const hosts: HostRecord[] = (Array.isArray(data) ? data : []).map((h: any) => ({ - id: Number(h.id), - ip: String(h.ip), - port: Number(h.port) || 22, - username: h.username, - authType: h.authType, - password: h.password ?? null, - key: h.key ?? null, - keyPassword: h.keyPassword ?? null, - keyType: h.keyType ?? null, - })).filter(h => !!h.id && !!h.ip && !!h.port); - return hosts; - } catch (err) { - logger.error('Failed to fetch hosts from database service', err); - return []; + if (!host.key.includes("-----BEGIN") || !host.key.includes("-----END")) { + throw new Error("Invalid private key format"); + } + + const cleanKey = host.key + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + + (base as any).privateKey = Buffer.from(cleanKey, "utf8"); + + if (host.keyPassword) { + (base as any).passphrase = host.keyPassword; + } + } catch (keyError) { + statsLogger.error( + `SSH key format error for host ${host.ip}: ${keyError instanceof Error ? keyError.message : "Unknown error"}`, + ); + throw new Error(`Invalid SSH key format for host ${host.ip}`); } + } else { + throw new Error( + `Unsupported authentication type '${host.authType}' for host ${host.ip}`, + ); + } + + return base; } -async function fetchHostById(id: number): Promise { - const all = await fetchAllHosts(); - return all.find(h => h.id === id); +async function withSshConnection( + host: SSHHostWithCredentials, + fn: (client: Client) => Promise, +): Promise { + const client = await connectionPool.getConnection(host); + try { + const result = await fn(client); + return result; + } finally { + connectionPool.releaseConnection(host, client); + } } -function buildSshConfig(host: HostRecord): ConnectConfig { - const base: ConnectConfig = { - host: host.ip, - port: host.port || 22, - username: host.username || 'root', - readyTimeout: 10_000, - algorithms: {} - } as ConnectConfig; - - if (host.authType === 'password') { - (base as any).password = host.password || ''; - } else if (host.authType === 'key') { - if (host.key) { - try { - if (!host.key.includes('-----BEGIN') || !host.key.includes('-----END')) { - throw new Error('Invalid private key format'); - } - - const cleanKey = host.key.trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - (base as any).privateKey = Buffer.from(cleanKey, 'utf8'); - - if (host.keyPassword) { - (base as any).passphrase = host.keyPassword; - } - - } catch (keyError) { - logger.error(`SSH key format error for host ${host.ip}: ${keyError.message}`); - if (host.password) { - (base as any).password = host.password; - } else { - throw new Error(`Invalid SSH key format for host ${host.ip}`); - } - } - } - } - return base; -} - -async function withSshConnection(host: HostRecord, fn: (client: Client) => Promise): Promise { - return new Promise((resolve, reject) => { - const client = new Client(); - let settled = false; - - const onError = (err: Error) => { - if (!settled) { - settled = true; - try { - client.end(); - } catch { - } - reject(err); - } - }; - - client.on('ready', async () => { - try { - const result = await fn(client); - if (!settled) { - settled = true; - try { - client.end(); - } catch { - } - resolve(result); - } - } catch (err: any) { - onError(err); - } - }); - - client.on('error', onError); - client.on('timeout', () => onError(new Error('SSH connection timeout'))); - try { - client.connect(buildSshConfig(host)); - } catch (err: any) { - onError(err); - } - }); -} - -function execCommand(client: Client, command: string): Promise<{ - stdout: string; - stderr: string; - code: number | null; +function execCommand( + client: Client, + command: string, +): Promise<{ + stdout: string; + stderr: string; + code: number | null; }> { - return new Promise((resolve, reject) => { - client.exec(command, {pty: false}, (err, stream) => { - if (err) return reject(err); - let stdout = ''; - let stderr = ''; - let exitCode: number | null = null; - stream.on('close', (code: number | undefined) => { - exitCode = typeof code === 'number' ? code : null; - resolve({stdout, stderr, code: exitCode}); - }).on('data', (data: Buffer) => { - stdout += data.toString('utf8'); - }).stderr.on('data', (data: Buffer) => { - stderr += data.toString('utf8'); - }); + return new Promise((resolve, reject) => { + client.exec(command, { pty: false }, (err, stream) => { + if (err) return reject(err); + let stdout = ""; + let stderr = ""; + let exitCode: number | null = null; + stream + .on("close", (code: number | undefined) => { + exitCode = typeof code === "number" ? code : null; + resolve({ stdout, stderr, code: exitCode }); + }) + .on("data", (data: Buffer) => { + stdout += data.toString("utf8"); + }) + .stderr.on("data", (data: Buffer) => { + stderr += data.toString("utf8"); }); }); + }); } -function parseCpuLine(cpuLine: string): { total: number; idle: number } | undefined { - const parts = cpuLine.trim().split(/\s+/); - if (parts[0] !== 'cpu') return undefined; - const nums = parts.slice(1).map(n => Number(n)).filter(n => Number.isFinite(n)); - if (nums.length < 4) return undefined; - const idle = (nums[3] ?? 0) + (nums[4] ?? 0); - const total = nums.reduce((a, b) => a + b, 0); - return {total, idle}; +function parseCpuLine( + cpuLine: string, +): { total: number; idle: number } | undefined { + const parts = cpuLine.trim().split(/\s+/); + if (parts[0] !== "cpu") return undefined; + const nums = parts + .slice(1) + .map((n) => Number(n)) + .filter((n) => Number.isFinite(n)); + if (nums.length < 4) return undefined; + const idle = (nums[3] ?? 0) + (nums[4] ?? 0); + const total = nums.reduce((a, b) => a + b, 0); + return { total, idle }; } function toFixedNum(n: number | null | undefined, digits = 2): number | null { - if (typeof n !== 'number' || !Number.isFinite(n)) return null; - return Number(n.toFixed(digits)); + if (typeof n !== "number" || !Number.isFinite(n)) return null; + return Number(n.toFixed(digits)); } function kibToGiB(kib: number): number { - return kib / (1024 * 1024); + return kib / (1024 * 1024); } -async function collectMetrics(host: HostRecord): Promise<{ - cpu: { percent: number | null; cores: number | null; load: [number, number, number] | null }; - memory: { percent: number | null; usedGiB: number | null; totalGiB: number | null }; - disk: { percent: number | null; usedHuman: string | null; totalHuman: string | null }; +async function collectMetrics(host: SSHHostWithCredentials): Promise<{ + cpu: { + percent: number | null; + cores: number | null; + load: [number, number, number] | null; + }; + memory: { + percent: number | null; + usedGiB: number | null; + totalGiB: number | null; + }; + disk: { + percent: number | null; + usedHuman: string | null; + totalHuman: string | null; + }; }> { + const cached = metricsCache.get(host.id); + if (cached) { + return cached; + } + + return requestQueue.queueRequest(host.id, async () => { return withSshConnection(host, async (client) => { - let cpuPercent: number | null = null; - let cores: number | null = null; - let loadTriplet: [number, number, number] | null = null; - try { - const stat1 = await execCommand(client, 'cat /proc/stat'); - await new Promise(r => setTimeout(r, 500)); - const stat2 = await execCommand(client, 'cat /proc/stat'); - const loadAvgOut = await execCommand(client, 'cat /proc/loadavg'); - const coresOut = await execCommand(client, 'nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo'); + let cpuPercent: number | null = null; + let cores: number | null = null; + let loadTriplet: [number, number, number] | null = null; - const cpuLine1 = (stat1.stdout.split('\n').find(l => l.startsWith('cpu ')) || '').trim(); - const cpuLine2 = (stat2.stdout.split('\n').find(l => l.startsWith('cpu ')) || '').trim(); - const a = parseCpuLine(cpuLine1); - const b = parseCpuLine(cpuLine2); - if (a && b) { - const totalDiff = b.total - a.total; - const idleDiff = b.idle - a.idle; - const used = totalDiff - idleDiff; - if (totalDiff > 0) cpuPercent = Math.max(0, Math.min(100, (used / totalDiff) * 100)); - } + try { + const [stat1, loadAvgOut, coresOut] = await Promise.all([ + execCommand(client, "cat /proc/stat"), + execCommand(client, "cat /proc/loadavg"), + execCommand( + client, + "nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo", + ), + ]); - const laParts = loadAvgOut.stdout.trim().split(/\s+/); - if (laParts.length >= 3) { - loadTriplet = [Number(laParts[0]), Number(laParts[1]), Number(laParts[2])].map(v => Number.isFinite(v) ? Number(v) : 0) as [number, number, number]; - } + await new Promise((r) => setTimeout(r, 500)); + const stat2 = await execCommand(client, "cat /proc/stat"); - const coresNum = Number((coresOut.stdout || '').trim()); - cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null; - } catch (e) { - cpuPercent = null; - cores = null; - loadTriplet = null; + const cpuLine1 = ( + stat1.stdout.split("\n").find((l) => l.startsWith("cpu ")) || "" + ).trim(); + const cpuLine2 = ( + stat2.stdout.split("\n").find((l) => l.startsWith("cpu ")) || "" + ).trim(); + const a = parseCpuLine(cpuLine1); + const b = parseCpuLine(cpuLine2); + if (a && b) { + const totalDiff = b.total - a.total; + const idleDiff = b.idle - a.idle; + const used = totalDiff - idleDiff; + if (totalDiff > 0) + cpuPercent = Math.max(0, Math.min(100, (used / totalDiff) * 100)); } - let memPercent: number | null = null; - let usedGiB: number | null = null; - let totalGiB: number | null = null; - try { - const memInfo = await execCommand(client, 'cat /proc/meminfo'); - const lines = memInfo.stdout.split('\n'); - const getVal = (key: string) => { - const line = lines.find(l => l.startsWith(key)); - if (!line) return null; - const m = line.match(/\d+/); - return m ? Number(m[0]) : null; - }; - const totalKb = getVal('MemTotal:'); - const availKb = getVal('MemAvailable:'); - if (totalKb && availKb && totalKb > 0) { - const usedKb = totalKb - availKb; - memPercent = Math.max(0, Math.min(100, (usedKb / totalKb) * 100)); - usedGiB = kibToGiB(usedKb); - totalGiB = kibToGiB(totalKb); - } - } catch (e) { - memPercent = null; - usedGiB = null; - totalGiB = null; + const laParts = loadAvgOut.stdout.trim().split(/\s+/); + if (laParts.length >= 3) { + loadTriplet = [ + Number(laParts[0]), + Number(laParts[1]), + Number(laParts[2]), + ].map((v) => (Number.isFinite(v) ? Number(v) : 0)) as [ + number, + number, + number, + ]; } - let diskPercent: number | null = null; - let usedHuman: string | null = null; - let totalHuman: string | null = null; - try { - // Get both human-readable and bytes format for accurate calculation - const diskOutHuman = await execCommand(client, 'df -h -P / | tail -n +2'); - const diskOutBytes = await execCommand(client, 'df -B1 -P / | tail -n +2'); - - const humanLine = diskOutHuman.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || ''; - const bytesLine = diskOutBytes.stdout.split('\n').map(l => l.trim()).filter(Boolean)[0] || ''; - - const humanParts = humanLine.split(/\s+/); - const bytesParts = bytesLine.split(/\s+/); - - if (humanParts.length >= 6 && bytesParts.length >= 6) { - totalHuman = humanParts[1] || null; - usedHuman = humanParts[2] || null; - - // Calculate our own percentage using bytes for accuracy - const totalBytes = Number(bytesParts[1]); - const usedBytes = Number(bytesParts[2]); - - if (Number.isFinite(totalBytes) && Number.isFinite(usedBytes) && totalBytes > 0) { - diskPercent = Math.max(0, Math.min(100, (usedBytes / totalBytes) * 100)); - } - } - } catch (e) { - diskPercent = null; - usedHuman = null; - totalHuman = null; - } + const coresNum = Number((coresOut.stdout || "").trim()); + cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null; + } catch (e) { + statsLogger.warn( + `Failed to collect CPU metrics for host ${host.id}`, + e, + ); + cpuPercent = null; + cores = null; + loadTriplet = null; + } - return { - cpu: {percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet}, - memory: { - percent: toFixedNum(memPercent, 0), - usedGiB: usedGiB ? toFixedNum(usedGiB, 2) : null, - totalGiB: totalGiB ? toFixedNum(totalGiB, 2) : null - }, - disk: {percent: toFixedNum(diskPercent, 0), usedHuman, totalHuman}, + let memPercent: number | null = null; + let usedGiB: number | null = null; + let totalGiB: number | null = null; + try { + const memInfo = await execCommand(client, "cat /proc/meminfo"); + const lines = memInfo.stdout.split("\n"); + const getVal = (key: string) => { + const line = lines.find((l) => l.startsWith(key)); + if (!line) return null; + const m = line.match(/\d+/); + return m ? Number(m[0]) : null; }; + const totalKb = getVal("MemTotal:"); + const availKb = getVal("MemAvailable:"); + if (totalKb && availKb && totalKb > 0) { + const usedKb = totalKb - availKb; + memPercent = Math.max(0, Math.min(100, (usedKb / totalKb) * 100)); + usedGiB = kibToGiB(usedKb); + totalGiB = kibToGiB(totalKb); + } + } catch (e) { + statsLogger.warn( + `Failed to collect memory metrics for host ${host.id}`, + e, + ); + memPercent = null; + usedGiB = null; + totalGiB = null; + } + + let diskPercent: number | null = null; + let usedHuman: string | null = null; + let totalHuman: string | null = null; + try { + const [diskOutHuman, diskOutBytes] = await Promise.all([ + execCommand(client, "df -h -P / | tail -n +2"), + execCommand(client, "df -B1 -P / | tail -n +2"), + ]); + + const humanLine = + diskOutHuman.stdout + .split("\n") + .map((l) => l.trim()) + .filter(Boolean)[0] || ""; + const bytesLine = + diskOutBytes.stdout + .split("\n") + .map((l) => l.trim()) + .filter(Boolean)[0] || ""; + + const humanParts = humanLine.split(/\s+/); + const bytesParts = bytesLine.split(/\s+/); + + if (humanParts.length >= 6 && bytesParts.length >= 6) { + totalHuman = humanParts[1] || null; + usedHuman = humanParts[2] || null; + + const totalBytes = Number(bytesParts[1]); + const usedBytes = Number(bytesParts[2]); + + if ( + Number.isFinite(totalBytes) && + Number.isFinite(usedBytes) && + totalBytes > 0 + ) { + diskPercent = Math.max( + 0, + Math.min(100, (usedBytes / totalBytes) * 100), + ); + } + } + } catch (e) { + statsLogger.warn( + `Failed to collect disk metrics for host ${host.id}`, + e, + ); + diskPercent = null; + usedHuman = null; + totalHuman = null; + } + + const result = { + cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet }, + memory: { + percent: toFixedNum(memPercent, 0), + usedGiB: usedGiB ? toFixedNum(usedGiB, 2) : null, + totalGiB: totalGiB ? toFixedNum(totalGiB, 2) : null, + }, + disk: { percent: toFixedNum(diskPercent, 0), usedHuman, totalHuman }, + }; + + metricsCache.set(host.id, result); + return result; }); + }); } -function tcpPing(host: string, port: number, timeoutMs = 5000): Promise { - return new Promise((resolve) => { - const socket = new net.Socket(); - let settled = false; +function tcpPing( + host: string, + port: number, + timeoutMs = 5000, +): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + let settled = false; - const onDone = (result: boolean) => { - if (settled) return; - settled = true; - try { - socket.destroy(); - } catch { - } - resolve(result); - }; + const onDone = (result: boolean) => { + if (settled) return; + settled = true; + try { + socket.destroy(); + } catch {} + resolve(result); + }; - socket.setTimeout(timeoutMs); + socket.setTimeout(timeoutMs); - socket.once('connect', () => onDone(true)); - socket.once('timeout', () => onDone(false)); - socket.once('error', () => onDone(false)); - socket.connect(port, host); - }); + socket.once("connect", () => onDone(true)); + socket.once("timeout", () => onDone(false)); + socket.once("error", () => onDone(false)); + socket.connect(port, host); + }); } async function pollStatusesOnce(): Promise { - const hosts = await fetchAllHosts(); - if (hosts.length === 0) { - logger.warn('No hosts retrieved for status polling'); - return; - } - - const now = new Date().toISOString(); - - const checks = hosts.map(async (h) => { - const isOnline = await tcpPing(h.ip, h.port, 5000); - const now = new Date().toISOString(); - const statusEntry: StatusEntry = {status: isOnline ? 'online' : 'offline', lastChecked: now}; - hostStatuses.set(h.id, statusEntry); - return isOnline; + const hosts = await fetchAllHosts(); + if (hosts.length === 0) { + statsLogger.warn("No hosts retrieved for status polling", { + operation: "status_poll", }); + return; + } - const results = await Promise.allSettled(checks); - const onlineCount = results.filter(r => r.status === 'fulfilled' && r.value === true).length; - const offlineCount = hosts.length - onlineCount; + const now = new Date().toISOString(); + + const checks = hosts.map(async (h) => { + const isOnline = await tcpPing(h.ip, h.port, 5000); + const now = new Date().toISOString(); + const statusEntry: StatusEntry = { + status: isOnline ? "online" : "offline", + lastChecked: now, + }; + hostStatuses.set(h.id, statusEntry); + return isOnline; + }); + + const results = await Promise.allSettled(checks); + const onlineCount = results.filter( + (r) => r.status === "fulfilled" && r.value === true, + ).length; + const offlineCount = hosts.length - onlineCount; + statsLogger.success("Status polling completed", { + operation: "status_poll", + totalHosts: hosts.length, + onlineCount, + offlineCount, + }); } -app.get('/status', async (req, res) => { - if (hostStatuses.size === 0) { - await pollStatusesOnce(); - } - const result: Record = {}; - for (const [id, entry] of hostStatuses.entries()) { - result[id] = entry; - } - res.json(result); -}); - -app.get('/status/:id', async (req, res) => { - const id = Number(req.params.id); - if (!id) { - return res.status(400).json({error: 'Invalid id'}); - } - - try { - const host = await fetchHostById(id); - if (!host) { - return res.status(404).json({error: 'Host not found'}); - } - - const isOnline = await tcpPing(host.ip, host.port, 5000); - const now = new Date().toISOString(); - const statusEntry: StatusEntry = {status: isOnline ? 'online' : 'offline', lastChecked: now}; - - hostStatuses.set(id, statusEntry); - res.json(statusEntry); - } catch (err) { - logger.error('Failed to check host status', err); - res.status(500).json({error: 'Failed to check host status'}); - } -}); - -app.post('/refresh', async (req, res) => { +app.get("/status", async (req, res) => { + if (hostStatuses.size === 0) { await pollStatusesOnce(); - res.json({message: 'Refreshed'}); + } + const result: Record = {}; + for (const [id, entry] of hostStatuses.entries()) { + result[id] = entry; + } + res.json(result); }); -app.get('/metrics/:id', async (req, res) => { - const id = Number(req.params.id); - if (!id) { - return res.status(400).json({error: 'Invalid id'}); +app.get("/status/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + + try { + const host = await fetchHostById(id); + if (!host) { + return res.status(404).json({ error: "Host not found" }); } - try { - const host = await fetchHostById(id); - if (!host) { - return res.status(404).json({error: 'Host not found'}); - } - const metrics = await collectMetrics(host); - res.json({...metrics, lastChecked: new Date().toISOString()}); - } catch (err) { - logger.error('Failed to collect metrics', err); - return res.json({ - cpu: {percent: null, cores: null, load: null}, - memory: {percent: null, usedGiB: null, totalGiB: null}, - disk: {percent: null, usedHuman: null, totalHuman: null}, - lastChecked: new Date().toISOString() - }); + + const isOnline = await tcpPing(host.ip, host.port, 5000); + const now = new Date().toISOString(); + const statusEntry: StatusEntry = { + status: isOnline ? "online" : "offline", + lastChecked: now, + }; + + hostStatuses.set(id, statusEntry); + res.json(statusEntry); + } catch (err) { + statsLogger.error("Failed to check host status", err); + res.status(500).json({ error: "Failed to check host status" }); + } +}); + +app.post("/refresh", async (req, res) => { + await pollStatusesOnce(); + res.json({ message: "Refreshed" }); +}); + +app.get("/metrics/:id", validateHostId, async (req, res) => { + const id = Number(req.params.id); + + try { + const host = await fetchHostById(id); + if (!host) { + return res.status(404).json({ error: "Host not found" }); } + + const isOnline = await tcpPing(host.ip, host.port, 5000); + if (!isOnline) { + return res.status(503).json({ + error: "Host is offline", + cpu: { percent: null, cores: null, load: null }, + memory: { percent: null, usedGiB: null, totalGiB: null }, + disk: { percent: null, usedHuman: null, totalHuman: null }, + lastChecked: new Date().toISOString(), + }); + } + + const metrics = await collectMetrics(host); + res.json({ ...metrics, lastChecked: new Date().toISOString() }); + } catch (err) { + statsLogger.error("Failed to collect metrics", err); + + if (err instanceof Error && err.message.includes("timeout")) { + return res.status(504).json({ + error: "Metrics collection timeout", + cpu: { percent: null, cores: null, load: null }, + memory: { percent: null, usedGiB: null, totalGiB: null }, + disk: { percent: null, usedHuman: null, totalHuman: null }, + lastChecked: new Date().toISOString(), + }); + } + + return res.status(500).json({ + error: "Failed to collect metrics", + cpu: { percent: null, cores: null, load: null }, + memory: { percent: null, usedGiB: null, totalGiB: null }, + disk: { percent: null, usedHuman: null, totalHuman: null }, + lastChecked: new Date().toISOString(), + }); + } +}); + +process.on("SIGINT", () => { + statsLogger.info("Received SIGINT, shutting down gracefully"); + connectionPool.destroy(); + process.exit(0); +}); + +process.on("SIGTERM", () => { + statsLogger.info("Received SIGTERM, shutting down gracefully"); + connectionPool.destroy(); + process.exit(0); }); const PORT = 8085; app.listen(PORT, async () => { - try { - await pollStatusesOnce(); - } catch (err) { - logger.error('Initial poll failed', err); - } -}); \ No newline at end of file + statsLogger.success("Server Stats API server started", { + operation: "server_start", + port: PORT, + }); + try { + await pollStatusesOnce(); + } catch (err) { + statsLogger.error("Initial poll failed", err, { + operation: "initial_poll", + }); + } +}); diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index a77a7e90..cb1ec180 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -1,355 +1,498 @@ -import {WebSocketServer, WebSocket, type RawData} from 'ws'; -import {Client, type ClientChannel, type PseudoTtyOptions} from 'ssh2'; -import chalk from 'chalk'; - -const wss = new WebSocketServer({port: 8082}); - - - - -const sshIconSymbol = '🖥️'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${sshIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; - - - -wss.on('connection', (ws: WebSocket) => { - let sshConn: Client | null = null; - let sshStream: ClientChannel | null = null; - let pingInterval: NodeJS.Timeout | null = null; - - - - ws.on('close', () => { - cleanupSSH(); - }); - - ws.on('message', (msg: RawData) => { - - - let parsed: any; - try { - parsed = JSON.parse(msg.toString()); - } catch (e) { - logger.error('Invalid JSON received: ' + msg.toString()); - ws.send(JSON.stringify({type: 'error', message: 'Invalid JSON'})); - return; - } - - const {type, data} = parsed; - - switch (type) { - case 'connectToHost': - handleConnectToHost(data); - break; - - case 'resize': - handleResize(data); - break; - - case 'disconnect': - cleanupSSH(); - break; - - case 'input': - if (sshStream) { - if (data === '\t') { - sshStream.write(data); - } else if (data.startsWith('\x1b')) { - sshStream.write(data); - } else { - sshStream.write(Buffer.from(data, 'utf8')); - } - } - break; - - case 'ping': - ws.send(JSON.stringify({type: 'pong'})); - break; - - default: - logger.warn('Unknown message type: ' + type); - } - }); - - function handleConnectToHost(data: { - cols: number; - rows: number; - hostConfig: { - ip: string; - port: number; - username: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - authType?: string; - }; - }) { - const {cols, rows, hostConfig} = data; - const {ip, port, username, password, key, keyPassword, keyType, authType} = hostConfig; - - if (!username || typeof username !== 'string' || username.trim() === '') { - logger.error('Invalid username provided'); - ws.send(JSON.stringify({type: 'error', message: 'Invalid username provided'})); - return; - } - - if (!ip || typeof ip !== 'string' || ip.trim() === '') { - logger.error('Invalid IP provided'); - ws.send(JSON.stringify({type: 'error', message: 'Invalid IP provided'})); - return; - } - - if (!port || typeof port !== 'number' || port <= 0) { - logger.error('Invalid port provided'); - ws.send(JSON.stringify({type: 'error', message: 'Invalid port provided'})); - return; - } - - sshConn = new Client(); - - const connectionTimeout = setTimeout(() => { - if (sshConn) { - logger.error('SSH connection timeout'); - ws.send(JSON.stringify({type: 'error', message: 'SSH connection timeout'})); - cleanupSSH(connectionTimeout); - } - }, 60000); - - sshConn.on('ready', () => { - clearTimeout(connectionTimeout); - - - sshConn!.shell({ - rows: data.rows, - cols: data.cols, - term: 'xterm-256color' - } as PseudoTtyOptions, (err, stream) => { - if (err) { - logger.error('Shell error: ' + err.message); - ws.send(JSON.stringify({type: 'error', message: 'Shell error: ' + err.message})); - return; - } - - sshStream = stream; - - stream.on('data', (data: Buffer) => { - ws.send(JSON.stringify({type: 'data', data: data.toString()})); - }); - - stream.on('close', () => { - - ws.send(JSON.stringify({type: 'disconnected', message: 'Connection lost'})); - }); - - stream.on('error', (err: Error) => { - logger.error('SSH stream error: ' + err.message); - ws.send(JSON.stringify({type: 'error', message: 'SSH stream error: ' + err.message})); - }); - - setupPingInterval(); - - ws.send(JSON.stringify({type: 'connected', message: 'SSH connected'})); - }); - }); - - sshConn.on('error', (err: Error) => { - clearTimeout(connectionTimeout); - logger.error('SSH connection error: ' + err.message); - - let errorMessage = 'SSH error: ' + err.message; - if (err.message.includes('No matching key exchange algorithm')) { - errorMessage = 'SSH error: No compatible key exchange algorithm found. This may be due to an older SSH server or network device.'; - } else if (err.message.includes('No matching cipher')) { - errorMessage = 'SSH error: No compatible cipher found. This may be due to an older SSH server or network device.'; - } else if (err.message.includes('No matching MAC')) { - errorMessage = 'SSH error: No compatible MAC algorithm found. This may be due to an older SSH server or network device.'; - } else if (err.message.includes('ENOTFOUND') || err.message.includes('ENOENT')) { - errorMessage = 'SSH error: Could not resolve hostname or connect to server.'; - } else if (err.message.includes('ECONNREFUSED')) { - errorMessage = 'SSH error: Connection refused. The server may not be running or the port may be incorrect.'; - } else if (err.message.includes('ETIMEDOUT')) { - errorMessage = 'SSH error: Connection timed out. Check your network connection and server availability.'; - } else if (err.message.includes('ECONNRESET') || err.message.includes('EPIPE')) { - errorMessage = 'SSH error: Connection was reset. This may be due to network issues or server timeout.'; - } else if (err.message.includes('authentication failed') || err.message.includes('Permission denied')) { - errorMessage = 'SSH error: Authentication failed. Please check your username and password/key.'; - } - - ws.send(JSON.stringify({type: 'error', message: errorMessage})); - cleanupSSH(connectionTimeout); - }); - - sshConn.on('close', () => { - clearTimeout(connectionTimeout); - - cleanupSSH(connectionTimeout); - }); - - - - const connectConfig: any = { - host: ip, - port, - username, - keepaliveInterval: 30000, - keepaliveCountMax: 3, - readyTimeout: 60000, - tcpKeepAlive: true, - tcpKeepAliveInitialDelay: 30000, - - env: { - TERM: 'xterm-256color', - LANG: 'en_US.UTF-8', - LC_ALL: 'en_US.UTF-8', - LC_CTYPE: 'en_US.UTF-8', - LC_MESSAGES: 'en_US.UTF-8', - LC_MONETARY: 'en_US.UTF-8', - LC_NUMERIC: 'en_US.UTF-8', - LC_TIME: 'en_US.UTF-8', - LC_COLLATE: 'en_US.UTF-8', - COLORTERM: 'truecolor', - }, - - algorithms: { - kex: [ - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group1-sha1', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group-exchange-sha1', - 'ecdh-sha2-nistp256', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp521' - ], - cipher: [ - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - 'aes128-gcm@openssh.com', - 'aes256-gcm@openssh.com', - 'aes128-cbc', - 'aes192-cbc', - 'aes256-cbc', - '3des-cbc' - ], - hmac: [ - 'hmac-sha2-256', - 'hmac-sha2-512', - 'hmac-sha1', - 'hmac-md5' - ], - compress: [ - 'none', - 'zlib@openssh.com', - 'zlib' - ] - } - }; - if (authType === 'key' && key) { - try { - if (!key.includes('-----BEGIN') || !key.includes('-----END')) { - throw new Error('Invalid private key format'); - } - - const cleanKey = key.trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - connectConfig.privateKey = Buffer.from(cleanKey, 'utf8'); - - if (keyPassword) { - connectConfig.passphrase = keyPassword; - } - - if (keyType && keyType !== 'auto') { - connectConfig.privateKeyType = keyType; - } - } catch (keyError) { - logger.error('SSH key format error: ' + keyError.message); - ws.send(JSON.stringify({type: 'error', message: 'SSH key format error: Invalid private key format'})); - return; - } - } else if (authType === 'key') { - logger.error('SSH key authentication requested but no key provided'); - ws.send(JSON.stringify({type: 'error', message: 'SSH key authentication requested but no key provided'})); - return; - } else { - connectConfig.password = password; - } - - sshConn.connect(connectConfig); - } - - function handleResize(data: { cols: number; rows: number }) { - if (sshStream && sshStream.setWindow) { - sshStream.setWindow(data.rows, data.cols, data.rows, data.cols); - ws.send(JSON.stringify({type: 'resized', cols: data.cols, rows: data.rows})); - } - } - - function cleanupSSH(timeoutId?: NodeJS.Timeout) { - if (timeoutId) { - clearTimeout(timeoutId); - } - - if (pingInterval) { - clearInterval(pingInterval); - pingInterval = null; - } - - if (sshStream) { - try { - sshStream.end(); - } catch (e: any) { - logger.error('Error closing stream: ' + e.message); - } - sshStream = null; - } - - if (sshConn) { - try { - sshConn.end(); - } catch (e: any) { - logger.error('Error closing connection: ' + e.message); - } - sshConn = null; - } - } - - function setupPingInterval() { - pingInterval = setInterval(() => { - if (sshConn && sshStream) { - try { - sshStream.write('\x00'); - } catch (e: any) { - logger.error('SSH keepalive failed: ' + e.message); - cleanupSSH(); - } - } - }, 60000); - } +import { WebSocketServer, WebSocket, type RawData } from "ws"; +import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2"; +import { db } from "../database/db/index.js"; +import { sshCredentials } from "../database/db/schema.js"; +import { eq, and } from "drizzle-orm"; +import { sshLogger } from "../utils/logger.js"; +const wss = new WebSocketServer({ port: 8082 }); +sshLogger.success("SSH Terminal WebSocket server started", { + operation: "server_start", + port: 8082, +}); + +wss.on("connection", (ws: WebSocket) => { + let sshConn: Client | null = null; + let sshStream: ClientChannel | null = null; + let pingInterval: NodeJS.Timeout | null = null; + + ws.on("close", () => { + cleanupSSH(); + }); + + ws.on("message", (msg: RawData) => { + let parsed: any; + try { + parsed = JSON.parse(msg.toString()); + } catch (e) { + sshLogger.error("Invalid JSON received", e, { + operation: "websocket_message", + messageLength: msg.toString().length, + }); + ws.send(JSON.stringify({ type: "error", message: "Invalid JSON" })); + return; + } + + const { type, data } = parsed; + + switch (type) { + case "connectToHost": + handleConnectToHost(data).catch((error) => { + sshLogger.error("Failed to connect to host", error, { + operation: "ssh_connect", + hostId: data.hostConfig?.id, + ip: data.hostConfig?.ip, + }); + ws.send( + JSON.stringify({ + type: "error", + message: + "Failed to connect to host: " + + (error instanceof Error ? error.message : "Unknown error"), + }), + ); + }); + break; + + case "resize": + handleResize(data); + break; + + case "disconnect": + cleanupSSH(); + break; + + case "input": + if (sshStream) { + if (data === "\t") { + sshStream.write(data); + } else if (data.startsWith("\x1b")) { + sshStream.write(data); + } else { + sshStream.write(Buffer.from(data, "utf8")); + } + } + break; + + case "ping": + ws.send(JSON.stringify({ type: "pong" })); + break; + + default: + sshLogger.warn("Unknown message type received", { + operation: "websocket_message", + messageType: type, + }); + } + }); + + async function handleConnectToHost(data: { + cols: number; + rows: number; + hostConfig: { + id: number; + ip: string; + port: number; + username: string; + password?: string; + key?: string; + keyPassword?: string; + keyType?: string; + authType?: string; + credentialId?: number; + userId?: string; + }; + }) { + const { cols, rows, hostConfig } = data; + const { + id, + ip, + port, + username, + password, + key, + keyPassword, + keyType, + authType, + credentialId, + } = hostConfig; + + if (!username || typeof username !== "string" || username.trim() === "") { + sshLogger.error("Invalid username provided", undefined, { + operation: "ssh_connect", + hostId: id, + ip, + }); + ws.send( + JSON.stringify({ type: "error", message: "Invalid username provided" }), + ); + return; + } + + if (!ip || typeof ip !== "string" || ip.trim() === "") { + sshLogger.error("Invalid IP provided", undefined, { + operation: "ssh_connect", + hostId: id, + username, + }); + ws.send( + JSON.stringify({ type: "error", message: "Invalid IP provided" }), + ); + return; + } + + if (!port || typeof port !== "number" || port <= 0) { + sshLogger.error("Invalid port provided", undefined, { + operation: "ssh_connect", + hostId: id, + ip, + username, + port, + }); + ws.send( + JSON.stringify({ type: "error", message: "Invalid port provided" }), + ); + return; + } + + sshConn = new Client(); + + const connectionTimeout = setTimeout(() => { + if (sshConn) { + sshLogger.error("SSH connection timeout", undefined, { + operation: "ssh_connect", + hostId: id, + ip, + port, + username, + }); + ws.send( + JSON.stringify({ type: "error", message: "SSH connection timeout" }), + ); + cleanupSSH(connectionTimeout); + } + }, 60000); + + let resolvedCredentials = { password, key, keyPassword, keyType, authType }; + if (credentialId && id && hostConfig.userId) { + try { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, credentialId), + eq(sshCredentials.userId, hostConfig.userId), + ), + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + resolvedCredentials = { + password: credential.password, + key: credential.key, + keyPassword: credential.keyPassword, + keyType: credential.keyType, + authType: credential.authType, + }; + } else { + sshLogger.warn(`No credentials found for host ${id}`, { + operation: "ssh_credentials", + hostId: id, + credentialId, + userId: hostConfig.userId, + }); + } + } 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", + }); + } + } else if (credentialId && id) { + sshLogger.warn("Missing userId for credential resolution in terminal", { + operation: "ssh_credentials", + hostId: id, + credentialId, + hasUserId: !!hostConfig.userId, + }); + } + + sshConn.on("ready", () => { + clearTimeout(connectionTimeout); + + sshConn!.shell( + { + rows: data.rows, + cols: data.cols, + term: "xterm-256color", + } as PseudoTtyOptions, + (err, stream) => { + if (err) { + sshLogger.error("Shell error", err, { + operation: "ssh_shell", + hostId: id, + ip, + port, + username, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "Shell error: " + err.message, + }), + ); + return; + } + + sshStream = stream; + + stream.on("data", (data: Buffer) => { + ws.send(JSON.stringify({ type: "data", data: data.toString() })); + }); + + stream.on("close", () => { + ws.send( + JSON.stringify({ + type: "disconnected", + message: "Connection lost", + }), + ); + }); + + stream.on("error", (err: Error) => { + sshLogger.error("SSH stream error", err, { + operation: "ssh_stream", + hostId: id, + ip, + port, + username, + }); + ws.send( + JSON.stringify({ + type: "error", + message: "SSH stream error: " + err.message, + }), + ); + }); + + setupPingInterval(); + + ws.send( + JSON.stringify({ type: "connected", message: "SSH connected" }), + ); + }, + ); + }); + + sshConn.on("error", (err: Error) => { + clearTimeout(connectionTimeout); + sshLogger.error("SSH connection error", err, { + operation: "ssh_connect", + hostId: id, + ip, + port, + username, + authType: resolvedCredentials.authType, + }); + + let errorMessage = "SSH error: " + err.message; + if (err.message.includes("No matching key exchange algorithm")) { + errorMessage = + "SSH error: No compatible key exchange algorithm found. This may be due to an older SSH server or network device."; + } else if (err.message.includes("No matching cipher")) { + errorMessage = + "SSH error: No compatible cipher found. This may be due to an older SSH server or network device."; + } else if (err.message.includes("No matching MAC")) { + errorMessage = + "SSH error: No compatible MAC algorithm found. This may be due to an older SSH server or network device."; + } else if ( + err.message.includes("ENOTFOUND") || + err.message.includes("ENOENT") + ) { + errorMessage = + "SSH error: Could not resolve hostname or connect to server."; + } else if (err.message.includes("ECONNREFUSED")) { + errorMessage = + "SSH error: Connection refused. The server may not be running or the port may be incorrect."; + } else if (err.message.includes("ETIMEDOUT")) { + errorMessage = + "SSH error: Connection timed out. Check your network connection and server availability."; + } else if ( + err.message.includes("ECONNRESET") || + err.message.includes("EPIPE") + ) { + errorMessage = + "SSH error: Connection was reset. This may be due to network issues or server timeout."; + } else if ( + err.message.includes("authentication failed") || + err.message.includes("Permission denied") + ) { + errorMessage = + "SSH error: Authentication failed. Please check your username and password/key."; + } + + ws.send(JSON.stringify({ type: "error", message: errorMessage })); + cleanupSSH(connectionTimeout); + }); + + sshConn.on("close", () => { + clearTimeout(connectionTimeout); + cleanupSSH(connectionTimeout); + }); + + const connectConfig: any = { + host: ip, + port, + username, + keepaliveInterval: 30000, + keepaliveCountMax: 3, + readyTimeout: 60000, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 30000, + + env: { + TERM: "xterm-256color", + LANG: "en_US.UTF-8", + LC_ALL: "en_US.UTF-8", + LC_CTYPE: "en_US.UTF-8", + LC_MESSAGES: "en_US.UTF-8", + LC_MONETARY: "en_US.UTF-8", + LC_NUMERIC: "en_US.UTF-8", + LC_TIME: "en_US.UTF-8", + LC_COLLATE: "en_US.UTF-8", + COLORTERM: "truecolor", + }, + + algorithms: { + kex: [ + "diffie-hellman-group14-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group1-sha1", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group-exchange-sha1", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + ], + cipher: [ + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-gcm@openssh.com", + "aes256-gcm@openssh.com", + "aes128-cbc", + "aes192-cbc", + "aes256-cbc", + "3des-cbc", + ], + hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-md5"], + compress: ["none", "zlib@openssh.com", "zlib"], + }, + }; + if (resolvedCredentials.authType === "key" && resolvedCredentials.key) { + try { + if ( + !resolvedCredentials.key.includes("-----BEGIN") || + !resolvedCredentials.key.includes("-----END") + ) { + throw new Error("Invalid private key format"); + } + + const cleanKey = resolvedCredentials.key + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + + connectConfig.privateKey = Buffer.from(cleanKey, "utf8"); + + if (resolvedCredentials.keyPassword) { + connectConfig.passphrase = resolvedCredentials.keyPassword; + } + + if ( + resolvedCredentials.keyType && + resolvedCredentials.keyType !== "auto" + ) { + connectConfig.privateKeyType = resolvedCredentials.keyType; + } + } catch (keyError) { + sshLogger.error("SSH key format error: " + keyError.message); + ws.send( + JSON.stringify({ + type: "error", + message: "SSH key format error: Invalid private key format", + }), + ); + return; + } + } else if (resolvedCredentials.authType === "key") { + sshLogger.error("SSH key authentication requested but no key provided"); + ws.send( + JSON.stringify({ + type: "error", + message: "SSH key authentication requested but no key provided", + }), + ); + return; + } else { + connectConfig.password = resolvedCredentials.password; + } + + sshConn.connect(connectConfig); + } + + function handleResize(data: { cols: number; rows: number }) { + if (sshStream && sshStream.setWindow) { + sshStream.setWindow(data.rows, data.cols, data.rows, data.cols); + ws.send( + JSON.stringify({ type: "resized", cols: data.cols, rows: data.rows }), + ); + } + } + + function cleanupSSH(timeoutId?: NodeJS.Timeout) { + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } + + if (sshStream) { + try { + sshStream.end(); + } catch (e: any) { + sshLogger.error("Error closing stream: " + e.message); + } + sshStream = null; + } + + if (sshConn) { + try { + sshConn.end(); + } catch (e: any) { + sshLogger.error("Error closing connection: " + e.message); + } + sshConn = null; + } + } + + function setupPingInterval() { + pingInterval = setInterval(() => { + if (sshConn && sshStream) { + try { + sshStream.write("\x00"); + } catch (e: any) { + sshLogger.error("SSH keepalive failed: " + e.message); + cleanupSSH(); + } + } + }, 60000); + } }); diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 274af443..5d37c753 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -1,45 +1,39 @@ -import express from 'express'; -import cors from 'cors'; -import {Client} from 'ssh2'; -import {ChildProcess} from 'child_process'; -import chalk from 'chalk'; -import axios from 'axios'; -import * as net from 'net'; +import express from "express"; +import cors from "cors"; +import { Client } from "ssh2"; +import { ChildProcess } from "child_process"; +import axios from "axios"; +import { db } from "../database/db/index.js"; +import { sshCredentials } from "../database/db/schema.js"; +import { eq, and } from "drizzle-orm"; +import type { + SSHHost, + TunnelConfig, + TunnelStatus, + VerificationData, + ErrorType, +} from "../../types/index.js"; +import { CONNECTION_STATES } from "../../types/index.js"; +import { tunnelLogger } from "../utils/logger.js"; const app = express(); -app.use(cors({ - origin: '*', - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: 'Origin,X-Requested-With,Content-Type,Accept,Authorization', -})); +app.use( + cors({ + origin: "*", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: [ + "Origin", + "X-Requested-With", + "Content-Type", + "Accept", + "Authorization", + "User-Agent", + "X-Electron-App", + ], + }), +); app.use(express.json()); -const tunnelIconSymbol = '📡'; -const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${tunnelIconSymbol}]`)} ${message}`; -}; -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; - const activeTunnels = new Map(); const retryCounters = new Map(); const connectionStatus = new Map(); @@ -53,997 +47,1068 @@ const retryExhaustedTunnels = new Set(); const tunnelConfigs = new Map(); const activeTunnelProcesses = new Map(); -interface TunnelConnection { - sourcePort: number; - endpointPort: number; - endpointHost: string; - maxRetries: number; - retryInterval: number; - autoStart: boolean; -} - -interface SSHHost { - id: number; - name: string; - ip: string; - port: number; - username: string; - folder: string; - tags: string[]; - pin: boolean; - authType: string; - password?: string; - key?: string; - keyPassword?: string; - keyType?: string; - enableTerminal: boolean; - enableTunnel: boolean; - enableFileManager: boolean; - defaultPath: string; - tunnelConnections: TunnelConnection[]; - createdAt: string; - updatedAt: string; -} - -interface TunnelConfig { - name: string; - hostName: string; - sourceIP: string; - sourceSSHPort: number; - sourceUsername: string; - sourcePassword?: string; - sourceAuthMethod: string; - sourceSSHKey?: string; - sourceKeyPassword?: string; - sourceKeyType?: string; - endpointIP: string; - endpointSSHPort: number; - endpointUsername: string; - endpointPassword?: string; - endpointAuthMethod: string; - endpointSSHKey?: string; - endpointKeyPassword?: string; - endpointKeyType?: string; - sourcePort: number; - endpointPort: number; - maxRetries: number; - retryInterval: number; - autoStart: boolean; - isPinned: boolean; -} - -interface HostConfig { - host: SSHHost; - tunnels: TunnelConfig[]; -} - -interface TunnelStatus { - connected: boolean; - status: ConnectionState; - retryCount?: number; - maxRetries?: number; - nextRetryIn?: number; - reason?: string; - errorType?: ErrorType; - manualDisconnect?: boolean; - retryExhausted?: boolean; -} - -interface VerificationData { - conn: Client; - timeout: NodeJS.Timeout; -} - -const CONNECTION_STATES = { - DISCONNECTED: "disconnected", - CONNECTING: "connecting", - CONNECTED: "connected", - VERIFYING: "verifying", - FAILED: "failed", - UNSTABLE: "unstable", - RETRYING: "retrying", - WAITING: "waiting" -} as const; - -const ERROR_TYPES = { - AUTH: "authentication", - NETWORK: "network", - PORT: "port_conflict", - PERMISSION: "permission", - TIMEOUT: "timeout", - UNKNOWN: "unknown" -} as const; - -type ConnectionState = typeof CONNECTION_STATES[keyof typeof CONNECTION_STATES]; -type ErrorType = typeof ERROR_TYPES[keyof typeof ERROR_TYPES]; - function broadcastTunnelStatus(tunnelName: string, status: TunnelStatus): void { - if (status.status === CONNECTION_STATES.CONNECTED && activeRetryTimers.has(tunnelName)) { - return; - } + if ( + status.status === CONNECTION_STATES.CONNECTED && + activeRetryTimers.has(tunnelName) + ) { + return; + } - if (retryExhaustedTunnels.has(tunnelName) && status.status === CONNECTION_STATES.FAILED) { - status.reason = "Max retries exhausted"; - } + if ( + retryExhaustedTunnels.has(tunnelName) && + status.status === CONNECTION_STATES.FAILED + ) { + status.reason = "Max retries exhausted"; + } - connectionStatus.set(tunnelName, status); + connectionStatus.set(tunnelName, status); } function getAllTunnelStatus(): Record { - const tunnelStatus: Record = {}; - connectionStatus.forEach((status, key) => { - tunnelStatus[key] = status; - }); - return tunnelStatus; + const tunnelStatus: Record = {}; + connectionStatus.forEach((status, key) => { + tunnelStatus[key] = status; + }); + return tunnelStatus; } function classifyError(errorMessage: string): ErrorType { - if (!errorMessage) return ERROR_TYPES.UNKNOWN; + if (!errorMessage) return "UNKNOWN"; - const message = errorMessage.toLowerCase(); + const message = errorMessage.toLowerCase(); - if (message.includes("closed by remote host") || - message.includes("connection reset by peer") || - message.includes("connection refused") || - message.includes("broken pipe")) { - return ERROR_TYPES.NETWORK; - } + if ( + message.includes("closed by remote host") || + message.includes("connection reset by peer") || + message.includes("connection refused") || + message.includes("broken pipe") + ) { + return "NETWORK_ERROR"; + } - if (message.includes("authentication failed") || - message.includes("permission denied") || - message.includes("incorrect password")) { - return ERROR_TYPES.AUTH; - } + if ( + message.includes("authentication failed") || + message.includes("permission denied") || + message.includes("incorrect password") + ) { + return "AUTHENTICATION_FAILED"; + } - if (message.includes("connect etimedout") || - message.includes("timeout") || - message.includes("timed out") || - message.includes("keepalive timeout")) { - return ERROR_TYPES.TIMEOUT; - } + if ( + message.includes("connect etimedout") || + message.includes("timeout") || + message.includes("timed out") || + message.includes("keepalive timeout") + ) { + return "TIMEOUT"; + } - if (message.includes("bind: address already in use") || - message.includes("failed for listen port") || - message.includes("port forwarding failed")) { - return ERROR_TYPES.PORT; - } + if ( + message.includes("bind: address already in use") || + message.includes("failed for listen port") || + message.includes("port forwarding failed") + ) { + return "CONNECTION_FAILED"; + } - if (message.includes("permission") || - message.includes("access denied")) { - return ERROR_TYPES.PERMISSION; - } + if (message.includes("permission") || message.includes("access denied")) { + return "CONNECTION_FAILED"; + } - return ERROR_TYPES.UNKNOWN; + return "UNKNOWN"; } function getTunnelMarker(tunnelName: string) { - return `TUNNEL_MARKER_${tunnelName.replace(/[^a-zA-Z0-9]/g, '_')}`; + return `TUNNEL_MARKER_${tunnelName.replace(/[^a-zA-Z0-9]/g, "_")}`; } function cleanupTunnelResources(tunnelName: string): void { - const tunnelConfig = tunnelConfigs.get(tunnelName); - if (tunnelConfig) { - killRemoteTunnelByMarker(tunnelConfig, tunnelName, (err) => { - if (err) { - logger.error(`Failed to kill remote tunnel for '${tunnelName}': ${err.message}`); - } - }); - } - - if (activeTunnelProcesses.has(tunnelName)) { - try { - const proc = activeTunnelProcesses.get(tunnelName); - if (proc) { - proc.kill('SIGTERM'); - } - } catch (e) { - logger.error(`Error while killing local ssh process for tunnel '${tunnelName}'`, e); - } - activeTunnelProcesses.delete(tunnelName); - } - - if (activeTunnels.has(tunnelName)) { - try { - const conn = activeTunnels.get(tunnelName); - if (conn) { - conn.end(); - } - } catch (e) { - logger.error(`Error while closing SSH2 Client for tunnel '${tunnelName}'`, e); - } - activeTunnels.delete(tunnelName); - } - - if (tunnelVerifications.has(tunnelName)) { - const verification = tunnelVerifications.get(tunnelName); - if (verification?.timeout) clearTimeout(verification.timeout); - try { - verification?.conn.end(); - } catch (e) { - } - tunnelVerifications.delete(tunnelName); - } - - const timerKeys = [ - tunnelName, - `${tunnelName}_confirm`, - `${tunnelName}_retry`, - `${tunnelName}_verify_retry`, - `${tunnelName}_ping` - ]; - - timerKeys.forEach(key => { - if (verificationTimers.has(key)) { - clearTimeout(verificationTimers.get(key)!); - verificationTimers.delete(key); - } + const tunnelConfig = tunnelConfigs.get(tunnelName); + if (tunnelConfig) { + killRemoteTunnelByMarker(tunnelConfig, tunnelName, (err) => { + if (err) { + tunnelLogger.error( + `Failed to kill remote tunnel for '${tunnelName}': ${err.message}`, + ); + } }); + } - if (activeRetryTimers.has(tunnelName)) { - clearTimeout(activeRetryTimers.get(tunnelName)!); - activeRetryTimers.delete(tunnelName); + if (activeTunnelProcesses.has(tunnelName)) { + try { + const proc = activeTunnelProcesses.get(tunnelName); + if (proc) { + proc.kill("SIGTERM"); + } + } catch (e) { + tunnelLogger.error( + `Error while killing local ssh process for tunnel '${tunnelName}'`, + e, + ); } + activeTunnelProcesses.delete(tunnelName); + } - if (countdownIntervals.has(tunnelName)) { - clearInterval(countdownIntervals.get(tunnelName)!); - countdownIntervals.delete(tunnelName); + if (activeTunnels.has(tunnelName)) { + try { + const conn = activeTunnels.get(tunnelName); + if (conn) { + conn.end(); + } + } catch (e) { + tunnelLogger.error( + `Error while closing SSH2 Client for tunnel '${tunnelName}'`, + e, + ); } + activeTunnels.delete(tunnelName); + } + + if (tunnelVerifications.has(tunnelName)) { + const verification = tunnelVerifications.get(tunnelName); + if (verification?.timeout) clearTimeout(verification.timeout); + try { + verification?.conn.end(); + } catch (e) {} + tunnelVerifications.delete(tunnelName); + } + + const timerKeys = [ + tunnelName, + `${tunnelName}_confirm`, + `${tunnelName}_retry`, + `${tunnelName}_verify_retry`, + `${tunnelName}_ping`, + ]; + + timerKeys.forEach((key) => { + if (verificationTimers.has(key)) { + clearTimeout(verificationTimers.get(key)!); + verificationTimers.delete(key); + } + }); + + if (activeRetryTimers.has(tunnelName)) { + clearTimeout(activeRetryTimers.get(tunnelName)!); + activeRetryTimers.delete(tunnelName); + } + + if (countdownIntervals.has(tunnelName)) { + clearInterval(countdownIntervals.get(tunnelName)!); + countdownIntervals.delete(tunnelName); + } } function resetRetryState(tunnelName: string): void { - retryCounters.delete(tunnelName); - retryExhaustedTunnels.delete(tunnelName); + retryCounters.delete(tunnelName); + retryExhaustedTunnels.delete(tunnelName); - if (activeRetryTimers.has(tunnelName)) { - clearTimeout(activeRetryTimers.get(tunnelName)!); - activeRetryTimers.delete(tunnelName); + if (activeRetryTimers.has(tunnelName)) { + clearTimeout(activeRetryTimers.get(tunnelName)!); + activeRetryTimers.delete(tunnelName); + } + + if (countdownIntervals.has(tunnelName)) { + clearInterval(countdownIntervals.get(tunnelName)!); + countdownIntervals.delete(tunnelName); + } + + ["", "_confirm", "_retry", "_verify_retry", "_ping"].forEach((suffix) => { + const timerKey = `${tunnelName}${suffix}`; + if (verificationTimers.has(timerKey)) { + clearTimeout(verificationTimers.get(timerKey)!); + verificationTimers.delete(timerKey); } - - if (countdownIntervals.has(tunnelName)) { - clearInterval(countdownIntervals.get(tunnelName)!); - countdownIntervals.delete(tunnelName); - } - - ['', '_confirm', '_retry', '_verify_retry', '_ping'].forEach(suffix => { - const timerKey = `${tunnelName}${suffix}`; - if (verificationTimers.has(timerKey)) { - clearTimeout(verificationTimers.get(timerKey)!); - verificationTimers.delete(timerKey); - } - }); + }); } -function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null, shouldRetry = true): void { - if (tunnelVerifications.has(tunnelName)) { - try { +function handleDisconnect( + tunnelName: string, + tunnelConfig: TunnelConfig | null, + shouldRetry = true, +): void { + if (tunnelVerifications.has(tunnelName)) { + try { + const verification = tunnelVerifications.get(tunnelName); + if (verification?.timeout) clearTimeout(verification.timeout); + verification?.conn.end(); + } catch (e) {} + tunnelVerifications.delete(tunnelName); + } + + cleanupTunnelResources(tunnelName); + + if (manualDisconnects.has(tunnelName)) { + resetRetryState(tunnelName); + + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.DISCONNECTED, + manualDisconnect: true, + }); + return; + } + + if (retryExhaustedTunnels.has(tunnelName)) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: "Max retries already exhausted", + }); + return; + } + + if (activeRetryTimers.has(tunnelName)) { + return; + } + + if (shouldRetry && tunnelConfig) { + const maxRetries = tunnelConfig.maxRetries || 3; + const retryInterval = tunnelConfig.retryInterval || 5000; + + let retryCount = retryCounters.get(tunnelName) || 0; + retryCount = retryCount + 1; + + if (retryCount > maxRetries) { + tunnelLogger.error(`All ${maxRetries} retries failed for ${tunnelName}`); + + retryExhaustedTunnels.add(tunnelName); + activeTunnels.delete(tunnelName); + retryCounters.delete(tunnelName); + + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + retryExhausted: true, + reason: `Max retries exhausted`, + }); + return; + } + + retryCounters.set(tunnelName, retryCount); + + if (retryCount <= maxRetries) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.RETRYING, + retryCount: retryCount, + maxRetries: maxRetries, + nextRetryIn: retryInterval / 1000, + }); + + if (activeRetryTimers.has(tunnelName)) { + clearTimeout(activeRetryTimers.get(tunnelName)!); + activeRetryTimers.delete(tunnelName); + } + + const initialNextRetryIn = Math.ceil(retryInterval / 1000); + let currentNextRetryIn = initialNextRetryIn; + + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.WAITING, + retryCount: retryCount, + maxRetries: maxRetries, + nextRetryIn: currentNextRetryIn, + }); + + const countdownInterval = setInterval(() => { + currentNextRetryIn--; + if (currentNextRetryIn > 0) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.WAITING, + retryCount: retryCount, + maxRetries: maxRetries, + nextRetryIn: currentNextRetryIn, + }); + } + }, 1000); + + countdownIntervals.set(tunnelName, countdownInterval); + + const timer = setTimeout(() => { + clearInterval(countdownInterval); + countdownIntervals.delete(tunnelName); + activeRetryTimers.delete(tunnelName); + + if (!manualDisconnects.has(tunnelName)) { + activeTunnels.delete(tunnelName); + connectSSHTunnel(tunnelConfig, retryCount).catch((error) => { + tunnelLogger.error( + `Failed to connect tunnel ${tunnelConfig.name}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + }); + } + }, retryInterval); + + activeRetryTimers.set(tunnelName, timer); + } + } else { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + }); + + activeTunnels.delete(tunnelName); + } +} + +function setupPingInterval(tunnelName: string): void { + const pingKey = `${tunnelName}_ping`; + if (verificationTimers.has(pingKey)) { + clearInterval(verificationTimers.get(pingKey)!); + verificationTimers.delete(pingKey); + } + + const pingInterval = setInterval(() => { + const currentStatus = connectionStatus.get(tunnelName); + if (currentStatus?.status === CONNECTION_STATES.CONNECTED) { + if (!activeTunnels.has(tunnelName)) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.DISCONNECTED, + reason: "Tunnel connection lost", + }); + clearInterval(pingInterval); + verificationTimers.delete(pingKey); + } + } else { + clearInterval(pingInterval); + verificationTimers.delete(pingKey); + } + }, 120000); + + verificationTimers.set(pingKey, pingInterval); +} + +async function connectSSHTunnel( + tunnelConfig: TunnelConfig, + retryAttempt = 0, +): Promise { + const tunnelName = tunnelConfig.name; + const tunnelMarker = getTunnelMarker(tunnelName); + + if (manualDisconnects.has(tunnelName)) { + return; + } + + cleanupTunnelResources(tunnelName); + + if (retryAttempt === 0) { + retryExhaustedTunnels.delete(tunnelName); + retryCounters.delete(tunnelName); + } + + const currentStatus = connectionStatus.get(tunnelName); + if (!currentStatus || currentStatus.status !== CONNECTION_STATES.WAITING) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.CONNECTING, + retryCount: retryAttempt > 0 ? retryAttempt : undefined, + }); + } + + if ( + !tunnelConfig || + !tunnelConfig.sourceIP || + !tunnelConfig.sourceUsername || + !tunnelConfig.sourceSSHPort + ) { + tunnelLogger.error("Invalid tunnel connection details", { + operation: "tunnel_connect", + tunnelName, + hasSourceIP: !!tunnelConfig?.sourceIP, + hasSourceUsername: !!tunnelConfig?.sourceUsername, + hasSourceSSHPort: !!tunnelConfig?.sourceSSHPort, + }); + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: "Missing required connection details", + }); + return; + } + + let resolvedSourceCredentials = { + password: tunnelConfig.sourcePassword, + sshKey: tunnelConfig.sourceSSHKey, + keyPassword: tunnelConfig.sourceKeyPassword, + keyType: tunnelConfig.sourceKeyType, + authMethod: tunnelConfig.sourceAuthMethod, + }; + + if (tunnelConfig.sourceCredentialId && tunnelConfig.sourceUserId) { + try { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, tunnelConfig.sourceCredentialId), + eq(sshCredentials.userId, tunnelConfig.sourceUserId), + ), + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + resolvedSourceCredentials = { + password: credential.password, + sshKey: credential.key, + keyPassword: credential.keyPassword, + keyType: credential.keyType, + authMethod: credential.authType, + }; + } else { + tunnelLogger.warn("No source credentials found in database", { + operation: "tunnel_connect", + tunnelName, + credentialId: tunnelConfig.sourceCredentialId, + }); + } + } catch (error) { + tunnelLogger.warn("Failed to resolve source credentials from database", { + operation: "tunnel_connect", + tunnelName, + credentialId: tunnelConfig.sourceCredentialId, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + let resolvedEndpointCredentials = { + password: tunnelConfig.endpointPassword, + sshKey: tunnelConfig.endpointSSHKey, + keyPassword: tunnelConfig.endpointKeyPassword, + keyType: tunnelConfig.endpointKeyType, + authMethod: tunnelConfig.endpointAuthMethod, + }; + + if (tunnelConfig.endpointCredentialId && tunnelConfig.endpointUserId) { + try { + const credentials = await db + .select() + .from(sshCredentials) + .where( + and( + eq(sshCredentials.id, tunnelConfig.endpointCredentialId), + eq(sshCredentials.userId, tunnelConfig.endpointUserId), + ), + ); + + if (credentials.length > 0) { + const credential = credentials[0]; + resolvedEndpointCredentials = { + password: credential.password, + sshKey: credential.key, + keyPassword: credential.keyPassword, + keyType: credential.keyType, + authMethod: credential.authType, + }; + } else { + tunnelLogger.warn("No endpoint credentials found in database", { + operation: "tunnel_connect", + tunnelName, + credentialId: tunnelConfig.endpointCredentialId, + }); + } + } catch (error) { + tunnelLogger.warn( + `Failed to resolve endpoint credentials for tunnel ${tunnelName}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + } else if (tunnelConfig.endpointCredentialId) { + tunnelLogger.warn("Missing userId for endpoint credential resolution", { + operation: "tunnel_connect", + tunnelName, + credentialId: tunnelConfig.endpointCredentialId, + hasUserId: !!tunnelConfig.endpointUserId, + }); + } + + const conn = new Client(); + + const connectionTimeout = setTimeout(() => { + if (conn) { + if (activeRetryTimers.has(tunnelName)) { + return; + } + + try { + conn.end(); + } catch (e) {} + + activeTunnels.delete(tunnelName); + + if (!activeRetryTimers.has(tunnelName)) { + handleDisconnect( + tunnelName, + tunnelConfig, + !manualDisconnects.has(tunnelName), + ); + } + } + }, 60000); + + conn.on("error", (err) => { + clearTimeout(connectionTimeout); + tunnelLogger.error(`SSH error for '${tunnelName}': ${err.message}`); + + if (activeRetryTimers.has(tunnelName)) { + return; + } + + const errorType = classifyError(err.message); + + if (!manualDisconnects.has(tunnelName)) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + errorType: errorType, + reason: err.message, + }); + } + + activeTunnels.delete(tunnelName); + + const shouldNotRetry = + errorType === "AUTHENTICATION_FAILED" || + errorType === "CONNECTION_FAILED" || + manualDisconnects.has(tunnelName); + + handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry); + }); + + conn.on("close", () => { + clearTimeout(connectionTimeout); + + if (activeRetryTimers.has(tunnelName)) { + return; + } + + if (!manualDisconnects.has(tunnelName)) { + const currentStatus = connectionStatus.get(tunnelName); + if (!currentStatus || currentStatus.status !== CONNECTION_STATES.FAILED) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.DISCONNECTED, + }); + } + + if (!activeRetryTimers.has(tunnelName)) { + handleDisconnect( + tunnelName, + tunnelConfig, + !manualDisconnects.has(tunnelName), + ); + } + } + }); + + conn.on("ready", () => { + clearTimeout(connectionTimeout); + + const isAlreadyVerifying = tunnelVerifications.has(tunnelName); + if (isAlreadyVerifying) { + return; + } + + let tunnelCmd: string; + if ( + resolvedEndpointCredentials.authMethod === "key" && + resolvedEndpointCredentials.sshKey + ) { + const keyFilePath = `/tmp/tunnel_key_${tunnelName.replace(/[^a-zA-Z0-9]/g, "_")}`; + tunnelCmd = `echo '${resolvedEndpointCredentials.sshKey}' > ${keyFilePath} && chmod 600 ${keyFilePath} && ssh -i ${keyFilePath} -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP} ${tunnelMarker} && rm -f ${keyFilePath}`; + } else { + tunnelCmd = `sshpass -p '${resolvedEndpointCredentials.password || ""}' ssh -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP} ${tunnelMarker}`; + } + + conn.exec(tunnelCmd, (err, stream) => { + if (err) { + tunnelLogger.error( + `Connection error for '${tunnelName}': ${err.message}`, + ); + + conn.end(); + + activeTunnels.delete(tunnelName); + + const errorType = classifyError(err.message); + const shouldNotRetry = + errorType === "AUTHENTICATION_FAILED" || + errorType === "CONNECTION_FAILED"; + + handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry); + return; + } + + activeTunnels.set(tunnelName, conn); + + setTimeout(() => { + if ( + !manualDisconnects.has(tunnelName) && + activeTunnels.has(tunnelName) + ) { + broadcastTunnelStatus(tunnelName, { + connected: true, + status: CONNECTION_STATES.CONNECTED, + }); + setupPingInterval(tunnelName); + } + }, 2000); + + stream.on("close", (code: number) => { + if (activeRetryTimers.has(tunnelName)) { + return; + } + + activeTunnels.delete(tunnelName); + + if (tunnelVerifications.has(tunnelName)) { + try { const verification = tunnelVerifications.get(tunnelName); if (verification?.timeout) clearTimeout(verification.timeout); verification?.conn.end(); - } catch (e) { - } - tunnelVerifications.delete(tunnelName); - } - - cleanupTunnelResources(tunnelName); - - if (manualDisconnects.has(tunnelName)) { - resetRetryState(tunnelName); - - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.DISCONNECTED, - manualDisconnect: true - }); - return; - } - - - if (retryExhaustedTunnels.has(tunnelName)) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: "Max retries already exhausted" - }); - return; - } - - if (activeRetryTimers.has(tunnelName)) { - return; - } - - if (shouldRetry && tunnelConfig) { - const maxRetries = tunnelConfig.maxRetries || 3; - const retryInterval = tunnelConfig.retryInterval || 5000; - - let retryCount = retryCounters.get(tunnelName) || 0; - retryCount = retryCount + 1; - - if (retryCount > maxRetries) { - logger.error(`All ${maxRetries} retries failed for ${tunnelName}`); - - retryExhaustedTunnels.add(tunnelName); - activeTunnels.delete(tunnelName); - retryCounters.delete(tunnelName); - - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - retryExhausted: true, - reason: `Max retries exhausted` - }); - return; + } catch (e) {} + tunnelVerifications.delete(tunnelName); } - retryCounters.set(tunnelName, retryCount); + const isLikelyRemoteClosure = code === 255; - if (retryCount <= maxRetries) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.RETRYING, - retryCount: retryCount, - maxRetries: maxRetries, - nextRetryIn: retryInterval / 1000 - }); - - if (activeRetryTimers.has(tunnelName)) { - clearTimeout(activeRetryTimers.get(tunnelName)!); - activeRetryTimers.delete(tunnelName); - } - - const initialNextRetryIn = Math.ceil(retryInterval / 1000); - let currentNextRetryIn = initialNextRetryIn; - - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.WAITING, - retryCount: retryCount, - maxRetries: maxRetries, - nextRetryIn: currentNextRetryIn - }); - - const countdownInterval = setInterval(() => { - currentNextRetryIn--; - if (currentNextRetryIn > 0) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.WAITING, - retryCount: retryCount, - maxRetries: maxRetries, - nextRetryIn: currentNextRetryIn - }); - } - }, 1000); - - countdownIntervals.set(tunnelName, countdownInterval); - - const timer = setTimeout(() => { - clearInterval(countdownInterval); - countdownIntervals.delete(tunnelName); - activeRetryTimers.delete(tunnelName); - - if (!manualDisconnects.has(tunnelName)) { - activeTunnels.delete(tunnelName); - connectSSHTunnel(tunnelConfig, retryCount); - } - }, retryInterval); - - activeRetryTimers.set(tunnelName, timer); + if (isLikelyRemoteClosure && retryExhaustedTunnels.has(tunnelName)) { + retryExhaustedTunnels.delete(tunnelName); } - } else { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED - }); - activeTunnels.delete(tunnelName); + if ( + !manualDisconnects.has(tunnelName) && + code !== 0 && + code !== undefined + ) { + if (retryExhaustedTunnels.has(tunnelName)) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: "Max retries exhausted", + }); + } else { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: isLikelyRemoteClosure + ? "Connection closed by remote host" + : "Connection closed unexpectedly", + }); + } + } + + if ( + !activeRetryTimers.has(tunnelName) && + !retryExhaustedTunnels.has(tunnelName) + ) { + handleDisconnect( + tunnelName, + tunnelConfig, + !manualDisconnects.has(tunnelName), + ); + } else if ( + retryExhaustedTunnels.has(tunnelName) && + isLikelyRemoteClosure + ) { + retryExhaustedTunnels.delete(tunnelName); + retryCounters.delete(tunnelName); + handleDisconnect(tunnelName, tunnelConfig, true); + } + }); + + stream.stdout?.on("data", (data: Buffer) => {}); + + stream.on("error", (err: Error) => {}); + + stream.stderr.on("data", (data) => { + const errorMsg = data.toString().trim(); + }); + }); + }); + + const connOptions: any = { + host: tunnelConfig.sourceIP, + port: tunnelConfig.sourceSSHPort, + username: tunnelConfig.sourceUsername, + keepaliveInterval: 30000, + keepaliveCountMax: 3, + readyTimeout: 60000, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 15000, + algorithms: { + kex: [ + "diffie-hellman-group14-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group1-sha1", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group-exchange-sha1", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + ], + cipher: [ + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-gcm@openssh.com", + "aes256-gcm@openssh.com", + "aes128-cbc", + "aes192-cbc", + "aes256-cbc", + "3des-cbc", + ], + hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-md5"], + compress: ["none", "zlib@openssh.com", "zlib"], + }, + }; + + if ( + resolvedSourceCredentials.authMethod === "key" && + resolvedSourceCredentials.sshKey + ) { + if ( + !resolvedSourceCredentials.sshKey.includes("-----BEGIN") || + !resolvedSourceCredentials.sshKey.includes("-----END") + ) { + tunnelLogger.error( + `Invalid SSH key format for tunnel '${tunnelName}'. Key should contain both BEGIN and END markers`, + ); + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: "Invalid SSH key format", + }); + return; } + + const cleanKey = resolvedSourceCredentials.sshKey + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + connOptions.privateKey = Buffer.from(cleanKey, "utf8"); + if (resolvedSourceCredentials.keyPassword) { + connOptions.passphrase = resolvedSourceCredentials.keyPassword; + } + if ( + resolvedSourceCredentials.keyType && + resolvedSourceCredentials.keyType !== "auto" + ) { + connOptions.privateKeyType = resolvedSourceCredentials.keyType; + } + } else if (resolvedSourceCredentials.authMethod === "key") { + tunnelLogger.error( + `SSH key authentication requested but no key provided for tunnel '${tunnelName}'`, + ); + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: "SSH key authentication requested but no key provided", + }); + return; + } else { + connOptions.password = resolvedSourceCredentials.password; + } + + const finalStatus = connectionStatus.get(tunnelName); + if (!finalStatus || finalStatus.status !== CONNECTION_STATES.WAITING) { + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.CONNECTING, + retryCount: retryAttempt > 0 ? retryAttempt : undefined, + }); + } + + conn.connect(connOptions); } -function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig, isPeriodic = false): void { - if (isPeriodic) { - if (!activeTunnels.has(tunnelName)) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.DISCONNECTED, - reason: 'Tunnel connection lost' - }); - } - } -} - -function setupPingInterval(tunnelName: string, tunnelConfig: TunnelConfig): void { - const pingKey = `${tunnelName}_ping`; - if (verificationTimers.has(pingKey)) { - clearInterval(verificationTimers.get(pingKey)!); - verificationTimers.delete(pingKey); - } - - const pingInterval = setInterval(() => { - const currentStatus = connectionStatus.get(tunnelName); - if (currentStatus?.status === CONNECTION_STATES.CONNECTED) { - if (!activeTunnels.has(tunnelName)) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.DISCONNECTED, - reason: 'Tunnel connection lost' - }); - clearInterval(pingInterval); - verificationTimers.delete(pingKey); - } - } else { - clearInterval(pingInterval); - verificationTimers.delete(pingKey); - } - }, 120000); - - verificationTimers.set(pingKey, pingInterval); -} - -function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void { - const tunnelName = tunnelConfig.name; - const tunnelMarker = getTunnelMarker(tunnelName); - - if (manualDisconnects.has(tunnelName)) { - return; +function killRemoteTunnelByMarker( + tunnelConfig: TunnelConfig, + tunnelName: string, + callback: (err?: Error) => void, +) { + const tunnelMarker = getTunnelMarker(tunnelName); + const conn = new Client(); + const connOptions: any = { + host: tunnelConfig.sourceIP, + port: tunnelConfig.sourceSSHPort, + username: tunnelConfig.sourceUsername, + keepaliveInterval: 30000, + keepaliveCountMax: 3, + readyTimeout: 60000, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 15000, + algorithms: { + kex: [ + "diffie-hellman-group14-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group1-sha1", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group-exchange-sha1", + "ecdh-sha2-nistp256", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp521", + ], + cipher: [ + "aes128-ctr", + "aes192-ctr", + "aes256-ctr", + "aes128-gcm@openssh.com", + "aes256-gcm@openssh.com", + "aes128-cbc", + "aes192-cbc", + "aes256-cbc", + "3des-cbc", + ], + hmac: ["hmac-sha2-256", "hmac-sha2-512", "hmac-sha1", "hmac-md5"], + compress: ["none", "zlib@openssh.com", "zlib"], + }, + }; + if (tunnelConfig.sourceAuthMethod === "key" && tunnelConfig.sourceSSHKey) { + if ( + !tunnelConfig.sourceSSHKey.includes("-----BEGIN") || + !tunnelConfig.sourceSSHKey.includes("-----END") + ) { + callback(new Error("Invalid SSH key format")); + return; } - cleanupTunnelResources(tunnelName); - - if (retryAttempt === 0) { - retryExhaustedTunnels.delete(tunnelName); - retryCounters.delete(tunnelName); + const cleanKey = tunnelConfig.sourceSSHKey + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + connOptions.privateKey = Buffer.from(cleanKey, "utf8"); + if (tunnelConfig.sourceKeyPassword) { + connOptions.passphrase = tunnelConfig.sourceKeyPassword; } - - const currentStatus = connectionStatus.get(tunnelName); - if (!currentStatus || currentStatus.status !== CONNECTION_STATES.WAITING) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.CONNECTING, - retryCount: retryAttempt > 0 ? retryAttempt : undefined - }); + if (tunnelConfig.sourceKeyType && tunnelConfig.sourceKeyType !== "auto") { + connOptions.privateKeyType = tunnelConfig.sourceKeyType; } - - if (!tunnelConfig || !tunnelConfig.sourceIP || !tunnelConfig.sourceUsername || !tunnelConfig.sourceSSHPort) { - logger.error(`Invalid connection details for '${tunnelName}'`); - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: "Missing required connection details" - }); - return; - } - - const conn = new Client(); - - const connectionTimeout = setTimeout(() => { - if (conn) { - if (activeRetryTimers.has(tunnelName)) { - return; - } - - try { - conn.end(); - } catch (e) { - } - - activeTunnels.delete(tunnelName); - - if (!activeRetryTimers.has(tunnelName)) { - handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName)); - } - } - }, 60000); - - conn.on("error", (err) => { - clearTimeout(connectionTimeout); - logger.error(`SSH error for '${tunnelName}': ${err.message}`); - - if (activeRetryTimers.has(tunnelName)) { - return; - } - - const errorType = classifyError(err.message); - - if (!manualDisconnects.has(tunnelName)) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - errorType: errorType, - reason: err.message - }); - } - - activeTunnels.delete(tunnelName); - - const shouldNotRetry = errorType === ERROR_TYPES.AUTH || - errorType === ERROR_TYPES.PORT || - errorType === ERROR_TYPES.PERMISSION || - manualDisconnects.has(tunnelName); - - - - handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry); - }); - - conn.on("close", () => { - clearTimeout(connectionTimeout); - - if (activeRetryTimers.has(tunnelName)) { - return; - } - - if (!manualDisconnects.has(tunnelName)) { - const currentStatus = connectionStatus.get(tunnelName); - if (!currentStatus || currentStatus.status !== CONNECTION_STATES.FAILED) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.DISCONNECTED - }); - } - - if (!activeRetryTimers.has(tunnelName)) { - handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName)); - } - } - }); - - conn.on("ready", () => { - clearTimeout(connectionTimeout); - - const isAlreadyVerifying = tunnelVerifications.has(tunnelName); - if (isAlreadyVerifying) { - return; - } - - let tunnelCmd: string; - if (tunnelConfig.endpointAuthMethod === "key" && tunnelConfig.endpointSSHKey) { - const keyFilePath = `/tmp/tunnel_key_${tunnelName.replace(/[^a-zA-Z0-9]/g, '_')}`; - tunnelCmd = `echo '${tunnelConfig.endpointSSHKey}' > ${keyFilePath} && chmod 600 ${keyFilePath} && ssh -i ${keyFilePath} -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP} ${tunnelMarker} && rm -f ${keyFilePath}`; - } else { - tunnelCmd = `sshpass -p '${tunnelConfig.endpointPassword || ''}' ssh -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP} ${tunnelMarker}`; - } - - conn.exec(tunnelCmd, (err, stream) => { - if (err) { - logger.error(`Connection error for '${tunnelName}': ${err.message}`); - - conn.end(); - - activeTunnels.delete(tunnelName); - - const errorType = classifyError(err.message); - const shouldNotRetry = errorType === ERROR_TYPES.AUTH || - errorType === ERROR_TYPES.PORT || - errorType === ERROR_TYPES.PERMISSION; - - handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry); - return; - } - - activeTunnels.set(tunnelName, conn); - - setTimeout(() => { - if (!manualDisconnects.has(tunnelName) && activeTunnels.has(tunnelName)) { - broadcastTunnelStatus(tunnelName, { - connected: true, - status: CONNECTION_STATES.CONNECTED - }); - setupPingInterval(tunnelName, tunnelConfig); - } - }, 2000); - - stream.on("close", (code: number) => { - if (activeRetryTimers.has(tunnelName)) { - return; - } - - activeTunnels.delete(tunnelName); - - if (tunnelVerifications.has(tunnelName)) { - try { - const verification = tunnelVerifications.get(tunnelName); - if (verification?.timeout) clearTimeout(verification.timeout); - verification?.conn.end(); - } catch (e) { - } - tunnelVerifications.delete(tunnelName); - } - - const isLikelyRemoteClosure = code === 255; - - if (isLikelyRemoteClosure && retryExhaustedTunnels.has(tunnelName)) { - retryExhaustedTunnels.delete(tunnelName); - } - - if (!manualDisconnects.has(tunnelName) && code !== 0 && code !== undefined) { - if (retryExhaustedTunnels.has(tunnelName)) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: "Max retries exhausted" - }); - } else { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: isLikelyRemoteClosure ? "Connection closed by remote host" : "Connection closed unexpectedly" - }); - } - } - - if (!activeRetryTimers.has(tunnelName) && !retryExhaustedTunnels.has(tunnelName)) { - handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName)); - } else if (retryExhaustedTunnels.has(tunnelName) && isLikelyRemoteClosure) { - retryExhaustedTunnels.delete(tunnelName); - retryCounters.delete(tunnelName); - handleDisconnect(tunnelName, tunnelConfig, true); - } - }); - - stream.stdout?.on("data", (data: Buffer) => { - }); - - stream.on("error", (err: Error) => { - }); - - stream.stderr.on("data", (data) => { - const errorMsg = data.toString().trim(); - }); - }); - }); - - const connOptions: any = { - host: tunnelConfig.sourceIP, - port: tunnelConfig.sourceSSHPort, - username: tunnelConfig.sourceUsername, - keepaliveInterval: 30000, - keepaliveCountMax: 3, - readyTimeout: 60000, - tcpKeepAlive: true, - tcpKeepAliveInitialDelay: 15000, - algorithms: { - kex: [ - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group1-sha1', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group-exchange-sha1', - 'ecdh-sha2-nistp256', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp521' - ], - cipher: [ - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - 'aes128-gcm@openssh.com', - 'aes256-gcm@openssh.com', - 'aes128-cbc', - 'aes192-cbc', - 'aes256-cbc', - '3des-cbc' - ], - hmac: [ - 'hmac-sha2-256', - 'hmac-sha2-512', - 'hmac-sha1', - 'hmac-md5' - ], - compress: [ - 'none', - 'zlib@openssh.com', - 'zlib' - ] - } - }; - - if (tunnelConfig.sourceAuthMethod === "key" && tunnelConfig.sourceSSHKey) { - if (!tunnelConfig.sourceSSHKey.includes('-----BEGIN') || !tunnelConfig.sourceSSHKey.includes('-----END')) { - logger.error(`Invalid SSH key format for tunnel '${tunnelName}'. Key should contain both BEGIN and END markers`); - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: "Invalid SSH key format" - }); - return; - } - - const cleanKey = tunnelConfig.sourceSSHKey.trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - connOptions.privateKey = Buffer.from(cleanKey, 'utf8'); - if (tunnelConfig.sourceKeyPassword) { - connOptions.passphrase = tunnelConfig.sourceKeyPassword; - } - if (tunnelConfig.sourceKeyType && tunnelConfig.sourceKeyType !== 'auto') { - connOptions.privateKeyType = tunnelConfig.sourceKeyType; - } - } else if (tunnelConfig.sourceAuthMethod === "key") { - logger.error(`SSH key authentication requested but no key provided for tunnel '${tunnelName}'`); - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: "SSH key authentication requested but no key provided" - }); - return; - } else { - connOptions.password = tunnelConfig.sourcePassword; - } - - const finalStatus = connectionStatus.get(tunnelName); - if (!finalStatus || finalStatus.status !== CONNECTION_STATES.WAITING) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.CONNECTING, - retryCount: retryAttempt > 0 ? retryAttempt : undefined - }); - } - - conn.connect(connOptions); -} - -function killRemoteTunnelByMarker(tunnelConfig: TunnelConfig, tunnelName: string, callback: (err?: Error) => void) { - const tunnelMarker = getTunnelMarker(tunnelName); - const conn = new Client(); - const connOptions: any = { - host: tunnelConfig.sourceIP, - port: tunnelConfig.sourceSSHPort, - username: tunnelConfig.sourceUsername, - keepaliveInterval: 30000, - keepaliveCountMax: 3, - readyTimeout: 60000, - tcpKeepAlive: true, - tcpKeepAliveInitialDelay: 15000, - algorithms: { - kex: [ - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group14-sha1', - 'diffie-hellman-group1-sha1', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group-exchange-sha1', - 'ecdh-sha2-nistp256', - 'ecdh-sha2-nistp384', - 'ecdh-sha2-nistp521' - ], - cipher: [ - 'aes128-ctr', - 'aes192-ctr', - 'aes256-ctr', - 'aes128-gcm@openssh.com', - 'aes256-gcm@openssh.com', - 'aes128-cbc', - 'aes192-cbc', - 'aes256-cbc', - '3des-cbc' - ], - hmac: [ - 'hmac-sha2-256', - 'hmac-sha2-512', - 'hmac-sha1', - 'hmac-md5' - ], - compress: [ - 'none', - 'zlib@openssh.com', - 'zlib' - ] - } - }; - if (tunnelConfig.sourceAuthMethod === "key" && tunnelConfig.sourceSSHKey) { - if (!tunnelConfig.sourceSSHKey.includes('-----BEGIN') || !tunnelConfig.sourceSSHKey.includes('-----END')) { - callback(new Error('Invalid SSH key format')); - return; - } - - const cleanKey = tunnelConfig.sourceSSHKey.trim().replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - connOptions.privateKey = Buffer.from(cleanKey, 'utf8'); - if (tunnelConfig.sourceKeyPassword) { - connOptions.passphrase = tunnelConfig.sourceKeyPassword; - } - if (tunnelConfig.sourceKeyType && tunnelConfig.sourceKeyType !== 'auto') { - connOptions.privateKeyType = tunnelConfig.sourceKeyType; - } - } else { - connOptions.password = tunnelConfig.sourcePassword; - } - conn.on('ready', () => { - const killCmd = `pkill -f '${tunnelMarker}'`; - conn.exec(killCmd, (err, stream) => { - if (err) { - conn.end(); - callback(err); - return; - } - stream.on('close', () => { - conn.end(); - callback(); - }); - stream.on('data', () => { - }); - stream.stderr.on('data', () => { - }); - }); - }); - conn.on('error', (err) => { + } else { + connOptions.password = tunnelConfig.sourcePassword; + } + conn.on("ready", () => { + const killCmd = `pkill -f '${tunnelMarker}'`; + conn.exec(killCmd, (err, stream) => { + if (err) { + conn.end(); callback(err); + return; + } + stream.on("close", () => { + conn.end(); + callback(); + }); + stream.on("data", () => {}); + stream.stderr.on("data", () => {}); }); - conn.connect(connOptions); + }); + conn.on("error", (err) => { + callback(err); + }); + conn.connect(connOptions); } -app.get('/ssh/tunnel/status', (req, res) => { - res.json(getAllTunnelStatus()); +app.get("/ssh/tunnel/status", (req, res) => { + res.json(getAllTunnelStatus()); }); -app.get('/ssh/tunnel/status/:tunnelName', (req, res) => { - const {tunnelName} = req.params; - const status = connectionStatus.get(tunnelName); +app.get("/ssh/tunnel/status/:tunnelName", (req, res) => { + const { tunnelName } = req.params; + const status = connectionStatus.get(tunnelName); - if (!status) { - return res.status(404).json({error: 'Tunnel not found'}); - } + if (!status) { + return res.status(404).json({ error: "Tunnel not found" }); + } - res.json({name: tunnelName, status}); + res.json({ name: tunnelName, status }); }); -app.post('/ssh/tunnel/connect', (req, res) => { - const tunnelConfig: TunnelConfig = req.body; +app.post("/ssh/tunnel/connect", (req, res) => { + const tunnelConfig: TunnelConfig = req.body; - if (!tunnelConfig || !tunnelConfig.name) { - return res.status(400).json({error: 'Invalid tunnel configuration'}); - } + if (!tunnelConfig || !tunnelConfig.name) { + return res.status(400).json({ error: "Invalid tunnel configuration" }); + } - const tunnelName = tunnelConfig.name; + const tunnelName = tunnelConfig.name; + manualDisconnects.delete(tunnelName); + retryCounters.delete(tunnelName); + retryExhaustedTunnels.delete(tunnelName); + + tunnelConfigs.set(tunnelName, tunnelConfig); + + connectSSHTunnel(tunnelConfig, 0).catch((error) => { + tunnelLogger.error( + `Failed to connect tunnel ${tunnelConfig.name}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + }); + + res.json({ message: "Connection request received", tunnelName }); +}); + +app.post("/ssh/tunnel/disconnect", (req, res) => { + const { tunnelName } = req.body; + + if (!tunnelName) { + return res.status(400).json({ error: "Tunnel name required" }); + } + + manualDisconnects.add(tunnelName); + retryCounters.delete(tunnelName); + retryExhaustedTunnels.delete(tunnelName); + + if (activeRetryTimers.has(tunnelName)) { + clearTimeout(activeRetryTimers.get(tunnelName)!); + activeRetryTimers.delete(tunnelName); + } + + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.DISCONNECTED, + manualDisconnect: true, + }); + + const tunnelConfig = tunnelConfigs.get(tunnelName) || null; + handleDisconnect(tunnelName, tunnelConfig, false); + + setTimeout(() => { manualDisconnects.delete(tunnelName); - retryCounters.delete(tunnelName); - retryExhaustedTunnels.delete(tunnelName); + }, 5000); - tunnelConfigs.set(tunnelName, tunnelConfig); - - connectSSHTunnel(tunnelConfig, 0); - - res.json({message: 'Connection request received', tunnelName}); + res.json({ message: "Disconnect request received", tunnelName }); }); -app.post('/ssh/tunnel/disconnect', (req, res) => { - const {tunnelName} = req.body; +app.post("/ssh/tunnel/cancel", (req, res) => { + const { tunnelName } = req.body; - if (!tunnelName) { - return res.status(400).json({error: 'Tunnel name required'}); - } + if (!tunnelName) { + return res.status(400).json({ error: "Tunnel name required" }); + } - manualDisconnects.add(tunnelName); - retryCounters.delete(tunnelName); - retryExhaustedTunnels.delete(tunnelName); + retryCounters.delete(tunnelName); + retryExhaustedTunnels.delete(tunnelName); - if (activeRetryTimers.has(tunnelName)) { - clearTimeout(activeRetryTimers.get(tunnelName)!); - activeRetryTimers.delete(tunnelName); - } + if (activeRetryTimers.has(tunnelName)) { + clearTimeout(activeRetryTimers.get(tunnelName)!); + activeRetryTimers.delete(tunnelName); + } - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.DISCONNECTED, - manualDisconnect: true - }); + if (countdownIntervals.has(tunnelName)) { + clearInterval(countdownIntervals.get(tunnelName)!); + countdownIntervals.delete(tunnelName); + } - const tunnelConfig = tunnelConfigs.get(tunnelName) || null; - handleDisconnect(tunnelName, tunnelConfig, false); + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.DISCONNECTED, + manualDisconnect: true, + }); - setTimeout(() => { - manualDisconnects.delete(tunnelName); - }, 5000); + const tunnelConfig = tunnelConfigs.get(tunnelName) || null; + handleDisconnect(tunnelName, tunnelConfig, false); - res.json({message: 'Disconnect request received', tunnelName}); -}); + setTimeout(() => { + manualDisconnects.delete(tunnelName); + }, 5000); -app.post('/ssh/tunnel/cancel', (req, res) => { - const {tunnelName} = req.body; - - if (!tunnelName) { - return res.status(400).json({error: 'Tunnel name required'}); - } - - retryCounters.delete(tunnelName); - retryExhaustedTunnels.delete(tunnelName); - - if (activeRetryTimers.has(tunnelName)) { - clearTimeout(activeRetryTimers.get(tunnelName)!); - activeRetryTimers.delete(tunnelName); - } - - if (countdownIntervals.has(tunnelName)) { - clearInterval(countdownIntervals.get(tunnelName)!); - countdownIntervals.delete(tunnelName); - } - - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.DISCONNECTED, - manualDisconnect: true - }); - - const tunnelConfig = tunnelConfigs.get(tunnelName) || null; - handleDisconnect(tunnelName, tunnelConfig, false); - - setTimeout(() => { - manualDisconnects.delete(tunnelName); - }, 5000); - - res.json({message: 'Cancel request received', tunnelName}); + res.json({ message: "Cancel request received", tunnelName }); }); async function initializeAutoStartTunnels(): Promise { - try { - const response = await axios.get('http://localhost:8081/ssh/db/host/internal', { - headers: { - 'Content-Type': 'application/json', - 'X-Internal-Request': '1' - } - }); - - const hosts: SSHHost[] = response.data || []; - const autoStartTunnels: TunnelConfig[] = []; - - for (const host of hosts) { - if (host.enableTunnel && host.tunnelConnections) { - for (const tunnelConnection of host.tunnelConnections) { - if (tunnelConnection.autoStart) { - const endpointHost = hosts.find(h => - h.name === tunnelConnection.endpointHost || - `${h.username}@${h.ip}` === tunnelConnection.endpointHost - ); - - if (endpointHost) { - const tunnelConfig: TunnelConfig = { - name: `${host.name || `${host.username}@${host.ip}`}_${tunnelConnection.sourcePort}_${tunnelConnection.endpointPort}`, - hostName: host.name || `${host.username}@${host.ip}`, - sourceIP: host.ip, - sourceSSHPort: host.port, - sourceUsername: host.username, - sourcePassword: host.password, - sourceAuthMethod: host.authType, - sourceSSHKey: host.key, - sourceKeyPassword: host.keyPassword, - sourceKeyType: host.keyType, - endpointIP: endpointHost.ip, - endpointSSHPort: endpointHost.port, - endpointUsername: endpointHost.username, - endpointPassword: endpointHost.password, - endpointAuthMethod: endpointHost.authType, - endpointSSHKey: endpointHost.key, - endpointKeyPassword: endpointHost.keyPassword, - endpointKeyType: endpointHost.keyType, - sourcePort: tunnelConnection.sourcePort, - endpointPort: tunnelConnection.endpointPort, - maxRetries: tunnelConnection.maxRetries, - retryInterval: tunnelConnection.retryInterval * 1000, - autoStart: tunnelConnection.autoStart, - isPinned: host.pin - }; - - autoStartTunnels.push(tunnelConfig); - } - } - } + try { + const response = await axios.get( + "http://localhost:8081/ssh/db/host/internal", + { + headers: { + "Content-Type": "application/json", + "X-Internal-Request": "1", + }, + }, + ); + + const hosts: SSHHost[] = response.data || []; + const autoStartTunnels: TunnelConfig[] = []; + + for (const host of hosts) { + if (host.enableTunnel && host.tunnelConnections) { + for (const tunnelConnection of host.tunnelConnections) { + if (tunnelConnection.autoStart) { + const endpointHost = hosts.find( + (h) => + h.name === tunnelConnection.endpointHost || + `${h.username}@${h.ip}` === tunnelConnection.endpointHost, + ); + + if (endpointHost) { + const tunnelConfig: TunnelConfig = { + name: `${host.name || `${host.username}@${host.ip}`}_${tunnelConnection.sourcePort}_${tunnelConnection.endpointPort}`, + hostName: host.name || `${host.username}@${host.ip}`, + sourceIP: host.ip, + sourceSSHPort: host.port, + sourceUsername: host.username, + sourcePassword: host.password, + sourceAuthMethod: host.authType, + sourceSSHKey: host.key, + sourceKeyPassword: host.keyPassword, + sourceKeyType: host.keyType, + endpointIP: endpointHost.ip, + endpointSSHPort: endpointHost.port, + endpointUsername: endpointHost.username, + endpointPassword: endpointHost.password, + endpointAuthMethod: endpointHost.authType, + endpointSSHKey: endpointHost.key, + endpointKeyPassword: endpointHost.keyPassword, + endpointKeyType: endpointHost.keyType, + sourcePort: tunnelConnection.sourcePort, + endpointPort: tunnelConnection.endpointPort, + maxRetries: tunnelConnection.maxRetries, + retryInterval: tunnelConnection.retryInterval * 1000, + autoStart: tunnelConnection.autoStart, + isPinned: host.pin, + }; + + autoStartTunnels.push(tunnelConfig); } + } } - - logger.info(`Found ${autoStartTunnels.length} auto-start tunnels`); - - for (const tunnelConfig of autoStartTunnels) { - tunnelConfigs.set(tunnelConfig.name, tunnelConfig); - - setTimeout(() => { - connectSSHTunnel(tunnelConfig, 0); - }, 1000); - } - } catch (error: any) { - logger.error('Failed to initialize auto-start tunnels:', error.message); + } } + + tunnelLogger.info(`Found ${autoStartTunnels.length} auto-start tunnels`); + + for (const tunnelConfig of autoStartTunnels) { + tunnelConfigs.set(tunnelConfig.name, tunnelConfig); + + setTimeout(() => { + connectSSHTunnel(tunnelConfig, 0).catch((error) => { + tunnelLogger.error( + `Failed to connect tunnel ${tunnelConfig.name}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + }); + }, 1000); + } + } catch (error: any) { + tunnelLogger.error( + "Failed to initialize auto-start tunnels:", + error.message, + ); + } } const PORT = 8083; app.listen(PORT, () => { - setTimeout(() => { - initializeAutoStartTunnels(); - }, 2000); -}); \ No newline at end of file + tunnelLogger.success("SSH Tunnel API server started", { + operation: "server_start", + port: PORT, + }); + setTimeout(() => { + initializeAutoStartTunnels(); + }, 2000); +}); diff --git a/src/backend/starter.ts b/src/backend/starter.ts index fcfe1dd9..83caf7ed 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -1,56 +1,65 @@ // npx tsc -p tsconfig.node.json // node ./dist/backend/starter.js -import './database/database.js' -import './ssh/terminal.js'; -import './ssh/tunnel.js'; -import './ssh/file-manager.js'; -import './ssh/server-stats.js'; -import chalk from 'chalk'; - -const fixedIconSymbol = '🚀'; - -const getTimeStamp = (): string => { - return chalk.gray(`[${new Date().toLocaleTimeString()}]`); -}; - -const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { - return `${getTimeStamp()} ${colorFn(`[${level.toUpperCase()}]`)} ${chalk.hex('#1e3a8a')(`[${fixedIconSymbol}]`)} ${message}`; -}; - -const logger = { - info: (msg: string): void => { - console.log(formatMessage('info', chalk.cyan, msg)); - }, - warn: (msg: string): void => { - console.warn(formatMessage('warn', chalk.yellow, msg)); - }, - error: (msg: string, err?: unknown): void => { - console.error(formatMessage('error', chalk.redBright, msg)); - if (err) console.error(err); - }, - success: (msg: string): void => { - console.log(formatMessage('success', chalk.greenBright, msg)); - }, - debug: (msg: string): void => { - if (process.env.NODE_ENV !== 'production') { - console.debug(formatMessage('debug', chalk.magenta, msg)); - } - } -}; +import "./database/database.js"; +import "./ssh/terminal.js"; +import "./ssh/tunnel.js"; +import "./ssh/file-manager.js"; +import "./ssh/server-stats.js"; +import { systemLogger, versionLogger } from "./utils/logger.js"; +import "dotenv/config"; (async () => { - try { - logger.info("Starting all backend servers..."); + try { + const version = process.env.VERSION || "unknown"; + versionLogger.info(`Termix Backend starting - Version: ${version}`, { + operation: "startup", + version: version, + }); - logger.success("All servers started successfully"); + systemLogger.info("Initializing backend services...", { + operation: "startup", + }); - process.on('SIGINT', () => { - logger.info("Shutting down servers..."); - process.exit(0); - }); - } catch (error) { - logger.error("Failed to start servers:", error); - process.exit(1); - } -})(); \ No newline at end of file + systemLogger.success("All backend services initialized successfully", { + operation: "startup_complete", + services: ["database", "terminal", "tunnel", "file_manager", "stats"], + version: version, + }); + + process.on("SIGINT", () => { + systemLogger.info( + "Received SIGINT signal, initiating graceful shutdown...", + { operation: "shutdown" }, + ); + process.exit(0); + }); + + process.on("SIGTERM", () => { + systemLogger.info( + "Received SIGTERM signal, initiating graceful shutdown...", + { operation: "shutdown" }, + ); + process.exit(0); + }); + + process.on("uncaughtException", (error) => { + systemLogger.error("Uncaught exception occurred", error, { + operation: "error_handling", + }); + process.exit(1); + }); + + process.on("unhandledRejection", (reason, promise) => { + systemLogger.error("Unhandled promise rejection", reason, { + operation: "error_handling", + }); + process.exit(1); + }); + } catch (error) { + systemLogger.error("Failed to initialize backend services", error, { + operation: "startup_failed", + }); + process.exit(1); + } +})(); diff --git a/src/backend/utils/logger.ts b/src/backend/utils/logger.ts new file mode 100644 index 00000000..598e10a8 --- /dev/null +++ b/src/backend/utils/logger.ts @@ -0,0 +1,174 @@ +import chalk from "chalk"; + +export type LogLevel = "debug" | "info" | "warn" | "error" | "success"; + +export interface LogContext { + service?: string; + operation?: string; + userId?: string; + hostId?: number; + tunnelName?: string; + sessionId?: string; + requestId?: string; + duration?: number; + [key: string]: any; +} + +class Logger { + private serviceName: string; + private serviceIcon: string; + private serviceColor: string; + + constructor(serviceName: string, serviceIcon: string, serviceColor: string) { + this.serviceName = serviceName; + this.serviceIcon = serviceIcon; + this.serviceColor = serviceColor; + } + + private getTimeStamp(): string { + return chalk.gray(`[${new Date().toLocaleTimeString()}]`); + } + + private formatMessage( + level: LogLevel, + message: string, + context?: LogContext, + ): string { + const timestamp = this.getTimeStamp(); + const levelColor = this.getLevelColor(level); + const serviceTag = chalk.hex(this.serviceColor)(`[${this.serviceIcon}]`); + const levelTag = levelColor(`[${level.toUpperCase()}]`); + + let contextStr = ""; + if (context) { + const contextParts = []; + if (context.operation) contextParts.push(`op:${context.operation}`); + if (context.userId) contextParts.push(`user:${context.userId}`); + if (context.hostId) contextParts.push(`host:${context.hostId}`); + if (context.tunnelName) contextParts.push(`tunnel:${context.tunnelName}`); + if (context.sessionId) contextParts.push(`session:${context.sessionId}`); + if (context.requestId) contextParts.push(`req:${context.requestId}`); + if (context.duration) contextParts.push(`duration:${context.duration}ms`); + + if (contextParts.length > 0) { + contextStr = chalk.gray(` [${contextParts.join(",")}]`); + } + } + + return `${timestamp} ${levelTag} ${serviceTag} ${message}${contextStr}`; + } + + private getLevelColor(level: LogLevel): chalk.Chalk { + switch (level) { + case "debug": + return chalk.magenta; + case "info": + return chalk.cyan; + case "warn": + return chalk.yellow; + case "error": + return chalk.redBright; + case "success": + return chalk.greenBright; + default: + return chalk.white; + } + } + + private shouldLog(level: LogLevel): boolean { + if (level === "debug" && process.env.NODE_ENV === "production") { + return false; + } + return true; + } + + debug(message: string, context?: LogContext): void { + if (!this.shouldLog("debug")) return; + console.debug(this.formatMessage("debug", message, context)); + } + + info(message: string, context?: LogContext): void { + if (!this.shouldLog("info")) return; + console.log(this.formatMessage("info", message, context)); + } + + warn(message: string, context?: LogContext): void { + if (!this.shouldLog("warn")) return; + console.warn(this.formatMessage("warn", message, context)); + } + + error(message: string, error?: unknown, context?: LogContext): void { + if (!this.shouldLog("error")) return; + console.error(this.formatMessage("error", message, context)); + if (error) { + console.error(error); + } + } + + success(message: string, context?: LogContext): void { + if (!this.shouldLog("success")) return; + console.log(this.formatMessage("success", message, context)); + } + + auth(message: string, context?: LogContext): void { + this.info(`AUTH: ${message}`, { ...context, operation: "auth" }); + } + + db(message: string, context?: LogContext): void { + this.info(`DB: ${message}`, { ...context, operation: "database" }); + } + + ssh(message: string, context?: LogContext): void { + this.info(`SSH: ${message}`, { ...context, operation: "ssh" }); + } + + tunnel(message: string, context?: LogContext): void { + this.info(`TUNNEL: ${message}`, { ...context, operation: "tunnel" }); + } + + file(message: string, context?: LogContext): void { + this.info(`FILE: ${message}`, { ...context, operation: "file" }); + } + + api(message: string, context?: LogContext): void { + this.info(`API: ${message}`, { ...context, operation: "api" }); + } + + request(message: string, context?: LogContext): void { + this.info(`REQUEST: ${message}`, { ...context, operation: "request" }); + } + + response(message: string, context?: LogContext): void { + this.info(`RESPONSE: ${message}`, { ...context, operation: "response" }); + } + + connection(message: string, context?: LogContext): void { + this.info(`CONNECTION: ${message}`, { + ...context, + operation: "connection", + }); + } + + disconnect(message: string, context?: LogContext): void { + this.info(`DISCONNECT: ${message}`, { + ...context, + operation: "disconnect", + }); + } + + retry(message: string, context?: LogContext): void { + this.warn(`RETRY: ${message}`, { ...context, operation: "retry" }); + } +} + +export const databaseLogger = new Logger("DATABASE", "🗄️", "#6366f1"); +export const sshLogger = new Logger("SSH", "🖥️", "#0ea5e9"); +export const tunnelLogger = new Logger("TUNNEL", "📡", "#a855f7"); +export const fileLogger = new Logger("FILE", "📁", "#f59e0b"); +export const statsLogger = new Logger("STATS", "📊", "#22c55e"); +export const apiLogger = new Logger("API", "🌐", "#3b82f6"); +export const authLogger = new Logger("AUTH", "🔐", "#ef4444"); +export const systemLogger = new Logger("SYSTEM", "🚀", "#14b8a6"); +export const versionLogger = new Logger("VERSION", "📦", "#8b5cf6"); + +export const logger = systemLogger; diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx index 4977fd23..e18440d7 100644 --- a/src/components/theme-provider.tsx +++ b/src/components/theme-provider.tsx @@ -1,73 +1,73 @@ -import {createContext, useContext, useEffect, useState} from "react" +import { createContext, useContext, useEffect, useState } from "react"; -type Theme = "dark" | "light" | "system" +type Theme = "dark" | "light" | "system"; type ThemeProviderProps = { - children: React.ReactNode - defaultTheme?: Theme - storageKey?: string -} + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; type ThemeProviderState = { - theme: Theme - setTheme: (theme: Theme) => void -} + theme: Theme; + setTheme: (theme: Theme) => void; +}; const initialState: ThemeProviderState = { - theme: "system", - setTheme: () => null, -} + theme: "system", + setTheme: () => null, +}; -const ThemeProviderContext = createContext(initialState) +const ThemeProviderContext = createContext(initialState); export function ThemeProvider({ - children, - defaultTheme = "system", - storageKey = "vite-ui-theme", - ...props - }: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme - ) + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, + ); - useEffect(() => { - const root = window.document.documentElement + useEffect(() => { + const root = window.document.documentElement; - root.classList.remove("light", "dark") + root.classList.remove("light", "dark"); - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light" + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; - root.classList.add(systemTheme) - return - } - - root.classList.add(theme) - }, [theme]) - - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme) - setTheme(theme) - }, + root.classList.add(systemTheme); + return; } - return ( - - {children} - - ) + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); } export const useTheme = () => { - const context = useContext(ThemeProviderContext) + const context = useContext(ThemeProviderContext); - if (context === undefined) - throw new Error("useTheme must be used within a ThemeProvider") + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); - return context -} \ No newline at end of file + return context; +}; diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index d21b65f7..720bff51 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -1,13 +1,13 @@ -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDownIcon } from "lucide-react" +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Accordion({ ...props }: React.ComponentProps) { - return + return ; } function AccordionItem({ @@ -20,7 +20,7 @@ function AccordionItem({ className={cn("border-b last:border-b-0", className)} {...props} /> - ) + ); } function AccordionTrigger({ @@ -34,7 +34,7 @@ function AccordionTrigger({ data-slot="accordion-trigger" className={cn( "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", - className + className, )} {...props} > @@ -42,7 +42,7 @@ function AccordionTrigger({ - ) + ); } function AccordionContent({ @@ -58,7 +58,7 @@ function AccordionContent({ >
{children}
- ) + ); } -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index eda4eee8..2879e585 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const alertVariants = cva( "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", @@ -16,8 +16,8 @@ const alertVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); function Alert({ className, @@ -31,7 +31,7 @@ function Alert({ className={cn(alertVariants({ variant }), className)} {...props} /> - ) + ); } function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -40,11 +40,11 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { data-slot="alert-title" className={cn( "col-start-2 font-medium tracking-tight whitespace-normal break-words", - className + className, )} {...props} /> - ) + ); } function AlertDescription({ @@ -56,11 +56,11 @@ function AlertDescription({ data-slot="alert-description" className={cn( "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed", - className + className, )} {...props} /> - ) + ); } -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 02054139..46f988c2 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", @@ -22,8 +22,8 @@ const badgeVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); function Badge({ className, @@ -32,7 +32,7 @@ function Badge({ ...props }: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" + const Comp = asChild ? Slot : "span"; return ( - ) + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/src/components/ui/button-group.tsx b/src/components/ui/button-group.tsx index e3215e1b..46f8cda3 100644 --- a/src/components/ui/button-group.tsx +++ b/src/components/ui/button-group.tsx @@ -1,37 +1,37 @@ -import { Children, ReactElement, cloneElement, isValidElement } from 'react'; +import { Children, ReactElement, cloneElement, isValidElement } from "react"; -import { ButtonProps } from '@/components/ui/button'; -import { cn } from '@/lib/utils'; +import { type ButtonProps } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; interface ButtonGroupProps { className?: string; - orientation?: 'horizontal' | 'vertical'; + orientation?: "horizontal" | "vertical"; children: ReactElement[] | React.ReactNode; } export const ButtonGroup = ({ className, - orientation = 'horizontal', + orientation = "horizontal", children, }: ButtonGroupProps) => { - const isHorizontal = orientation === 'horizontal'; - const isVertical = orientation === 'vertical'; + const isHorizontal = orientation === "horizontal"; + const isVertical = orientation === "vertical"; // Normalize and filter only valid React elements - const childArray = Children.toArray(children).filter((child): child is ReactElement => - isValidElement(child) + const childArray = Children.toArray(children).filter( + (child): child is ReactElement => isValidElement(child), ); const totalButtons = childArray.length; return (
{childArray.map((child, index) => { @@ -41,18 +41,18 @@ export const ButtonGroup = ({ return cloneElement(child, { className: cn( { - 'rounded-l-none': isHorizontal && !isFirst, - 'rounded-r-none': isHorizontal && !isLast, - 'border-l-0': isHorizontal && !isFirst, + "rounded-l-none": isHorizontal && !isFirst, + "rounded-r-none": isHorizontal && !isLast, + "border-l-0": isHorizontal && !isFirst, - 'rounded-t-none': isVertical && !isFirst, - 'rounded-b-none': isVertical && !isLast, - 'border-t-0': isVertical && !isFirst, + "rounded-t-none": isVertical && !isFirst, + "rounded-b-none": isVertical && !isLast, + "border-t-0": isVertical && !isFirst, }, - child.props.className + child.props.className, ), }); })}
); -}; \ No newline at end of file +}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index a2df8dce..8b2e9e72 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -32,8 +32,14 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); + +export interface ButtonProps + extends React.ComponentProps<"button">, + VariantProps { + asChild?: boolean; +} function Button({ className, @@ -41,11 +47,8 @@ function Button({ size, asChild = false, ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" +}: ButtonProps) { + const Comp = asChild ? Slot : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants, type ButtonProps }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index d05bbc6c..113d66c7 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Card({ className, ...props }: React.ComponentProps<"div">) { return ( @@ -8,11 +8,11 @@ function Card({ className, ...props }: React.ComponentProps<"div">) { data-slot="card" className={cn( "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", - className + className, )} {...props} /> - ) + ); } function CardHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -21,11 +21,11 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-header" className={cn( "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", - className + className, )} {...props} /> - ) + ); } function CardTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -35,7 +35,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) { className={cn("leading-none font-semibold", className)} {...props} /> - ) + ); } function CardDescription({ className, ...props }: React.ComponentProps<"div">) { @@ -45,7 +45,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) { className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } function CardAction({ className, ...props }: React.ComponentProps<"div">) { @@ -54,11 +54,11 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) { data-slot="card-action" className={cn( "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className + className, )} {...props} /> - ) + ); } function CardContent({ className, ...props }: React.ComponentProps<"div">) { @@ -68,7 +68,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { className={cn("px-6", className)} {...props} /> - ) + ); } function CardFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -78,7 +78,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex items-center px-6 [.border-t]:pt-6", className)} {...props} /> - ) + ); } export { @@ -89,4 +89,4 @@ export { CardAction, CardDescription, CardContent, -} +}; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index defeb01f..29c5f2ed 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { CheckIcon } from "lucide-react" +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { CheckIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Checkbox({ className, @@ -13,7 +13,7 @@ function Checkbox({ data-slot="checkbox" className={cn( "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} {...props} > @@ -24,7 +24,7 @@ function Checkbox({ - ) + ); } -export { Checkbox } +export { Checkbox }; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..61ab08e9 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, Circle } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index 7d7474cc..4ebbfe9c 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -1,6 +1,6 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; import { Controller, FormProvider, @@ -9,23 +9,23 @@ import { type ControllerProps, type FieldPath, type FieldValues, -} from "react-hook-form" +} from "react-hook-form"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; -const Form = FormProvider +const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath, > = { - name: TName -} + name: TName; +}; const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) + {} as FormFieldContextValue, +); const FormField = < TFieldValues extends FieldValues = FieldValues, @@ -37,21 +37,21 @@ const FormField = < - ) -} + ); +}; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState } = useFormContext() - const formState = useFormState({ name: fieldContext.name }) - const fieldState = getFieldState(fieldContext.name, formState) + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState } = useFormContext(); + const formState = useFormState({ name: fieldContext.name }); + const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error("useFormField should be used within "); } - const { id } = itemContext + const { id } = itemContext; return { id, @@ -60,19 +60,19 @@ const useFormField = () => { formDescriptionId: `${id}-form-item-description`, formMessageId: `${id}-form-item-message`, ...fieldState, - } -} + }; +}; type FormItemContextValue = { - id: string -} + id: string; +}; const FormItemContext = React.createContext( - {} as FormItemContextValue -) + {} as FormItemContextValue, +); function FormItem({ className, ...props }: React.ComponentProps<"div">) { - const id = React.useId() + const id = React.useId(); return ( @@ -82,14 +82,14 @@ function FormItem({ className, ...props }: React.ComponentProps<"div">) { {...props} /> - ) + ); } function FormLabel({ className, ...props }: React.ComponentProps) { - const { error, formItemId } = useFormField() + const { error, formItemId } = useFormField(); return (
- ) + ); } // Commented out mobile behavior to keep sidebar always visible @@ -222,7 +222,7 @@ function Sidebar({ "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" - : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", )} />
@@ -249,7 +249,7 @@ function Sidebar({
- ) + ); } function SidebarTrigger({ @@ -257,7 +257,7 @@ function SidebarTrigger({ onClick, ...props }: React.ComponentProps) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return ( - ) + ); } function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return (

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 literal 226046 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�_A+OZH<{J< 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 literal 0 HcmV?d00001 diff --git a/public/icon.ico b/public/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..567b77b0617dec64e32bbffd17015e870da2aaf8 GIT binary patch literal 361102 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