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 @@
+# 仓库统计
+
+
+ 英文 |
+ 中文
+
+
+
+
+
+
+
+#### 核心技术
+
+[](#)
+[](#)
+[](#)
+[](#)
+[](#)
+[](#)
+[](#)
+[](#)
+
+
+
+
+
+
+
+如果你愿意,可以在这里支持这个项目!\
+[](https://github.com/sponsors/LukeGus)
+
+# 概览
+
+
+
+
+
+
+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。
+
+# 展示
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 你的浏览器不支持 video 标签。
+
+
+
+# 许可证
+
+根据 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 |
+ 中文
+
+



+
#### Top Technologies
+
[](#)
[](#)
[](#)
@@ -29,26 +37,34 @@ If you would like, you can support the project here!\
-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 862e8505..d33a4ae5 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/public/icon.icns b/public/icon.icns
new file mode 100644
index 00000000..8a97f2ae
Binary files /dev/null and b/public/icon.icns differ
diff --git a/public/icon.ico b/public/icon.ico
new file mode 100644
index 00000000..567b77b0
Binary files /dev/null and b/public/icon.ico differ
diff --git a/public/icon.png b/public/icon.png
new file mode 100644
index 00000000..498d37d6
Binary files /dev/null and b/public/icon.png differ
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 00000000..66f1cc01
Binary files /dev/null and b/public/icons/1024x1024.png differ
diff --git a/public/icons/128x128.png b/public/icons/128x128.png
new file mode 100644
index 00000000..166c9543
Binary files /dev/null and b/public/icons/128x128.png differ
diff --git a/public/icons/16x16.png b/public/icons/16x16.png
new file mode 100644
index 00000000..12a5bf98
Binary files /dev/null and b/public/icons/16x16.png differ
diff --git a/public/icons/24x24.png b/public/icons/24x24.png
new file mode 100644
index 00000000..8252e1a8
Binary files /dev/null and b/public/icons/24x24.png differ
diff --git a/public/icons/256x256.png b/public/icons/256x256.png
new file mode 100644
index 00000000..bffdbce7
Binary files /dev/null and b/public/icons/256x256.png differ
diff --git a/public/icons/32x32.png b/public/icons/32x32.png
new file mode 100644
index 00000000..eeb3d34c
Binary files /dev/null and b/public/icons/32x32.png differ
diff --git a/public/icons/48x48.png b/public/icons/48x48.png
new file mode 100644
index 00000000..f7b1cc75
Binary files /dev/null and b/public/icons/48x48.png differ
diff --git a/public/icons/512x512.png b/public/icons/512x512.png
new file mode 100644
index 00000000..2c58b82e
Binary files /dev/null and b/public/icons/512x512.png differ
diff --git a/public/icons/64x64.png b/public/icons/64x64.png
new file mode 100644
index 00000000..13da126a
Binary files /dev/null and b/public/icons/64x64.png differ
diff --git a/public/icons/icon.icns b/public/icons/icon.icns
new file mode 100644
index 00000000..8a97f2ae
Binary files /dev/null and b/public/icons/icon.icns differ
diff --git a/public/icons/icon.ico b/public/icons/icon.ico
new file mode 100644
index 00000000..567b77b0
Binary files /dev/null and b/public/icons/icon.ico differ
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 2894ede1..00000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-import React, {useState, useEffect} from "react"
-import {LeftSidebar} from "@/ui/Navigation/LeftSidebar.tsx"
-import {Homepage} from "@/ui/Homepage/Homepage.tsx"
-import {AppView} from "@/ui/Navigation/AppView.tsx"
-import {HostManager} from "@/ui/Apps/Host Manager/HostManager.tsx"
-import {TabProvider, useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx"
-import {TopNavbar} from "@/ui/Navigation/TopNavbar.tsx";
-import { AdminSettings } from "@/ui/Admin/AdminSettings";
-import { UserProfile } from "@/ui/User/UserProfile.tsx";
-import { Toaster } from "@/components/ui/sonner";
-import { getUserInfo } from "@/ui/main-axios.ts";
-
-function getCookie(name: string) {
- return document.cookie.split('; ').reduce((r, v) => {
- 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 (
- )
+ );
}
function FormControl({ ...props }: React.ComponentProps) {
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
return (
) {
aria-invalid={!!error}
{...props}
/>
- )
+ );
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
- const { formDescriptionId } = useFormField()
+ const { formDescriptionId } = useFormField();
return (
) {
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
- )
+ );
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
- const { error, formMessageId } = useFormField()
- const body = error ? String(error?.message ?? "") : props.children
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
- return null
+ return null;
}
return (
@@ -150,7 +151,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
>
{body}
- )
+ );
}
export {
@@ -162,4 +163,4 @@ export {
FormDescription,
FormMessage,
FormField,
-}
+};
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx
index 03295ca6..b1a060f5 100644
--- a/src/components/ui/input.tsx
+++ b/src/components/ui/input.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 Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
@@ -11,11 +11,11 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Input }
+export { Input };
diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx
index ef7133a7..4ef28a9b 100644
--- a/src/components/ui/label.tsx
+++ b/src/components/ui/label.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Label({
className,
@@ -12,11 +12,11 @@ function Label({
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Label }
+export { Label };
diff --git a/src/components/ui/password-input.tsx b/src/components/ui/password-input.tsx
new file mode 100644
index 00000000..f1cd0066
--- /dev/null
+++ b/src/components/ui/password-input.tsx
@@ -0,0 +1,41 @@
+"use client";
+
+import * as React from "react";
+import { Eye, EyeOff } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+
+interface PasswordInputProps
+ extends React.InputHTMLAttributes {}
+
+export const PasswordInput = React.forwardRef<
+ HTMLInputElement,
+ PasswordInputProps
+>(({ className, ...props }, ref) => {
+ const [showPassword, setShowPassword] = React.useState(false);
+
+ return (
+
+
+ setShowPassword((prev) => !prev)}
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition"
+ aria-label={showPassword ? "Hide password" : "Show password"}
+ >
+ {showPassword ? (
+
+ ) : (
+
+ )}
+
+
+ );
+});
+
+PasswordInput.displayName = "PasswordInput";
diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx
index 6d51b6ce..ef5bfd04 100644
--- a/src/components/ui/popover.tsx
+++ b/src/components/ui/popover.tsx
@@ -1,18 +1,18 @@
-import * as React from "react"
-import * as PopoverPrimitive from "@radix-ui/react-popover"
+import * as React from "react";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Popover({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function PopoverTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function PopoverContent({
@@ -29,18 +29,18 @@ function PopoverContent({
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function PopoverAnchor({
...props
}: React.ComponentProps) {
- return
+ return ;
}
-export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
index 10af7e63..d683666e 100644
--- a/src/components/ui/progress.tsx
+++ b/src/components/ui/progress.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as ProgressPrimitive from "@radix-ui/react-progress"
+import * as React from "react";
+import * as ProgressPrimitive from "@radix-ui/react-progress";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Progress({
className,
@@ -13,7 +13,7 @@ function Progress({
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
- className
+ className,
)}
{...props}
>
@@ -23,7 +23,7 @@ function Progress({
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
- )
+ );
}
-export { Progress }
+export { Progress };
diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx
index dd5a6bad..7909c556 100644
--- a/src/components/ui/resizable.tsx
+++ b/src/components/ui/resizable.tsx
@@ -1,8 +1,8 @@
-import * as React from "react"
-import { GripVerticalIcon } from "lucide-react"
-import * as ResizablePrimitive from "react-resizable-panels"
+import * as React from "react";
+import { GripVerticalIcon } from "lucide-react";
+import * as ResizablePrimitive from "react-resizable-panels";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
@@ -13,17 +13,17 @@ function ResizablePanelGroup({
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function ResizablePanel({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function ResizableHandle({
@@ -31,24 +31,24 @@ function ResizableHandle({
className,
...props
}: React.ComponentProps & {
- withHandle?: boolean
+ withHandle?: boolean;
}) {
return (
div]:rotate-90 bg-[#434345] hover:bg-[#2a2a2c] active:bg-[#1a1a1c] transition-colors duration-150",
- className
+ "relative flex w-1 items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-1 data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90 bg-dark-border-hover hover:bg-dark-active active:bg-dark-pressed transition-colors duration-150",
+ className,
)}
{...props}
>
{withHandle && (
-
+
)}
- )
+ );
}
-export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
index 9376f594..51ecedcd 100644
--- a/src/components/ui/scroll-area.tsx
+++ b/src/components/ui/scroll-area.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+import * as React from "react";
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function ScrollArea({
className,
@@ -23,7 +23,7 @@ function ScrollArea({
- )
+ );
}
function ScrollBar({
@@ -41,7 +41,7 @@ function ScrollBar({
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
- className
+ className,
)}
{...props}
>
@@ -50,7 +50,7 @@ function ScrollBar({
className="bg-border relative flex-1 rounded-full"
/>
- )
+ );
}
-export { ScrollArea, ScrollBar }
+export { ScrollArea, ScrollBar };
diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx
index 51f466ec..0c883e37 100644
--- a/src/components/ui/select.tsx
+++ b/src/components/ui/select.tsx
@@ -1,25 +1,25 @@
-import * as React from "react"
-import * as SelectPrimitive from "@radix-ui/react-select"
-import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+import * as React from "react";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps
) {
- return
+ return ;
}
function SelectGroup({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SelectValue({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SelectTrigger({
@@ -28,7 +28,7 @@ function SelectTrigger({
children,
...props
}: React.ComponentProps & {
- size?: "sm" | "default"
+ size?: "sm" | "default";
}) {
return (
@@ -45,7 +45,7 @@ function SelectTrigger({
- )
+ );
}
function SelectContent({
@@ -62,7 +62,7 @@ function SelectContent({
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
- className
+ className,
)}
position={position}
{...props}
@@ -72,7 +72,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
)}
>
{children}
@@ -80,7 +80,7 @@ function SelectContent({
- )
+ );
}
function SelectLabel({
@@ -93,7 +93,7 @@ function SelectLabel({
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
- )
+ );
}
function SelectItem({
@@ -106,7 +106,7 @@ function SelectItem({
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
- className
+ className,
)}
{...props}
>
@@ -117,7 +117,7 @@ function SelectItem({
{children}
- )
+ );
}
function SelectSeparator({
@@ -130,7 +130,7 @@ function SelectSeparator({
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
- )
+ );
}
function SelectScrollUpButton({
@@ -142,13 +142,13 @@ function SelectScrollUpButton({
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
- className
+ className,
)}
{...props}
>
- )
+ );
}
function SelectScrollDownButton({
@@ -160,13 +160,13 @@ function SelectScrollDownButton({
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
- className
+ className,
)}
{...props}
>
- )
+ );
}
export {
@@ -180,4 +180,4 @@ export {
SelectSeparator,
SelectTrigger,
SelectValue,
-}
+};
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 275381ca..72c18e33 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -1,9 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as SeparatorPrimitive from "@radix-ui/react-separator"
+import * as React from "react";
+import * as SeparatorPrimitive from "@radix-ui/react-separator";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Separator({
className,
@@ -18,11 +18,11 @@ function Separator({
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
-export { Separator }
+export { Separator };
diff --git a/src/components/ui/shadcn-io/status/index.tsx b/src/components/ui/shadcn-io/status/index.tsx
index 29b6c6ec..7139942b 100644
--- a/src/components/ui/shadcn-io/status/index.tsx
+++ b/src/components/ui/shadcn-io/status/index.tsx
@@ -1,15 +1,15 @@
-import type { ComponentProps, HTMLAttributes } from 'react';
-import { Badge } from '@/components/ui/badge';
-import { cn } from '@/lib/utils';
-import { useTranslation } from 'react-i18next';
+import type { ComponentProps, HTMLAttributes } from "react";
+import { Badge } from "@/components/ui/badge";
+import { cn } from "@/lib/utils";
+import { useTranslation } from "react-i18next";
export type StatusProps = ComponentProps & {
- status: 'online' | 'offline' | 'maintenance' | 'degraded';
+ status: "online" | "offline" | "maintenance" | "degraded";
};
export const Status = ({ className, status, ...props }: StatusProps) => (
@@ -24,20 +24,20 @@ export const StatusIndicator = ({
@@ -52,13 +52,21 @@ export const StatusLabel = ({
}: StatusLabelProps) => {
const { t } = useTranslation();
return (
-
+
{children ?? (
<>
- {t('common.online')}
- {t('common.offline')}
- {t('common.maintenance')}
- {t('common.degraded')}
+
+ {t("common.online")}
+
+
+ {t("common.offline")}
+
+
+ {t("common.maintenance")}
+
+
+ {t("common.degraded")}
+
>
)}
diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx
index 1a6ea1e2..a227f7ac 100644
--- a/src/components/ui/sheet.tsx
+++ b/src/components/ui/sheet.tsx
@@ -1,29 +1,29 @@
-import * as React from "react"
-import * as SheetPrimitive from "@radix-ui/react-dialog"
-import { XIcon } from "lucide-react"
+import * as React from "react";
+import * as SheetPrimitive from "@radix-ui/react-dialog";
+import { XIcon } from "lucide-react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps) {
- return
+ return ;
}
function SheetTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetClose({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetPortal({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function SheetOverlay({
@@ -35,11 +35,11 @@ function SheetOverlay({
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:pointer-events-none",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SheetContent({
@@ -48,7 +48,7 @@ function SheetContent({
side = "right",
...props
}: React.ComponentProps & {
- side?: "top" | "right" | "bottom" | "left"
+ side?: "top" | "right" | "bottom" | "left";
}) {
return (
@@ -58,14 +58,14 @@ function SheetContent({
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=closed]:pointer-events-none",
side === "right" &&
- "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
+ "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l",
side === "left" &&
- "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
+ "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
- className
+ className,
)}
{...props}
>
@@ -76,7 +76,7 @@ function SheetContent({
- )
+ );
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -86,7 +86,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
- )
+ );
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -96,7 +96,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
- )
+ );
}
function SheetTitle({
@@ -109,7 +109,7 @@ function SheetTitle({
className={cn("text-foreground font-semibold", className)}
{...props}
/>
- )
+ );
}
function SheetDescription({
@@ -122,7 +122,7 @@ function SheetDescription({
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
- )
+ );
}
export {
@@ -134,4 +134,4 @@ export {
SheetFooter,
SheetTitle,
SheetDescription,
-}
+};
diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx
index 291c11b2..dfe0a727 100644
--- a/src/components/ui/sidebar.tsx
+++ b/src/components/ui/sidebar.tsx
@@ -1,54 +1,54 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, VariantProps } from "class-variance-authority"
-import { PanelLeftIcon } from "lucide-react"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import { PanelLeftIcon } from "lucide-react";
-import { useIsMobile } from "@/hooks/use-mobile"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Separator } from "@/components/ui/separator"
+import { useIsMobile } from "@/hooks/use-mobile";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
-} from "@/components/ui/sheet"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "@/components/ui/sheet";
+import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
-} from "@/components/ui/tooltip"
+} from "@/components/ui/tooltip";
-const SIDEBAR_COOKIE_NAME = "sidebar_state"
-const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
-const SIDEBAR_WIDTH = "16rem"
-const SIDEBAR_WIDTH_MOBILE = "18rem"
-const SIDEBAR_WIDTH_ICON = "3rem"
-const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+const SIDEBAR_COOKIE_NAME = "sidebar_state";
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
+const SIDEBAR_WIDTH = "16rem";
+const SIDEBAR_WIDTH_MOBILE = "18rem";
+const SIDEBAR_WIDTH_ICON = "3rem";
+const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
- state: "expanded" | "collapsed"
- open: boolean
- setOpen: (open: boolean) => void
- openMobile: boolean
- setOpenMobile: (open: boolean) => void
- isMobile: boolean
- toggleSidebar: () => void
-}
+ state: "expanded" | "collapsed";
+ open: boolean;
+ setOpen: (open: boolean) => void;
+ openMobile: boolean;
+ setOpenMobile: (open: boolean) => void;
+ isMobile: boolean;
+ toggleSidebar: () => void;
+};
-const SidebarContext = React.createContext(null)
+const SidebarContext = React.createContext(null);
function useSidebar() {
- const context = React.useContext(SidebarContext)
+ const context = React.useContext(SidebarContext);
if (!context) {
- throw new Error("useSidebar must be used within a SidebarProvider.")
+ throw new Error("useSidebar must be used within a SidebarProvider.");
}
- return context
+ return context;
}
function SidebarProvider({
@@ -60,36 +60,36 @@ function SidebarProvider({
children,
...props
}: React.ComponentProps<"div"> & {
- defaultOpen?: boolean
- open?: boolean
- onOpenChange?: (open: boolean) => void
+ defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChange?: (open: boolean) => void;
}) {
- const isMobile = useIsMobile()
- const [openMobile, setOpenMobile] = React.useState(false)
+ const isMobile = useIsMobile();
+ const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
- const [_open, _setOpen] = React.useState(defaultOpen)
- const open = openProp ?? _open
+ const [_open, _setOpen] = React.useState(defaultOpen);
+ const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
- const openState = typeof value === "function" ? value(open) : value
+ const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
- setOpenProp(openState)
+ setOpenProp(openState);
} else {
- _setOpen(openState)
+ _setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
- document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
- [setOpenProp, open]
- )
+ [setOpenProp, open],
+ );
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
- return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
- }, [isMobile, setOpen, setOpenMobile])
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
+ }, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
@@ -98,18 +98,18 @@ function SidebarProvider({
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
- event.preventDefault()
- toggleSidebar()
+ event.preventDefault();
+ toggleSidebar();
}
- }
+ };
- window.addEventListener("keydown", handleKeyDown)
- return () => window.removeEventListener("keydown", handleKeyDown)
- }, [toggleSidebar])
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
- const state = open ? "expanded" : "collapsed"
+ const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo(
() => ({
@@ -121,8 +121,8 @@ function SidebarProvider({
setOpenMobile,
toggleSidebar,
}),
- [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
- )
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
+ );
return (
@@ -138,7 +138,7 @@ function SidebarProvider({
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
- className
+ className,
)}
{...props}
>
@@ -146,7 +146,7 @@ function SidebarProvider({
- )
+ );
}
function Sidebar({
@@ -157,11 +157,11 @@ function Sidebar({
children,
...props
}: React.ComponentProps<"div"> & {
- side?: "left" | "right"
- variant?: "sidebar" | "floating" | "inset"
- collapsible?: "offcanvas" | "icon" | "none"
+ side?: "left" | "right";
+ variant?: "sidebar" | "floating" | "inset";
+ collapsible?: "offcanvas" | "icon" | "none";
}) {
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
@@ -169,13 +169,13 @@ function Sidebar({
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
- className
+ className,
)}
{...props}
>
{children}
- )
+ );
}
// 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 (
{
- onClick?.(event)
- toggleSidebar()
+ onClick?.(event);
+ toggleSidebar();
}}
{...props}
>
Toggle Sidebar
- )
+ );
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
- const { toggleSidebar } = useSidebar()
+ const { toggleSidebar } = useSidebar();
return (
) {
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
@@ -310,11 +310,11 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarInput({
@@ -328,7 +328,7 @@ function SidebarInput({
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
- )
+ );
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
@@ -339,7 +339,7 @@ function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
- )
+ );
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
@@ -350,7 +350,7 @@ function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
- )
+ );
}
function SidebarSeparator({
@@ -364,7 +364,7 @@ function SidebarSeparator({
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
- )
+ );
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
@@ -374,11 +374,11 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
@@ -389,7 +389,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
- )
+ );
}
function SidebarGroupLabel({
@@ -397,7 +397,7 @@ function SidebarGroupLabel({
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "div"
+ const Comp = asChild ? Slot : "div";
return (
svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarGroupAction({
@@ -418,7 +418,7 @@ function SidebarGroupAction({
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "button"
+ const Comp = asChild ? Slot : "button";
return (
- )
+ );
}
function SidebarGroupContent({
@@ -447,7 +447,7 @@ function SidebarGroupContent({
className={cn("w-full text-sm", className)}
{...props}
/>
- )
+ );
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
@@ -458,7 +458,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
- )
+ );
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
@@ -469,7 +469,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
className={cn("group/menu-item relative", className)}
{...props}
/>
- )
+ );
}
const sidebarMenuButtonVariants = cva(
@@ -491,8 +491,8 @@ const sidebarMenuButtonVariants = cva(
variant: "default",
size: "default",
},
- }
-)
+ },
+);
function SidebarMenuButton({
asChild = false,
@@ -503,12 +503,12 @@ function SidebarMenuButton({
className,
...props
}: React.ComponentProps<"button"> & {
- asChild?: boolean
- isActive?: boolean
- tooltip?: string | React.ComponentProps
+ asChild?: boolean;
+ isActive?: boolean;
+ tooltip?: string | React.ComponentProps;
} & VariantProps) {
- const Comp = asChild ? Slot : "button"
- const { isMobile, state } = useSidebar()
+ const Comp = asChild ? Slot : "button";
+ const { isMobile, state } = useSidebar();
const button = (
- )
+ );
if (!tooltip) {
- return button
+ return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
- }
+ };
}
return (
@@ -541,7 +541,7 @@ function SidebarMenuButton({
{...tooltip}
/>
- )
+ );
}
function SidebarMenuAction({
@@ -550,10 +550,10 @@ function SidebarMenuAction({
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
- asChild?: boolean
- showOnHover?: boolean
+ asChild?: boolean;
+ showOnHover?: boolean;
}) {
- const Comp = asChild ? Slot : "button"
+ const Comp = asChild ? Slot : "button";
return (
- )
+ );
}
function SidebarMenuBadge({
@@ -591,11 +591,11 @@ function SidebarMenuBadge({
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarMenuSkeleton({
@@ -603,12 +603,12 @@ function SidebarMenuSkeleton({
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
- showIcon?: boolean
+ showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
- return `${Math.floor(Math.random() * 40) + 50}%`
- }, [])
+ return `${Math.floor(Math.random() * 40) + 50}%`;
+ }, []);
return (
- )
+ );
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
@@ -644,11 +644,11 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function SidebarMenuSubItem({
@@ -662,7 +662,7 @@ function SidebarMenuSubItem({
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
- )
+ );
}
function SidebarMenuSubButton({
@@ -672,11 +672,11 @@ function SidebarMenuSubButton({
className,
...props
}: React.ComponentProps<"a"> & {
- asChild?: boolean
- size?: "sm" | "md"
- isActive?: boolean
+ asChild?: boolean;
+ size?: "sm" | "md";
+ isActive?: boolean;
}) {
- const Comp = asChild ? Slot : "a"
+ const Comp = asChild ? Slot : "a";
return (
- )
+ );
}
export {
@@ -722,4 +722,4 @@ export {
SidebarSeparator,
SidebarTrigger,
useSidebar,
-}
+};
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
index 32ea0ef7..01689981 100644
--- a/src/components/ui/skeleton.tsx
+++ b/src/components/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
@@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
- )
+ );
}
-export { Skeleton }
+export { Skeleton };
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index cd62aff2..7264637e 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -1,8 +1,8 @@
-import { useTheme } from "next-themes"
-import { Toaster as Sonner, ToasterProps } from "sonner"
+import { useTheme } from "next-themes";
+import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme()
+ const { theme = "system" } = useTheme();
return (
{
}
{...props}
/>
- )
-}
+ );
+};
-export { Toaster }
+export { Toaster };
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
index b0363e3f..a026a47c 100644
--- a/src/components/ui/switch.tsx
+++ b/src/components/ui/switch.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as SwitchPrimitive from "@radix-ui/react-switch"
+import * as React from "react";
+import * as SwitchPrimitive from "@radix-ui/react-switch";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Switch({
className,
@@ -12,18 +12,18 @@ function Switch({
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
- className
+ className,
)}
{...props}
>
- )
+ );
}
-export { Switch }
+export { Switch };
diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx
index 5513a5cd..2ad27ce8 100644
--- a/src/components/ui/table.tsx
+++ b/src/components/ui/table.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 Table({ className, ...props }: React.ComponentProps<"table">) {
return (
@@ -14,7 +14,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
{...props}
/>
- )
+ );
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
@@ -24,7 +24,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
className={cn("[&_tr]:border-b", className)}
{...props}
/>
- )
+ );
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
@@ -34,7 +34,7 @@ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
- )
+ );
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
@@ -43,11 +43,11 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
@@ -56,11 +56,11 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
@@ -69,11 +69,11 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
@@ -82,11 +82,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TableCaption({
@@ -99,7 +99,7 @@ function TableCaption({
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
- )
+ );
}
export {
@@ -111,4 +111,4 @@ export {
TableRow,
TableCell,
TableCaption,
-}
+};
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
index 3d6f3acf..46295c2f 100644
--- a/src/components/ui/tabs.tsx
+++ b/src/components/ui/tabs.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import * as TabsPrimitive from "@radix-ui/react-tabs"
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Tabs({
className,
@@ -13,7 +13,7 @@ function Tabs({
className={cn("flex flex-col gap-2", className)}
{...props}
/>
- )
+ );
}
function TabsList({
@@ -25,11 +25,11 @@ function TabsList({
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TabsTrigger({
@@ -41,11 +41,11 @@ function TabsTrigger({
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
- className
+ className,
)}
{...props}
/>
- )
+ );
}
function TabsContent({
@@ -58,7 +58,7 @@ function TabsContent({
className={cn("flex-1 outline-none", className)}
{...props}
/>
- )
+ );
}
-export { Tabs, TabsList, TabsTrigger, TabsContent }
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 00000000..e306ca0a
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+
+import { cn } from "../../lib/utils";
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };
diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx
index 750c93cd..6d34a2b5 100644
--- a/src/components/ui/tooltip.tsx
+++ b/src/components/ui/tooltip.tsx
@@ -1,9 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+import * as React from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
@@ -15,7 +15,7 @@ function TooltipProvider({
delayDuration={delayDuration}
{...props}
/>
- )
+ );
}
function Tooltip({
@@ -25,13 +25,13 @@ function Tooltip({
- )
+ );
}
function TooltipTrigger({
...props
}: React.ComponentProps) {
- return
+ return ;
}
function TooltipContent({
@@ -47,14 +47,14 @@ function TooltipContent({
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
- className
+ className,
)}
{...props}
>
{children}
- )
+ );
}
-export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts
new file mode 100644
index 00000000..18037d8f
--- /dev/null
+++ b/src/hooks/use-confirmation.ts
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import { toast } from "sonner";
+
+interface ConfirmationOptions {
+ title: string;
+ description: string;
+ confirmText?: string;
+ cancelText?: string;
+ variant?: "default" | "destructive";
+}
+
+export function useConfirmation() {
+ const [isOpen, setIsOpen] = useState(false);
+ const [options, setOptions] = useState(null);
+ const [onConfirm, setOnConfirm] = useState<(() => void) | null>(null);
+
+ const confirm = (opts: ConfirmationOptions, callback: () => void) => {
+ setOptions(opts);
+ setOnConfirm(() => callback);
+ setIsOpen(true);
+ };
+
+ const handleConfirm = () => {
+ if (onConfirm) {
+ onConfirm();
+ }
+ setIsOpen(false);
+ setOptions(null);
+ setOnConfirm(null);
+ };
+
+ const handleCancel = () => {
+ setIsOpen(false);
+ setOptions(null);
+ setOnConfirm(null);
+ };
+
+ const confirmWithToast = (
+ message: string,
+ callback: () => void,
+ variant: "default" | "destructive" = "default",
+ ) => {
+ const actionText = variant === "destructive" ? "Delete" : "Confirm";
+ const cancelText = "Cancel";
+
+ toast(message, {
+ action: {
+ label: actionText,
+ onClick: callback,
+ },
+ cancel: {
+ label: cancelText,
+ onClick: () => {},
+ },
+ duration: 10000,
+ className: variant === "destructive" ? "border-red-500" : "",
+ });
+ };
+
+ return {
+ isOpen,
+ options,
+ confirm,
+ handleConfirm,
+ handleCancel,
+ confirmWithToast,
+ };
+}
diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts
index 2b0fe1df..a93d5839 100644
--- a/src/hooks/use-mobile.ts
+++ b/src/hooks/use-mobile.ts
@@ -1,19 +1,21 @@
-import * as React from "react"
+import * as React from "react";
-const MOBILE_BREAKPOINT = 768
+const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
- const [isMobile, setIsMobile] = React.useState(undefined)
+ const [isMobile, setIsMobile] = React.useState(
+ undefined,
+ );
React.useEffect(() => {
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- }
- mql.addEventListener("change", onChange)
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener("change", onChange)
- }, [])
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
- return !!isMobile
+ return !!isMobile;
}
diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts
index e5a54b96..884d4e43 100644
--- a/src/i18n/i18n.ts
+++ b/src/i18n/i18n.ts
@@ -1,40 +1,42 @@
-// i18n configuration for multi-language support
-import i18n from 'i18next';
-import { initReactI18next } from 'react-i18next';
-import LanguageDetector from 'i18next-browser-languagedetector';
-import HttpApi from 'i18next-http-backend';
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import LanguageDetector from "i18next-browser-languagedetector";
+
+import enTranslation from "../locales/en/translation.json";
+import zhTranslation from "../locales/zh/translation.json";
-// Initialize i18n
i18n
- .use(HttpApi) // Load translations using http
- .use(LanguageDetector) // Detect user language
- .use(initReactI18next) // Pass i18n instance to react-i18next
+ .use(LanguageDetector)
+ .use(initReactI18next)
.init({
- supportedLngs: ['en', 'zh'], // Supported languages
- fallbackLng: 'en', // Fallback language
+ supportedLngs: ["en", "zh"],
+ fallbackLng: "en",
debug: false,
-
- // Detection options - disabled to always use English by default
+
detection: {
- order: ['localStorage', 'cookie'], // Only check user's saved preference
- caches: ['localStorage', 'cookie'],
- lookupLocalStorage: 'i18nextLng',
- lookupCookie: 'i18nextLng',
+ order: ["localStorage", "cookie"],
+ caches: ["localStorage", "cookie"],
+ lookupLocalStorage: "i18nextLng",
+ lookupCookie: "i18nextLng",
checkWhitelist: true,
},
-
- // Backend options
- backend: {
- loadPath: '/locales/{{lng}}/translation.json',
+
+ resources: {
+ en: {
+ translation: enTranslation,
+ },
+ zh: {
+ translation: zhTranslation,
+ },
},
-
+
interpolation: {
- escapeValue: false, // React already escapes values
+ escapeValue: false,
},
-
+
react: {
- useSuspense: false, // Disable suspense for SSR compatibility
+ useSuspense: false,
},
});
-export default i18n;
\ No newline at end of file
+export default i18n;
diff --git a/src/index.css b/src/index.css
index 89185ec7..f463abb8 100644
--- a/src/index.css
+++ b/src/index.css
@@ -4,153 +4,180 @@
@custom-variant dark (&:is(.dark *));
:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #09090b;
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #09090b;
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- --radius: 0.625rem;
- --background: oklch(1 0 0);
- --foreground: oklch(0.141 0.005 285.823);
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.141 0.005 285.823);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.141 0.005 285.823);
- --primary: oklch(0.21 0.006 285.885);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.967 0.001 286.375);
- --secondary-foreground: oklch(0.21 0.006 285.885);
- --muted: oklch(0.967 0.001 286.375);
- --muted-foreground: oklch(0.552 0.016 285.938);
- --accent: oklch(0.967 0.001 286.375);
- --accent-foreground: oklch(0.21 0.006 285.885);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.92 0.004 286.32);
- --input: oklch(0.92 0.004 286.32);
- --ring: oklch(0.705 0.015 286.067);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.141 0.005 285.823);
- --sidebar-primary: oklch(0.21 0.006 285.885);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.967 0.001 286.375);
- --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
- --sidebar-border: oklch(0.92 0.004 286.32);
- --sidebar-ring: oklch(0.705 0.015 286.067);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.141 0.005 285.823);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.141 0.005 285.823);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.141 0.005 285.823);
+ --primary: oklch(0.21 0.006 285.885);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.967 0.001 286.375);
+ --secondary-foreground: oklch(0.21 0.006 285.885);
+ --muted: oklch(0.967 0.001 286.375);
+ --muted-foreground: oklch(0.552 0.016 285.938);
+ --accent: oklch(0.967 0.001 286.375);
+ --accent-foreground: oklch(0.21 0.006 285.885);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.92 0.004 286.32);
+ --input: oklch(0.92 0.004 286.32);
+ --ring: oklch(0.705 0.015 286.067);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
+ --sidebar-primary: oklch(0.21 0.006 285.885);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.967 0.001 286.375);
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
+ --sidebar-border: oklch(0.92 0.004 286.32);
+ --sidebar-ring: oklch(0.705 0.015 286.067);
}
@theme inline {
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --color-card: var(--card);
- --color-card-foreground: var(--card-foreground);
- --color-popover: var(--popover);
- --color-popover-foreground: var(--popover-foreground);
- --color-primary: var(--primary);
- --color-primary-foreground: var(--primary-foreground);
- --color-secondary: var(--secondary);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-muted: var(--muted);
- --color-muted-foreground: var(--muted-foreground);
- --color-accent: var(--accent);
- --color-accent-foreground: var(--accent-foreground);
- --color-destructive: var(--destructive);
- --color-border: var(--border);
- --color-input: var(--input);
- --color-ring: var(--ring);
- --color-chart-1: var(--chart-1);
- --color-chart-2: var(--chart-2);
- --color-chart-3: var(--chart-3);
- --color-chart-4: var(--chart-4);
- --color-chart-5: var(--chart-5);
- --color-sidebar: var(--sidebar);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-ring: var(--sidebar-ring);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+
+ --color-dark-bg: #18181b;
+ --color-dark-bg-darker: #0e0e10;
+ --color-dark-bg-darkest: #09090b;
+ --color-dark-bg-input: #222225;
+ --color-dark-bg-button: #23232a;
+ --color-dark-bg-active: #1d1d1f;
+ --color-dark-bg-header: #131316;
+ --color-dark-border: #303032;
+ --color-dark-border-active: #2d2d30;
+ --color-dark-border-hover: #434345;
+ --color-dark-hover: #2d2d30;
+ --color-dark-active: #2a2a2c;
+ --color-dark-pressed: #1a1a1c;
+ --color-dark-hover-alt: #2a2a2d;
+ --color-dark-border-light: #5a5a5d;
+ --color-dark-bg-light: #141416;
+ --color-dark-border-medium: #373739;
+ --color-dark-bg-very-light: #101014;
+ --color-dark-bg-panel: #1b1b1e;
+ --color-dark-border-panel: #222224;
+ --color-dark-bg-panel-hover: #232327;
}
.dark {
- --background: oklch(0.141 0.005 285.823);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.21 0.006 285.885);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.21 0.006 285.885);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.92 0.004 286.32);
- --primary-foreground: oklch(0.21 0.006 285.885);
- --secondary: oklch(0.274 0.006 286.033);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.274 0.006 286.033);
- --muted-foreground: oklch(0.705 0.015 286.067);
- --accent: oklch(0.274 0.006 286.033);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.552 0.016 285.938);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.21 0.006 285.885);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.274 0.006 286.033);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.552 0.016 285.938);
+ --background: oklch(0.141 0.005 285.823);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.21 0.006 285.885);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.21 0.006 285.885);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.92 0.004 286.32);
+ --primary-foreground: oklch(0.21 0.006 285.885);
+ --secondary: oklch(0.274 0.006 286.033);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.274 0.006 286.033);
+ --muted-foreground: oklch(0.705 0.015 286.067);
+ --accent: oklch(0.274 0.006 286.033);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.552 0.016 285.938);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.21 0.006 285.885);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.274 0.006 286.033);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.552 0.016 285.938);
}
@layer base {
- * {
- @apply border-border outline-ring/50;
- }
+ html,
+ body {
+ height: 100%;
+ }
- body {
- @apply bg-background text-foreground;
- }
+ * {
+ @apply border-border outline-ring/50;
+ }
+
+ body {
+ @apply bg-background text-foreground;
+ }
}
.thin-scrollbar {
- scrollbar-width: thin;
- scrollbar-color: #303032 transparent;
+ scrollbar-width: thin;
+ scrollbar-color: #303032 transparent;
}
.thin-scrollbar::-webkit-scrollbar {
- height: 6px;
- width: 6px;
+ height: 6px;
+ width: 6px;
}
.thin-scrollbar::-webkit-scrollbar-track {
- background: transparent;
+ background: transparent;
}
.thin-scrollbar::-webkit-scrollbar-thumb {
- background-color: #303032;
- border-radius: 9999px;
- border: 2px solid transparent;
- background-clip: content-box;
+ background-color: #303032;
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ background-clip: content-box;
}
.thin-scrollbar::-webkit-scrollbar {
@@ -174,4 +201,4 @@
.thin-scrollbar {
scrollbar-width: thin;
scrollbar-color: #434345 #18181b;
-}
\ No newline at end of file
+}
diff --git a/src/lib/frontend-logger.ts b/src/lib/frontend-logger.ts
new file mode 100644
index 00000000..b0558de7
--- /dev/null
+++ b/src/lib/frontend-logger.ts
@@ -0,0 +1,388 @@
+export type LogLevel = "debug" | "info" | "warn" | "error" | "success";
+
+export interface LogContext {
+ operation?: string;
+ userId?: string;
+ hostId?: number;
+ tunnelName?: string;
+ sessionId?: string;
+ requestId?: string;
+ duration?: number;
+ method?: string;
+ url?: string;
+ status?: number;
+ statusText?: string;
+ responseTime?: number;
+ retryCount?: number;
+ errorCode?: string;
+ errorMessage?: string;
+
+ [key: string]: any;
+}
+
+class FrontendLogger {
+ private serviceName: string;
+ private serviceIcon: string;
+ private serviceColor: string;
+ private isDevelopment: boolean;
+
+ constructor(serviceName: string, serviceIcon: string, serviceColor: string) {
+ this.serviceName = serviceName;
+ this.serviceIcon = serviceIcon;
+ this.serviceColor = serviceColor;
+ this.isDevelopment = process.env.NODE_ENV === "development";
+ }
+
+ private getTimeStamp(): string {
+ const now = new Date();
+ return `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, "0")}]`;
+ }
+
+ private formatMessage(
+ level: LogLevel,
+ message: string,
+ context?: LogContext,
+ ): string {
+ const timestamp = this.getTimeStamp();
+ const levelTag = this.getLevelTag(level);
+ const serviceTag = this.getServiceTag();
+
+ let contextStr = "";
+ if (context && this.isDevelopment) {
+ const contextParts = [];
+ if (context.operation) contextParts.push(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.responseTime) contextParts.push(`${context.responseTime}ms`);
+ if (context.status) contextParts.push(`status:${context.status}`);
+ if (context.errorCode) contextParts.push(`code:${context.errorCode}`);
+
+ if (contextParts.length > 0) {
+ contextStr = ` (${contextParts.join(", ")})`;
+ }
+ }
+
+ return `${timestamp} ${levelTag} ${serviceTag} ${message}${contextStr}`;
+ }
+
+ private getLevelTag(level: LogLevel): string {
+ const symbols = {
+ debug: "🔍",
+ info: "ℹ️",
+ warn: "⚠️",
+ error: "❌",
+ success: "✅",
+ };
+ return `${symbols[level]} [${level.toUpperCase()}]`;
+ }
+
+ private getServiceTag(): string {
+ return `${this.serviceIcon} [${this.serviceName}]`;
+ }
+
+ private shouldLog(level: LogLevel): boolean {
+ if (level === "debug" && !this.isDevelopment) {
+ return false;
+ }
+ return true;
+ }
+
+ private log(
+ level: LogLevel,
+ message: string,
+ context?: LogContext,
+ error?: unknown,
+ ): void {
+ if (!this.shouldLog(level)) return;
+
+ const formattedMessage = this.formatMessage(level, message, context);
+
+ switch (level) {
+ case "debug":
+ console.debug(formattedMessage);
+ break;
+ case "info":
+ console.log(formattedMessage);
+ break;
+ case "warn":
+ console.warn(formattedMessage);
+ break;
+ case "error":
+ console.error(formattedMessage);
+ if (error) {
+ console.error("Error details:", error);
+ }
+ break;
+ case "success":
+ console.log(formattedMessage);
+ break;
+ }
+ }
+
+ debug(message: string, context?: LogContext): void {
+ this.log("debug", message, context);
+ }
+
+ info(message: string, context?: LogContext): void {
+ this.log("info", message, context);
+ }
+
+ warn(message: string, context?: LogContext): void {
+ this.log("warn", message, context);
+ }
+
+ error(message: string, error?: unknown, context?: LogContext): void {
+ this.log("error", message, context, error);
+ }
+
+ success(message: string, context?: LogContext): void {
+ this.log("success", message, context);
+ }
+
+ 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" });
+ }
+
+ auth(message: string, context?: LogContext): void {
+ this.info(`AUTH: ${message}`, { ...context, operation: "auth" });
+ }
+
+ 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" });
+ }
+
+ 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" });
+ }
+
+ performance(message: string, context?: LogContext): void {
+ this.info(`PERFORMANCE: ${message}`, {
+ ...context,
+ operation: "performance",
+ });
+ }
+
+ security(message: string, context?: LogContext): void {
+ this.warn(`SECURITY: ${message}`, { ...context, operation: "security" });
+ }
+
+ requestStart(method: string, url: string, context?: LogContext): void {
+ const cleanUrl = this.sanitizeUrl(url);
+ const shortUrl = this.getShortUrl(cleanUrl);
+
+ console.group(`🚀 ${method.toUpperCase()} ${shortUrl}`);
+ this.request(`→ Starting request to ${cleanUrl}`, {
+ ...context,
+ method: method.toUpperCase(),
+ url: cleanUrl,
+ });
+ }
+
+ requestSuccess(
+ method: string,
+ url: string,
+ status: number,
+ responseTime: number,
+ context?: LogContext,
+ ): void {
+ const cleanUrl = this.sanitizeUrl(url);
+ const shortUrl = this.getShortUrl(cleanUrl);
+ const statusIcon = this.getStatusIcon(status);
+ const performanceIcon = this.getPerformanceIcon(responseTime);
+
+ this.response(
+ `← ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`,
+ {
+ ...context,
+ method: method.toUpperCase(),
+ url: cleanUrl,
+ status,
+ responseTime,
+ },
+ );
+ console.groupEnd();
+ }
+
+ requestError(
+ method: string,
+ url: string,
+ status: number,
+ errorMessage: string,
+ responseTime?: number,
+ context?: LogContext,
+ ): void {
+ const cleanUrl = this.sanitizeUrl(url);
+ const shortUrl = this.getShortUrl(cleanUrl);
+ const statusIcon = this.getStatusIcon(status);
+
+ this.error(`← ${statusIcon} ${status} ${errorMessage}`, undefined, {
+ ...context,
+ method: method.toUpperCase(),
+ url: cleanUrl,
+ status,
+ errorMessage,
+ responseTime,
+ });
+ console.groupEnd();
+ }
+
+ networkError(
+ method: string,
+ url: string,
+ errorMessage: string,
+ context?: LogContext,
+ ): void {
+ const cleanUrl = this.sanitizeUrl(url);
+ const shortUrl = this.getShortUrl(cleanUrl);
+
+ this.error(`🌐 Network Error: ${errorMessage}`, undefined, {
+ ...context,
+ method: method.toUpperCase(),
+ url: cleanUrl,
+ errorMessage,
+ errorCode: "NETWORK_ERROR",
+ });
+ console.groupEnd();
+ }
+
+ authError(method: string, url: string, context?: LogContext): void {
+ const cleanUrl = this.sanitizeUrl(url);
+ const shortUrl = this.getShortUrl(cleanUrl);
+
+ this.security(`🔐 Authentication Required`, {
+ ...context,
+ method: method.toUpperCase(),
+ url: cleanUrl,
+ errorCode: "AUTH_REQUIRED",
+ });
+ console.groupEnd();
+ }
+
+ retryAttempt(
+ method: string,
+ url: string,
+ attempt: number,
+ maxAttempts: number,
+ context?: LogContext,
+ ): void {
+ const cleanUrl = this.sanitizeUrl(url);
+ const shortUrl = this.getShortUrl(cleanUrl);
+
+ this.retry(`🔄 Retry ${attempt}/${maxAttempts}`, {
+ ...context,
+ method: method.toUpperCase(),
+ url: cleanUrl,
+ retryCount: attempt,
+ });
+ }
+
+ apiOperation(operation: string, details: string, context?: LogContext): void {
+ this.info(`🔧 ${operation}: ${details}`, {
+ ...context,
+ operation: "api_operation",
+ });
+ }
+
+ requestSummary(
+ method: string,
+ url: string,
+ status: number,
+ responseTime: number,
+ context?: LogContext,
+ ): void {
+ const cleanUrl = this.sanitizeUrl(url);
+ const shortUrl = this.getShortUrl(cleanUrl);
+ const statusIcon = this.getStatusIcon(status);
+ const performanceIcon = this.getPerformanceIcon(responseTime);
+
+ console.log(
+ `%c📊 ${method} ${shortUrl} ${statusIcon} ${status} ${performanceIcon} ${responseTime}ms`,
+ "color: #666; font-style: italic; font-size: 0.9em;",
+ context,
+ );
+ }
+
+ private getShortUrl(url: string): string {
+ try {
+ const urlObj = new URL(url);
+ const path = urlObj.pathname;
+ const query = urlObj.search;
+ return `${urlObj.hostname}${path}${query}`;
+ } catch {
+ return url.length > 50 ? url.substring(0, 47) + "..." : url;
+ }
+ }
+
+ private getStatusIcon(status: number): string {
+ if (status >= 200 && status < 300) return "✅";
+ if (status >= 300 && status < 400) return "↩️";
+ if (status >= 400 && status < 500) return "⚠️";
+ if (status >= 500) return "❌";
+ return "❓";
+ }
+
+ private getPerformanceIcon(responseTime: number): string {
+ if (responseTime < 100) return "⚡";
+ if (responseTime < 500) return "🚀";
+ if (responseTime < 1000) return "🏃";
+ if (responseTime < 3000) return "🚶";
+ return "🐌";
+ }
+
+ private sanitizeUrl(url: string): string {
+ try {
+ const urlObj = new URL(url);
+ if (
+ urlObj.searchParams.has("password") ||
+ urlObj.searchParams.has("token")
+ ) {
+ urlObj.search = "";
+ }
+ return urlObj.toString();
+ } catch {
+ return url;
+ }
+ }
+}
+
+export const apiLogger = new FrontendLogger("API", "🌐", "#3b82f6");
+export const authLogger = new FrontendLogger("AUTH", "🔐", "#dc2626");
+export const sshLogger = new FrontendLogger("SSH", "🖥️", "#1e3a8a");
+export const tunnelLogger = new FrontendLogger("TUNNEL", "📡", "#1e3a8a");
+export const fileLogger = new FrontendLogger("FILE", "📁", "#1e3a8a");
+export const statsLogger = new FrontendLogger("STATS", "📊", "#22c55e");
+export const systemLogger = new FrontendLogger("SYSTEM", "🚀", "#1e3a8a");
+
+export const logger = systemLogger;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index bd0c391d..a5ef1935 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}
diff --git a/public/locales/en/translation.json b/src/locales/en/translation.json
similarity index 74%
rename from public/locales/en/translation.json
rename to src/locales/en/translation.json
index d1d55c02..f40340c4 100644
--- a/public/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -1,4 +1,137 @@
{
+ "credentials": {
+ "credentialsViewer": "Credentials Viewer",
+ "manageYourSSHCredentials": "Manage your SSH credentials securely",
+ "addCredential": "Add Credential",
+ "createCredential": "Create Credential",
+ "editCredential": "Edit Credential",
+ "viewCredential": "View Credential",
+ "duplicateCredential": "Duplicate Credential",
+ "deleteCredential": "Delete Credential",
+ "updateCredential": "Update Credential",
+ "credentialName": "Credential Name",
+ "credentialDescription": "Description",
+ "username": "Username",
+ "searchCredentials": "Search credentials...",
+ "selectFolder": "Select Folder",
+ "selectAuthType": "Select Auth Type",
+ "allFolders": "All Folders",
+ "allAuthTypes": "All Auth Types",
+ "uncategorized": "Uncategorized",
+ "totalCredentials": "Total",
+ "keyBased": "Key-based",
+ "passwordBased": "Password-based",
+ "folders": "Folders",
+ "noCredentialsMatchFilters": "No credentials match your filters",
+ "noCredentialsYet": "No credentials created yet",
+ "createFirstCredential": "Create your first credential",
+ "failedToFetchCredentials": "Failed to fetch credentials",
+ "credentialDeletedSuccessfully": "Credential deleted successfully",
+ "failedToDeleteCredential": "Failed to delete credential",
+ "confirmDeleteCredential": "Are you sure you want to delete credential \"{{name}}\"?",
+ "credentialCreatedSuccessfully": "Credential created successfully",
+ "credentialUpdatedSuccessfully": "Credential updated successfully",
+ "failedToSaveCredential": "Failed to save credential",
+ "failedToFetchCredentialDetails": "Failed to fetch credential details",
+ "failedToFetchHostsUsing": "Failed to fetch hosts using this credential",
+ "loadingCredentials": "Loading credentials...",
+ "retry": "Retry",
+ "noCredentials": "No Credentials",
+ "noCredentialsMessage": "You haven't added any credentials yet. Click \"Add Credential\" to get started.",
+ "sshCredentials": "SSH Credentials",
+ "credentialsCount": "{{count}} credentials",
+ "refresh": "Refresh",
+ "passwordRequired": "Password is required",
+ "sshKeyRequired": "SSH key is required",
+ "credentialAddedSuccessfully": "Credential \"{{name}}\" added successfully",
+ "general": "General",
+ "description": "Description",
+ "folder": "Folder",
+ "tags": "Tags",
+ "addTagsSpaceToAdd": "Add tags (press space to add)",
+ "password": "Password",
+ "key": "Key",
+ "sshPrivateKey": "SSH Private Key",
+ "upload": "Upload",
+ "updateKey": "Update Key",
+ "keyPassword": "Key Password (optional)",
+ "keyType": "Key Type",
+ "keyTypeRSA": "RSA",
+ "keyTypeECDSA": "ECDSA",
+ "keyTypeEd25519": "Ed25519",
+ "updateCredential": "Update Credential",
+ "basicInfo": "Basic Info",
+ "authentication": "Authentication",
+ "organization": "Organization",
+ "basicInformation": "Basic Information",
+ "basicInformationDescription": "Enter the basic information for this credential",
+ "authenticationMethod": "Authentication Method",
+ "authenticationMethodDescription": "Choose how you want to authenticate with SSH servers",
+ "organizationDescription": "Organize your credentials with folders and tags",
+ "enterCredentialName": "Enter credential name",
+ "enterCredentialDescription": "Enter description (optional)",
+ "enterUsername": "Enter username",
+ "nameIsRequired": "Credential name is required",
+ "usernameIsRequired": "Username is required",
+ "authenticationType": "Authentication Type",
+ "passwordAuthDescription": "Use password authentication",
+ "sshKeyAuthDescription": "Use SSH key authentication",
+ "passwordIsRequired": "Password is required",
+ "sshKeyIsRequired": "SSH key is required",
+ "sshKeyType": "SSH Key Type",
+ "privateKey": "Private Key",
+ "enterPassword": "Enter password",
+ "enterPrivateKey": "Enter private key",
+ "keyPassphrase": "Key Passphrase",
+ "enterKeyPassphrase": "Enter key passphrase (optional)",
+ "keyPassphraseOptional": "Optional: leave empty if your key has no passphrase",
+ "leaveEmptyToKeepCurrent": "Leave empty to keep current value",
+ "uploadKeyFile": "Upload Key File",
+ "generateKeyPair": "Generate Key Pair",
+ "sshKeyGenerationNotImplemented": "SSH key generation feature coming soon",
+ "connectionTestingNotImplemented": "Connection testing feature coming soon",
+ "testConnection": "Test Connection",
+ "selectOrCreateFolder": "Select or create folder",
+ "noFolder": "No folder",
+ "orCreateNewFolder": "Or create new folder",
+ "addTag": "Add tag",
+ "saving": "Saving...",
+ "overview": "Overview",
+ "security": "Security",
+ "usage": "Usage",
+ "securityDetails": "Security Details",
+ "securityDetailsDescription": "View encrypted credential information",
+ "credentialSecured": "Credential Secured",
+ "credentialSecuredDescription": "All sensitive data is encrypted with AES-256",
+ "passwordAuthentication": "Password Authentication",
+ "keyAuthentication": "Key Authentication",
+ "keyType": "Key Type",
+ "securityReminder": "Security Reminder",
+ "securityReminderText": "Never share your credentials. All data is encrypted at rest.",
+ "hostsUsingCredential": "Hosts Using This Credential",
+ "noHostsUsingCredential": "No hosts are currently using this credential",
+ "timesUsed": "Times Used",
+ "lastUsed": "Last Used",
+ "connectedHosts": "Connected Hosts",
+ "created": "Created",
+ "lastModified": "Last Modified",
+ "usageStatistics": "Usage Statistics",
+ "copiedToClipboard": "{{field}} copied to clipboard",
+ "failedToCopy": "Failed to copy to clipboard",
+ "sshKey": "SSH Key",
+ "createCredentialDescription": "Create a new SSH credential for secure access",
+ "editCredentialDescription": "Update the credential information",
+ "listView": "List",
+ "folderView": "Folders",
+ "unknown": "Unknown",
+ "confirmRemoveFromFolder": "Are you sure you want to remove \"{{name}}\" from folder \"{{folder}}\"? The credential will be moved to \"Uncategorized\".",
+ "removedFromFolder": "Credential \"{{name}}\" removed from folder successfully",
+ "failedToRemoveFromFolder": "Failed to remove credential from folder",
+ "folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully",
+ "failedToRenameFolder": "Failed to rename folder",
+ "movedToFolder": "Credential \"{{name}}\" moved to \"{{folder}}\" successfully",
+ "failedToMoveToFolder": "Failed to move credential to folder"
+ },
"sshTools": {
"title": "SSH Tools",
"closeTools": "Close SSH Tools",
@@ -18,6 +151,24 @@
"failedToLoadAlerts": "Failed to load alerts",
"failedToDismissAlert": "Failed to dismiss alert"
},
+ "serverConfig": {
+ "title": "Server Configuration",
+ "description": "Configure the Termix server URL to connect to your backend services",
+ "serverUrl": "Server URL",
+ "enterServerUrl": "Please enter a server URL",
+ "testConnectionFirst": "Please test the connection first",
+ "connectionSuccess": "Connection successful!",
+ "connectionFailed": "Connection failed",
+ "connectionError": "Connection error occurred",
+ "connected": "Connected",
+ "disconnected": "Disconnected",
+ "configSaved": "Configuration saved successfully",
+ "saveFailed": "Failed to save configuration",
+ "saveError": "Error saving configuration",
+ "saving": "Saving...",
+ "saveConfig": "Save Configuration",
+ "helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:8081 or https://your-server.com)"
+ },
"common": {
"close": "Close",
"online": "Online",
@@ -32,6 +183,7 @@
"loading": "Loading",
"required": "Required",
"optional": "Optional",
+ "clear": "Clear",
"toggleSidebar": "Toggle Sidebar",
"sidebar": "Sidebar",
"home": "Home",
@@ -72,11 +224,13 @@
"register": "Register",
"username": "Username",
"password": "Password",
+ "version": "Version",
"confirmPassword": "Confirm Password",
"back": "Back",
"email": "Email",
"submit": "Submit",
"cancel": "Cancel",
+ "change": "Change",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
@@ -115,26 +269,31 @@
"passwordResetSuccess": "Password reset successfully! You can now log in with your new password.",
"failedToInitiatePasswordReset": "Failed to initiate password reset",
"failedToVerifyResetCode": "Failed to verify reset code",
- "failedToCompletePasswordReset": "Failed to complete password reset"
+ "failedToCompletePasswordReset": "Failed to complete password reset",
+ "documentation": "Documentation"
},
"nav": {
"home": "Home",
"hosts": "Hosts",
+ "credentials": "Credentials",
"terminal": "Terminal",
"tunnels": "Tunnels",
"fileManager": "File Manager",
"serverStats": "Server Stats",
"admin": "Admin",
+ "userProfile": "User Profile",
"tools": "Tools",
"newTab": "New Tab",
"splitScreen": "Split Screen",
"closeTab": "Close Tab",
"sshManager": "SSH Manager",
"hostManager": "Host Manager",
- "cannotSplitTab": "Cannot split this tab"
+ "cannotSplitTab": "Cannot split this tab",
+ "tabNavigation": "Tab Navigation"
},
"admin": {
"title": "Admin Settings",
+ "oidc": "OIDC",
"users": "Users",
"userManagement": "User Management",
"makeAdmin": "Make Admin",
@@ -179,7 +338,12 @@
"allowNewAccountRegistration": "Allow new account registration",
"missingRequiredFields": "Missing required fields: {{fields}}",
"oidcConfigurationUpdated": "OIDC configuration updated successfully!",
+ "failedToFetchOidcConfig": "Failed to fetch OIDC configuration",
+ "failedToFetchRegistrationStatus": "Failed to fetch registration status",
+ "failedToFetchUsers": "Failed to fetch users",
+ "oidcConfigurationDisabled": "OIDC configuration disabled successfully!",
"failedToUpdateOidcConfig": "Failed to update OIDC configuration",
+ "failedToDisableOidcConfig": "Failed to disable OIDC configuration",
"enterUsernameToMakeAdmin": "Enter username to make admin",
"userIsNowAdmin": "User {{username}} is now an admin",
"failedToMakeUserAdmin": "Failed to make user admin",
@@ -207,8 +371,10 @@
"importJsonDesc": "Upload a JSON file to bulk import multiple SSH hosts (max 100).",
"downloadSample": "Download Sample",
"formatGuide": "Format Guide",
+ "exportCredentialWarning": "Warning: Host \"{{name}}\" uses credential authentication. The exported file will not include the credential data and will need to be manually reconfigured after import. Do you want to continue?",
+ "exportSensitiveDataWarning": "Warning: Host \"{{name}}\" contains sensitive authentication data (password/SSH key). The exported file will not include this data for security reasons. You'll need to reconfigure authentication after import. Do you want to continue?",
"uncategorized": "Uncategorized",
- "confirmDelete": "Are you sure you want to delete \"{{name}}\"?",
+ "confirmDelete": "Are you sure you want to delete \"{{name}}\" ?",
"failedToDeleteHost": "Failed to delete host",
"jsonMustContainHosts": "JSON must contain a \"hosts\" array or be an array of hosts",
"noHostsInJson": "No hosts found in JSON file",
@@ -276,6 +442,11 @@
"authentication": "Authentication",
"password": "Password",
"key": "Key",
+ "credential": "Credential",
+ "selectCredential": "Select Credential",
+ "selectCredentialPlaceholder": "Choose a credential...",
+ "credentialRequired": "Credential is required when using credential authentication",
+ "credentialDescription": "Selecting a credential will overwrite the current username and use the credential's authentication details.",
"sshPrivateKey": "SSH Private Key",
"keyPassword": "Key Password",
"keyType": "Key Type",
@@ -288,7 +459,11 @@
"dsa": "DSA",
"rsaSha2256": "RSA SHA2-256",
"rsaSha2512": "RSA SHA2-512",
+ "uploadFile": "Upload File",
+ "pasteKey": "Paste Key",
"updateKey": "Update Key",
+ "existingKey": "Existing Key (click to change)",
+ "existingCredential": "Existing Credential (click to change)",
"addTagsSpaceToAdd": "add tags (space to add)",
"terminalBadge": "Terminal",
"tunnelBadge": "Tunnel",
@@ -297,7 +472,14 @@
"terminal": "Terminal",
"tunnel": "Tunnel",
"fileManager": "File Manager",
- "hostViewer": "Host Viewer"
+ "hostViewer": "Host Viewer",
+ "confirmRemoveFromFolder": "Are you sure you want to remove \"{{name}}\" from folder \"{{folder}}\"? The host will be moved to \"No Folder\".",
+ "removedFromFolder": "Host \"{{name}}\" removed from folder successfully",
+ "failedToRemoveFromFolder": "Failed to remove host from folder",
+ "folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully",
+ "failedToRenameFolder": "Failed to rename folder",
+ "movedToFolder": "Host \"{{name}}\" moved to \"{{folder}}\" successfully",
+ "failedToMoveToFolder": "Failed to move host to folder"
},
"terminal": {
"title": "Terminal",
@@ -314,10 +496,21 @@
"reconnect": "Reconnect",
"sessionEnded": "Session Ended",
"connectionLost": "Connection Lost",
- "error": "ERROR",
+ "error": "ERROR: {{message}}",
"disconnected": "Disconnected",
"connectionClosed": "Connection closed",
- "connectionError": "Connection error"
+ "connectionError": "Connection error: {{message}}",
+ "connected": "Connected",
+ "sshConnected": "SSH connection established",
+ "authError": "Authentication failed: {{message}}",
+ "unknownError": "Unknown error occurred",
+ "messageParseError": "Failed to parse server message",
+ "websocketError": "WebSocket connection error",
+ "connecting": "Connecting...",
+ "reconnecting": "Reconnecting... ({{attempt}}/{{max}})",
+ "reconnected": "Reconnected successfully",
+ "maxReconnectAttemptsReached": "Maximum reconnection attempts reached",
+ "connectionTimeout": "Connection timeout"
},
"fileManager": {
"title": "File Manager",
@@ -337,6 +530,11 @@
"clickToSelectFile": "Click to select a file",
"chooseFile": "Choose File",
"uploading": "Uploading...",
+ "uploadingFile": "Uploading {{name}}...",
+ "creatingFile": "Creating {{name}}...",
+ "creatingFolder": "Creating {{name}}...",
+ "deletingItem": "Deleting {{type}} {{name}}...",
+ "renamingItem": "Renaming {{type}} {{oldName}} to {{newName}}...",
"createNewFile": "Create New File",
"fileName": "File Name",
"creating": "Creating...",
@@ -506,7 +704,18 @@
"memoryUsage": "Memory Usage",
"rootStorageSpace": "Root Storage Space",
"of": "of",
- "feedbackMessage": "Have ideas for what should come next for server management? Share them on"
+ "feedbackMessage": "Have ideas for what should come next for server management? Share them on",
+ "failedToFetchHostConfig": "Failed to fetch host configuration",
+ "failedToFetchStatus": "Failed to fetch server status",
+ "failedToFetchMetrics": "Failed to fetch server metrics",
+ "failedToFetchHomeData": "Failed to fetch home data",
+ "loadingMetrics": "Loading metrics...",
+ "refreshing": "Refreshing...",
+ "serverOffline": "Server Offline",
+ "cannotFetchMetrics": "Cannot fetch metrics from offline server",
+ "load": "Load",
+ "free": "Free",
+ "available": "Available"
},
"auth": {
"loginTitle": "Login to Termix",
@@ -577,6 +786,7 @@
"external": "External",
"loginWithExternal": "Login with External Provider",
"loginWithExternalDesc": "Login using your configured external identity provider",
+ "externalNotSupportedInElectron": "External authentication is not supported in the Electron app yet. Please use the web version for OIDC login.",
"resetPasswordButton": "Reset Password",
"sendResetCode": "Send Reset Code",
"resetCodeDesc": "Enter your username to receive a password reset code. The code will be logged in the docker container logs.",
@@ -608,7 +818,6 @@
"oidcAuthFailed": "OIDC authentication failed",
"noTokenReceived": "No token received from login",
"invalidAuthUrl": "Invalid authorization URL received from backend",
- "connectionTimeout": "Connection timeout",
"invalidInput": "Invalid input",
"requiredField": "This field is required",
"minLength": "Minimum length is {{min}}",
@@ -653,6 +862,9 @@
"external": "External (OIDC)",
"selectPreferredLanguage": "Select your preferred language for the interface"
},
+ "user": {
+ "failedToLoadVersionInfo": "Failed to load version information"
+ },
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
@@ -665,6 +877,10 @@
"folder": "folder",
"password": "password",
"keyPassword": "key password",
+ "pastePrivateKey": "Paste your private key here...",
+ "credentialName": "My SSH Server",
+ "description": "SSH credential description",
+ "searchCredentials": "Search credentials by name, username, or tags...",
"sshConfig": "endpoint ssh configuration",
"homePath": "/home",
"clientId": "your-client-id",
@@ -675,6 +891,7 @@
"userIdField": "sub",
"usernameField": "name",
"scopes": "openid email profile",
+ "userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"enterUsername": "Enter username to make admin",
"searchHosts": "Search hosts by name, username, IP, folder, tags...",
"enterPassword": "Enter your password",
@@ -810,5 +1027,9 @@
"invalidVerificationCode": "Invalid verification code",
"failedToDisableTotp": "Failed to disable TOTP",
"failedToGenerateBackupCodes": "Failed to generate backup codes"
+ },
+ "mobile": {
+ "selectHostToStart": "Select a host to start your terminal session",
+ "limitedSupportMessage": "Mobile support is currently limited. A dedicated mobile app is coming soon to enhance your experience."
}
-}
\ No newline at end of file
+}
diff --git a/public/locales/zh/translation.json b/src/locales/zh/translation.json
similarity index 76%
rename from public/locales/zh/translation.json
rename to src/locales/zh/translation.json
index c8f299dc..370d906f 100644
--- a/public/locales/zh/translation.json
+++ b/src/locales/zh/translation.json
@@ -1,4 +1,136 @@
{
+ "credentials": {
+ "credentialsViewer": "凭证查看器",
+ "credentialsManager": "凭据管理器",
+ "manageYourSSHCredentials": "安全管理您的SSH凭据",
+ "addCredential": "添加凭据",
+ "createCredential": "创建凭据",
+ "editCredential": "编辑凭据",
+ "viewCredential": "查看凭据",
+ "duplicateCredential": "复制凭据",
+ "deleteCredential": "删除凭据",
+ "updateCredential": "更新凭据",
+ "credentialName": "凭据名称",
+ "credentialDescription": "描述",
+ "username": "用户名",
+ "searchCredentials": "搜索凭据...",
+ "selectFolder": "选择文件夹",
+ "selectAuthType": "选择认证类型",
+ "allFolders": "所有文件夹",
+ "allAuthTypes": "所有认证类型",
+ "uncategorized": "未分类",
+ "totalCredentials": "总计",
+ "keyBased": "密钥认证",
+ "passwordBased": "密码认证",
+ "folders": "文件夹",
+ "noCredentialsMatchFilters": "没有符合筛选条件的凭据",
+ "noCredentialsYet": "还未创建凭据",
+ "createFirstCredential": "创建您的第一个凭据",
+ "failedToFetchCredentials": "获取凭据失败",
+ "credentialDeletedSuccessfully": "凭据删除成功",
+ "failedToDeleteCredential": "删除凭据失败",
+ "confirmDeleteCredential": "确定要删除凭据「{{name}}」吗?",
+ "credentialCreatedSuccessfully": "凭据创建成功",
+ "credentialUpdatedSuccessfully": "凭据更新成功",
+ "failedToSaveCredential": "保存凭据失败",
+ "failedToFetchCredentialDetails": "获取凭据详情失败",
+ "failedToFetchHostsUsing": "获取使用此凭据的主机失败",
+ "loadingCredentials": "正在加载凭据...",
+ "retry": "重试",
+ "noCredentials": "暂无凭据",
+ "noCredentialsMessage": "你还没有添加任何凭证。点击“添加凭证”以开始。",
+ "sshCredentials": "SSH凭据",
+ "credentialsCount": "{{count}} 个凭据",
+ "refresh": "刷新",
+ "passwordRequired": "密码为必填项",
+ "sshKeyRequired": "SSH密钥为必填项",
+ "credentialAddedSuccessfully": "凭据「{{name}}」添加成功",
+ "general": "常规",
+ "description": "描述",
+ "folder": "文件夹",
+ "tags": "标签",
+ "addTagsSpaceToAdd": "添加标签(按空格键添加)",
+ "password": "密码",
+ "key": "密钥",
+ "sshPrivateKey": "SSH私钥",
+ "upload": "上传",
+ "updateKey": "更新密钥",
+ "keyPassword": "密钥密码(可选)",
+ "keyType": "密钥类型",
+ "keyTypeRSA": "RSA",
+ "keyTypeECDSA": "ECDSA",
+ "keyTypeEd25519": "Ed25519",
+ "basicInfo": "基本信息",
+ "authentication": "认证方式",
+ "organization": "组织管理",
+ "basicInformation": "基本信息",
+ "basicInformationDescription": "输入此凭据的基本信息",
+ "authenticationMethod": "认证方式",
+ "authenticationMethodDescription": "选择如何与SSH服务器进行认证",
+ "organizationDescription": "使用文件夹和标签来组织您的凭据",
+ "enterCredentialName": "输入凭据名称",
+ "enterCredentialDescription": "输入描述(可选)",
+ "enterUsername": "输入用户名",
+ "nameIsRequired": "凭据名称是必需的",
+ "usernameIsRequired": "用户名是必需的",
+ "authenticationType": "认证类型",
+ "passwordAuthDescription": "使用密码认证",
+ "sshKeyAuthDescription": "使用SSH密钥认证",
+ "passwordIsRequired": "密码是必需的",
+ "sshKeyIsRequired": "SSH密钥是必需的",
+ "sshKeyType": "SSH密钥类型",
+ "privateKey": "私钥",
+ "enterPassword": "输入密码",
+ "enterPrivateKey": "输入私钥",
+ "keyPassphrase": "密钥密码",
+ "enterKeyPassphrase": "输入密钥密码(可选)",
+ "keyPassphraseOptional": "可选:如果您的密钥没有密码,请留空",
+ "leaveEmptyToKeepCurrent": "留空以保持当前值",
+ "uploadKeyFile": "上传密钥文件",
+ "generateKeyPair": "生成密钥对",
+ "sshKeyGenerationNotImplemented": "SSH密钥生成功能即将推出",
+ "connectionTestingNotImplemented": "连接测试功能即将推出",
+ "testConnection": "测试连接",
+ "selectOrCreateFolder": "选择或创建文件夹",
+ "noFolder": "无文件夹",
+ "orCreateNewFolder": "或创建新文件夹",
+ "addTag": "添加标签",
+ "saving": "保存中...",
+ "overview": "概览",
+ "security": "安全",
+ "usage": "使用情况",
+ "securityDetails": "安全详情",
+ "securityDetailsDescription": "查看加密的凭据信息",
+ "credentialSecured": "凭据已加密",
+ "credentialSecuredDescription": "所有敏感数据均使用AES-256加密",
+ "passwordAuthentication": "密码认证",
+ "keyAuthentication": "密钥认证",
+ "securityReminder": "安全提醒",
+ "securityReminderText": "请勿分享您的凭据。所有数据均已静态加密。",
+ "hostsUsingCredential": "使用此凭据的主机",
+ "noHostsUsingCredential": "当前没有主机使用此凭据",
+ "timesUsed": "使用次数",
+ "lastUsed": "最后使用",
+ "connectedHosts": "连接的主机",
+ "created": "创建时间",
+ "lastModified": "最后修改",
+ "usageStatistics": "使用统计",
+ "copiedToClipboard": "{{field}}已复制到剪贴板",
+ "failedToCopy": "复制到剪贴板失败",
+ "sshKey": "SSH密钥",
+ "createCredentialDescription": "创建新的SSH凭据以进行安全访问",
+ "editCredentialDescription": "更新凭据信息",
+ "listView": "列表",
+ "folderView": "文件夹",
+ "unknown": "未知",
+ "confirmRemoveFromFolder": "确定要将\"{{name}}\"从文件夹\"{{folder}}\"中移除吗?凭据将被移动到\"未分类\"。",
+ "removedFromFolder": "凭据\"{{name}}\"已成功从文件夹中移除",
+ "failedToRemoveFromFolder": "从文件夹中移除凭据失败",
+ "folderRenamed": "文件夹\"{{oldName}}\"已成功重命名为\"{{newName}}\"",
+ "failedToRenameFolder": "重命名文件夹失败",
+ "movedToFolder": "凭据\"{{name}}\"已成功移动到\"{{folder}}\"",
+ "failedToMoveToFolder": "移动凭据到文件夹失败"
+ },
"sshTools": {
"title": "SSH 工具",
"closeTools": "关闭 SSH 工具",
@@ -18,6 +150,24 @@
"failedToLoadAlerts": "加载警报失败",
"failedToDismissAlert": "关闭警报失败"
},
+ "serverConfig": {
+ "title": "服务器配置",
+ "description": "配置 Termix 服务器 URL 以连接到您的后端服务",
+ "serverUrl": "服务器 URL",
+ "enterServerUrl": "请输入服务器 URL",
+ "testConnectionFirst": "请先测试连接",
+ "connectionSuccess": "连接成功!",
+ "connectionFailed": "连接失败",
+ "connectionError": "连接发生错误",
+ "connected": "已连接",
+ "disconnected": "未连接",
+ "configSaved": "配置保存成功",
+ "saveFailed": "保存配置失败",
+ "saveError": "保存配置时出错",
+ "saving": "保存中...",
+ "saveConfig": "保存配置",
+ "helpText": "输入您的 Termix 服务器运行地址(例如:http://localhost:8081 或 https://your-server.com)"
+ },
"common": {
"close": "关闭",
"online": "在线",
@@ -29,9 +179,10 @@
"warning": "警告",
"info": "信息",
"success": "成功",
- "loading": "加载中",
+ "loading": "加载中...",
"required": "必填",
"optional": "可选",
+ "clear": "清除",
"toggleSidebar": "切换侧边栏",
"sidebar": "侧边栏",
"home": "首页",
@@ -42,8 +193,7 @@
"updateAvailable": "有可用更新",
"sshPath": "SSH 路径",
"localPath": "本地路径",
- "loading": "加载中...",
- "noAuthCredentials": "此 SSH 主机没有可用的身份验证凭据",
+ "noAuthCredentials": "此 SSH 主机没有可用的认证凭据",
"noReleases": "没有发布版本",
"updatesAndReleases": "更新与发布",
"newVersionAvailable": "有新版本 ({{version}}) 可用。",
@@ -56,13 +206,10 @@
"resetPassword": "重置密码",
"resetCode": "重置代码",
"newPassword": "新密码",
- "sshPath": "SSH 路径",
- "localPath": "本地路径",
"folder": "文件夹",
"file": "文件",
"renamedSuccessfully": "重命名成功",
"deletedSuccessfully": "删除成功",
- "noAuthCredentials": "此 SSH 主机没有可用的认证凭据",
"noTunnelConnections": "没有配置隧道连接",
"sshTools": "SSH 工具",
"english": "英语",
@@ -77,27 +224,21 @@
"email": "邮箱",
"submit": "提交",
"cancel": "取消",
+ "change": "更改",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"add": "添加",
"search": "搜索",
- "loading": "加载中...",
- "error": "错误",
- "success": "成功",
- "warning": "警告",
- "info": "信息",
"confirm": "确认",
"yes": "是",
"no": "否",
"ok": "确定",
- "close": "关闭",
"enabled": "已启用",
"disabled": "已禁用",
"important": "重要",
"notEnabled": "未启用",
"settingUp": "设置中...",
- "back": "返回",
"next": "下一步",
"previous": "上一步",
"refresh": "刷新",
@@ -115,31 +256,36 @@
"passwordResetSuccess": "密码重置成功!您现在可以使用新密码登录。",
"failedToInitiatePasswordReset": "启动密码重置失败",
"failedToVerifyResetCode": "验证重置代码失败",
- "failedToCompletePasswordReset": "完成密码重置失败"
+ "failedToCompletePasswordReset": "完成密码重置失败",
+ "documentation": "文档"
},
"nav": {
"home": "首页",
"hosts": "主机",
+ "credentials": "凭据",
"terminal": "终端",
"tunnels": "隧道",
"fileManager": "文件管理器",
"serverStats": "服务器统计",
"admin": "管理员",
+ "userProfile": "用户资料",
"tools": "工具",
"newTab": "新标签页",
"splitScreen": "分屏",
"closeTab": "关闭标签页",
"sshManager": "SSH 管理器",
"hostManager": "主机管理器",
- "cannotSplitTab": "无法分割此标签页"
+ "cannotSplitTab": "无法分割此标签页",
+ "tabNavigation": "标签导航"
},
"admin": {
"title": "管理员设置",
+ "oidc": "OIDC",
"users": "用户",
"userManagement": "用户管理",
"makeAdmin": "设为管理员",
"removeAdmin": "移除管理员",
- "deleteUser": "删除用户",
+ "deleteUser": "删除用户 {{username}} 吗?此操作无法撤销。",
"allowRegistration": "允许注册",
"oidcSettings": "OIDC 设置",
"clientId": "客户端 ID",
@@ -179,14 +325,18 @@
"allowNewAccountRegistration": "允许新账户注册",
"missingRequiredFields": "缺少必填字段:{{fields}}",
"oidcConfigurationUpdated": "OIDC 配置更新成功!",
+ "failedToFetchOidcConfig": "获取 OIDC 配置失败",
+ "failedToFetchRegistrationStatus": "获取注册状态失败",
+ "failedToFetchUsers": "获取用户列表失败",
+ "oidcConfigurationDisabled": "OIDC 配置禁用成功!",
"failedToUpdateOidcConfig": "更新 OIDC 配置失败",
+ "failedToDisableOidcConfig": "禁用 OIDC 配置失败",
"enterUsernameToMakeAdmin": "输入用户名以设为管理员",
"userIsNowAdmin": "用户 {{username}} 现在是管理员",
"failedToMakeUserAdmin": "设为管理员失败",
"removeAdminStatus": "移除 {{username}} 的管理员权限吗?",
"adminStatusRemoved": "已移除 {{username}} 的管理员权限",
"failedToRemoveAdminStatus": "移除管理员权限失败",
- "deleteUser": "删除用户 {{username}} 吗?此操作无法撤销。",
"userDeletedSuccessfully": "用户 {{username}} 删除成功",
"failedToDeleteUser": "删除用户失败",
"overrideUserInfoUrl": "覆盖用户信息 URL(非必填)"
@@ -207,6 +357,8 @@
"importJsonDesc": "上传 JSON 文件以批量导入多个 SSH 主机(最多 100 个)。",
"downloadSample": "下载示例",
"formatGuide": "格式指南",
+ "exportCredentialWarning": "警告:主机 \"{{name}}\" 使用凭据认证。导出的文件将不包含凭据数据,导入后需要手动重新配置。您确定要继续吗?",
+ "exportSensitiveDataWarning": "警告:主机 \"{{name}}\" 包含敏感认证数据(密码/SSH密钥)。出于安全考虑,导出的文件将不包含此数据。导入后您需要重新配置认证。您确定要继续吗?",
"uncategorized": "未分类",
"confirmDelete": "确定要删除 \"{{name}}\" 吗?",
"failedToDeleteHost": "删除主机失败",
@@ -218,11 +370,12 @@
"importError": "导入错误",
"failedToImportJson": "导入 JSON 文件失败",
"connectionDetails": "连接详情",
- "organization": "组织",
+ "organization": "组织管理",
"ipAddress": "IP 地址",
"port": "端口",
"name": "名称",
"username": "用户名",
+ "hostName": "主机名",
"folder": "文件夹",
"tags": "标签",
"passwordRequired": "使用密码认证时需要密码",
@@ -232,16 +385,11 @@
"addHost": "添加主机",
"editHost": "编辑主机",
"deleteHost": "删除主机",
- "hostName": "主机名",
- "ipAddress": "IP 地址",
- "port": "端口",
"authType": "认证类型",
"passwordAuth": "密码",
"keyAuth": "SSH 密钥",
"keyPassword": "密钥密码",
"keyType": "密钥类型",
- "folder": "文件夹",
- "tags": "标签",
"pin": "固定",
"enableTerminal": "启用终端",
"enableTunnel": "启用隧道",
@@ -255,8 +403,6 @@
"connecting": "连接中...",
"connectionFailed": "连接失败",
"connectionSuccess": "连接成功",
- "connectionDetails": "连接详情",
- "organization": "组织管理",
"addTags": "添加标签(空格添加)",
"sourcePort": "源端口",
"sourcePortDesc": "(源指通用标签页中的当前连接详情)",
@@ -296,9 +442,12 @@
"authentication": "认证方式",
"password": "密码",
"key": "密钥",
+ "credential": "凭证",
+ "selectCredential": "选择凭证",
+ "selectCredentialPlaceholder": "选择一个凭证...",
+ "credentialRequired": "使用凭证认证时需要选择凭证",
+ "credentialDescription": "选择凭证将覆盖当前用户名并使用凭证的认证详细信息。",
"sshPrivateKey": "SSH 私钥",
- "keyPassword": "密钥密码",
- "keyType": "密钥类型",
"maxRetriesDescription": "隧道连接的最大重试次数。",
"retryIntervalDescription": "重试尝试之间的等待时间。",
"otherInstallMethods": "其他安装方法:",
@@ -326,7 +475,11 @@
"dsa": "DSA",
"rsaSha2256": "RSA SHA2-256",
"rsaSha2512": "RSA SHA2-512",
+ "uploadFile": "上传文件",
+ "pasteKey": "粘贴密钥",
"updateKey": "更新密钥",
+ "existingKey": "现有密钥(点击更改)",
+ "existingCredential": "现有凭据(点击更改)",
"addTagsSpaceToAdd": "添加标签(空格添加)",
"terminalBadge": "终端",
"tunnelBadge": "隧道",
@@ -334,7 +487,14 @@
"general": "常规",
"terminal": "终端",
"tunnel": "隧道",
- "fileManager": "文件管理器"
+ "fileManager": "文件管理器",
+ "confirmRemoveFromFolder": "确定要将\"{{name}}\"从文件夹\"{{folder}}\"中移除吗?主机将被移动到\"无文件夹\"。",
+ "removedFromFolder": "主机\"{{name}}\"已成功从文件夹中移除",
+ "failedToRemoveFromFolder": "从文件夹中移除主机失败",
+ "folderRenamed": "文件夹\"{{oldName}}\"已成功重命名为\"{{newName}}\"",
+ "failedToRenameFolder": "重命名文件夹失败",
+ "movedToFolder": "主机\"{{name}}\"已成功移动到\"{{folder}}\"",
+ "failedToMoveToFolder": "移动主机到文件夹失败"
},
"terminal": {
"title": "终端",
@@ -354,7 +514,18 @@
"error": "错误",
"disconnected": "已断开连接",
"connectionClosed": "连接已关闭",
- "connectionError": "连接错误"
+ "connectionError": "连接错误",
+ "connected": "已连接",
+ "sshConnected": "SSH 连接已建立",
+ "authError": "认证失败:{{message}}",
+ "unknownError": "发生未知错误",
+ "messageParseError": "解析服务器消息失败",
+ "websocketError": "WebSocket 连接错误",
+ "connecting": "连接中...",
+ "reconnecting": "重新连接中... ({{attempt}}/{{max}})",
+ "reconnected": "重新连接成功",
+ "maxReconnectAttemptsReached": "已达到最大重连尝试次数",
+ "connectionTimeout": "连接超时"
},
"fileManager": {
"title": "文件管理器",
@@ -374,6 +545,11 @@
"clickToSelectFile": "点击选择文件",
"chooseFile": "选择文件",
"uploading": "上传中...",
+ "uploadingFile": "正在上传 {{name}}...",
+ "creatingFile": "正在创建 {{name}}...",
+ "creatingFolder": "正在创建 {{name}}...",
+ "deletingItem": "正在删除 {{type}} {{name}}...",
+ "renamingItem": "正在重命名 {{type}} {{oldName}} 为 {{newName}}...",
"createNewFile": "创建新文件",
"fileName": "文件名",
"creating": "创建中...",
@@ -401,16 +577,11 @@
"failedToRenameItem": "重命名项目失败",
"upload": "上传",
"download": "下载",
- "newFile": "新建文件",
- "newFolder": "新建文件夹",
- "rename": "重命名",
"delete": "删除",
"permissions": "权限",
"size": "大小",
"modified": "修改时间",
"path": "路径",
- "fileName": "文件名",
- "folderName": "文件夹名",
"confirmDelete": "确定要删除 {{name}} 吗?",
"uploadSuccess": "文件上传成功",
"uploadFailed": "文件上传失败",
@@ -430,10 +601,7 @@
"fileSavedSuccessfully": "文件保存成功",
"saveTimeout": "保存操作超时。文件可能已成功保存,但操作用时过长。请检查 Docker 日志以确认。",
"failedToSaveFile": "保存文件失败",
- "folder": "文件夹",
- "file": "文件",
"deletedSuccessfully": "删除成功",
- "failedToDeleteItem": "删除项目失败",
"connectToServer": "连接到服务器",
"selectServerToEdit": "从侧边栏选择服务器以开始编辑文件",
"fileOperations": "文件操作",
@@ -461,11 +629,11 @@
"tunnels": {
"title": "SSH 隧道",
"noSshTunnels": "没有 SSH 隧道",
- "createFirstTunnelMessage": "您还没有创建任何 SSH 隧道。在主机管理器中配置隧道连接以开始使用。",
+ "createFirstTunnelMessage": "创建您的第一个 SSH 隧道以开始使用。使用 SSH 管理器添加具有隧道连接的主机。",
"connected": "已连接",
- "disconnected": "已断开",
+ "disconnected": "已断开连接",
"connecting": "连接中...",
- "disconnecting": "断开中...",
+ "disconnecting": "断开连接中...",
"unknown": "未知",
"error": "错误",
"failed": "失败",
@@ -501,17 +669,7 @@
"local": "本地",
"remote": "远程",
"dynamic": "动态",
- "noSshTunnels": "没有 SSH 隧道",
- "createFirstTunnelMessage": "创建您的第一个 SSH 隧道以开始使用。使用 SSH 管理器添加具有隧道连接的主机。",
- "unknown": "未知",
- "connected": "已连接",
- "connecting": "连接中...",
- "disconnecting": "断开连接中...",
- "disconnected": "已断开连接",
"portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
- "disconnect": "断开连接",
- "connect": "连接",
- "canceling": "取消中...",
"endpointHostNotFound": "未找到端点主机"
},
"serverStats": {
@@ -521,7 +679,7 @@
"disk": "磁盘",
"network": "网络",
"uptime": "运行时间",
- "loadAverage": "平均负载",
+ "loadAverage": "平均: {{avg1}}, {{avg5}}, {{avg15}}",
"processes": "进程",
"connections": "连接",
"usage": "使用率",
@@ -537,13 +695,20 @@
"cpuCores_one": "{{count}} 个 CPU",
"cpuCores_other": "{{count}} 个 CPU",
"naCpus": "N/A CPU",
- "loadAverage": "平均: {{avg1}}, {{avg5}}, {{avg15}}",
"loadAverageNA": "平均: N/A",
"cpuUsage": "CPU 使用率",
"memoryUsage": "内存使用率",
"rootStorageSpace": "根目录存储空间",
"of": "的",
- "feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧"
+ "feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧",
+ "failedToFetchHostConfig": "获取主机配置失败",
+ "failedToFetchStatus": "获取服务器状态失败",
+ "failedToFetchMetrics": "获取服务器指标失败",
+ "loadingMetrics": "正在加载指标...",
+ "refreshing": "正在刷新...",
+ "serverOffline": "服务器离线",
+ "cannotFetchMetrics": "无法从离线服务器获取指标",
+ "load": "负载"
},
"auth": {
"loginTitle": "登录 Termix",
@@ -613,7 +778,8 @@
"firstUserMessage": "您是第一个用户,将被设为管理员。您可以在侧边栏用户下拉菜单中查看管理员设置。如果您认为这是错误,请检查 docker 日志,或创建",
"external": "外部",
"loginWithExternal": "使用外部提供商登录",
- "loginWithExternalDesc": "使用您配置的外部身份提供商登录",
+ "loginWithExternalDesc": "使用您配置的外部身份提供者登录",
+ "externalNotSupportedInElectron": "Electron 应用暂不支持外部身份验证。请使用网页版本进行 OIDC 登录。",
"resetPasswordButton": "重置密码",
"sendResetCode": "发送重置代码",
"resetCodeDesc": "输入您的用户名以接收密码重置代码。代码将记录在 docker 容器日志中。",
@@ -645,7 +811,6 @@
"oidcAuthFailed": "OIDC 认证失败",
"noTokenReceived": "登录未收到令牌",
"invalidAuthUrl": "从后端收到无效的授权 URL",
- "connectionTimeout": "连接超时",
"invalidInput": "输入无效",
"requiredField": "此字段为必填项",
"minLength": "最小长度为 {{min}}",
@@ -690,6 +855,9 @@
"external": "外部 (OIDC)",
"selectPreferredLanguage": "选择您的界面首选语言"
},
+ "user": {
+ "failedToLoadVersionInfo": "加载版本信息失败"
+ },
"placeholders": {
"enterCode": "000000",
"ipAddress": "127.0.0.1",
@@ -701,7 +869,11 @@
"hostname": "主机名",
"folder": "文件夹",
"password": "密码",
+ "credentialName": "我的SSH服务器",
+ "description": "SSH凭据描述",
+ "searchCredentials": "按名称、用户名或标签搜索凭据...",
"keyPassword": "密钥密码",
+ "pastePrivateKey": "在此粘贴您的私钥...",
"sshConfig": "端点 SSH 配置",
"homePath": "/home",
"clientId": "您的客户端 ID",
@@ -712,6 +884,7 @@
"userIdField": "sub",
"usernameField": "name",
"scopes": "openid email profile",
+ "userinfoUrl": "https://your-provider.com/application/o/userinfo/",
"enterUsername": "输入用户名以设为管理员",
"searchHosts": "按名称、用户名、IP、文件夹、标签搜索主机...",
"enterPassword": "输入您的密码",
@@ -762,7 +935,6 @@
"deleteItem": "删除项目",
"createNewFile": "创建新文件",
"createNewFolder": "创建新文件夹",
- "deleteItem": "删除项目",
"renameItem": "重命名项目",
"clickToSelectFile": "点击选择文件",
"noSshHosts": "没有 SSH 主机",
@@ -827,8 +999,6 @@
"updateKey": "更新密钥",
"sshpassRequired": "密码认证需要 Sshpass",
"sshServerConfigRequired": "需要 SSH 服务器配置",
- "sshManagerAlreadyOpen": "SSH 管理器已打开",
- "disabledDuringSplitScreen": "分屏期间禁用",
"productionFolder": "生产环境",
"databaseServer": "数据库服务器",
"unknownError": "未知错误",
@@ -851,5 +1021,9 @@
"invalidVerificationCode": "无效的验证码",
"failedToDisableTotp": "禁用 TOTP 失败",
"failedToGenerateBackupCodes": "生成备用码失败"
+ },
+ "mobile": {
+ "selectHostToStart": "选择一个主机以开始您的终端会话",
+ "limitedSupportMessage": "移动端支持目前有限。我们即将推出专门的移动应用以提升您的体验。"
}
-}
\ No newline at end of file
+}
diff --git a/src/main.tsx b/src/main.tsx
index f605e48d..55a6815f 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,14 +1,72 @@
-import {StrictMode} from 'react'
-import {createRoot} from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-import {ThemeProvider} from "@/components/theme-provider"
-import './i18n/i18n' // Initialize i18n
+import { StrictMode, useEffect, useState, useRef } from "react";
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import DesktopApp from "./ui/Desktop/DesktopApp.tsx";
+import { MobileApp } from "./ui/Mobile/MobileApp.tsx";
+import { ThemeProvider } from "@/components/theme-provider";
+import "./i18n/i18n";
+import { isElectron } from "./ui/main-axios.ts";
-createRoot(document.getElementById('root')!).render(
-
-
-
-
- ,
-)
+function useWindowWidth() {
+ const [width, setWidth] = useState(window.innerWidth);
+ const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
+ const lastSwitchTime = useRef(0);
+ const isCurrentlyMobile = useRef(window.innerWidth < 768);
+ const hasSwitchedOnce = useRef(false);
+
+ useEffect(() => {
+ let timeoutId: NodeJS.Timeout;
+ const handleResize = () => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => {
+ const newWidth = window.innerWidth;
+ const newIsMobile = newWidth < 768;
+ const now = Date.now();
+
+ if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) {
+ setWidth(newWidth);
+ return;
+ }
+
+ if (
+ newIsMobile !== isCurrentlyMobile.current &&
+ now - lastSwitchTime.current > 5000
+ ) {
+ lastSwitchTime.current = now;
+ isCurrentlyMobile.current = newIsMobile;
+ hasSwitchedOnce.current = true;
+ setWidth(newWidth);
+ setIsMobile(newIsMobile);
+ } else {
+ setWidth(newWidth);
+ }
+ }, 2000);
+ };
+ window.addEventListener("resize", handleResize);
+
+ return () => {
+ clearTimeout(timeoutId);
+ window.removeEventListener("resize", handleResize);
+ };
+ }, []);
+
+ return width;
+}
+
+function RootApp() {
+ const width = useWindowWidth();
+ const isMobile = width < 768;
+ if (isElectron()) {
+ return ;
+ }
+
+ return isMobile ? : ;
+}
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+ ,
+);
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 00000000..706c8828
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,440 @@
+// ============================================================================
+// CENTRAL TYPE DEFINITIONS
+// ============================================================================
+// This file contains all shared interfaces and types used across the application
+// to avoid duplication and ensure consistency.
+
+import type { Client } from "ssh2";
+
+// ============================================================================
+// SSH HOST TYPES
+// ============================================================================
+
+export interface SSHHost {
+ id: number;
+ name: string;
+ ip: string;
+ port: number;
+ username: string;
+ folder: string;
+ tags: string[];
+ pin: boolean;
+ authType: "password" | "key" | "credential";
+ password?: string;
+ key?: string;
+ keyPassword?: string;
+ keyType?: string;
+ credentialId?: number;
+ userId?: string;
+ enableTerminal: boolean;
+ enableTunnel: boolean;
+ enableFileManager: boolean;
+ defaultPath: string;
+ tunnelConnections: TunnelConnection[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface SSHHostData {
+ name?: string;
+ ip: string;
+ port: number;
+ username: string;
+ folder?: string;
+ tags?: string[];
+ pin?: boolean;
+ authType: "password" | "key" | "credential";
+ password?: string;
+ key?: File | null;
+ keyPassword?: string;
+ keyType?: string;
+ credentialId?: number | null;
+ enableTerminal?: boolean;
+ enableTunnel?: boolean;
+ enableFileManager?: boolean;
+ defaultPath?: string;
+ tunnelConnections?: any[];
+}
+
+// ============================================================================
+// CREDENTIAL TYPES
+// ============================================================================
+
+export interface Credential {
+ id: number;
+ name: string;
+ description?: string;
+ folder?: string;
+ tags: string[];
+ authType: "password" | "key";
+ username: string;
+ password?: string;
+ key?: string;
+ keyPassword?: string;
+ keyType?: string;
+ usageCount: number;
+ lastUsed?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface CredentialData {
+ name: string;
+ description?: string;
+ folder?: string;
+ tags: string[];
+ authType: "password" | "key";
+ username: string;
+ password?: string;
+ key?: string;
+ keyPassword?: string;
+ keyType?: string;
+}
+
+// ============================================================================
+// TUNNEL TYPES
+// ============================================================================
+
+export interface TunnelConnection {
+ sourcePort: number;
+ endpointPort: number;
+ endpointHost: string;
+ maxRetries: number;
+ retryInterval: number;
+ autoStart: boolean;
+}
+
+export interface TunnelConfig {
+ name: string;
+ hostName: string;
+ sourceIP: string;
+ sourceSSHPort: number;
+ sourceUsername: string;
+ sourcePassword?: string;
+ sourceAuthMethod: string;
+ sourceSSHKey?: string;
+ sourceKeyPassword?: string;
+ sourceKeyType?: string;
+ sourceCredentialId?: number;
+ sourceUserId?: string;
+ endpointIP: string;
+ endpointSSHPort: number;
+ endpointUsername: string;
+ endpointPassword?: string;
+ endpointAuthMethod: string;
+ endpointSSHKey?: string;
+ endpointKeyPassword?: string;
+ endpointKeyType?: string;
+ endpointCredentialId?: number;
+ endpointUserId?: string;
+ sourcePort: number;
+ endpointPort: number;
+ maxRetries: number;
+ retryInterval: number;
+ autoStart: boolean;
+ isPinned: boolean;
+}
+
+export interface TunnelStatus {
+ connected: boolean;
+ status: ConnectionState;
+ retryCount?: number;
+ maxRetries?: number;
+ nextRetryIn?: number;
+ reason?: string;
+ errorType?: ErrorType;
+ manualDisconnect?: boolean;
+ retryExhausted?: boolean;
+}
+
+// ============================================================================
+// FILE MANAGER TYPES
+// ============================================================================
+
+export interface Tab {
+ id: string | number;
+ title: string;
+ fileName: string;
+ content: string;
+ isSSH?: boolean;
+ sshSessionId?: string;
+ filePath?: string;
+ loading?: boolean;
+ dirty?: boolean;
+}
+
+export interface FileManagerFile {
+ name: string;
+ path: string;
+ type?: "file" | "directory";
+ isSSH?: boolean;
+ sshSessionId?: string;
+}
+
+export interface FileManagerShortcut {
+ name: string;
+ path: string;
+}
+
+export interface FileItem {
+ name: string;
+ path: string;
+ isPinned?: boolean;
+ type: "file" | "directory";
+ sshSessionId?: string;
+}
+
+export interface ShortcutItem {
+ name: string;
+ path: string;
+}
+
+export interface SSHConnection {
+ id: number;
+ name: string;
+ ip: string;
+ port: number;
+ username: string;
+ isPinned?: boolean;
+}
+
+// ============================================================================
+// HOST INFO TYPES
+// ============================================================================
+
+export interface HostInfo {
+ id: number;
+ name?: string;
+ ip: string;
+ port: number;
+ createdAt: string;
+}
+
+// ============================================================================
+// ALERT TYPES
+// ============================================================================
+
+export interface TermixAlert {
+ id: string;
+ title: string;
+ message: string;
+ expiresAt: string;
+ priority?: "low" | "medium" | "high" | "critical";
+ type?: "info" | "warning" | "error" | "success";
+ actionUrl?: string;
+ actionText?: string;
+}
+
+// ============================================================================
+// TAB TYPES
+// ============================================================================
+
+export interface TabContextTab {
+ id: number;
+ type:
+ | "home"
+ | "terminal"
+ | "ssh_manager"
+ | "server"
+ | "admin"
+ | "file_manager"
+ | "user_profile";
+ title: string;
+ hostConfig?: any;
+ terminalRef?: React.RefObject;
+}
+
+// ============================================================================
+// CONNECTION STATES
+// ============================================================================
+
+export const CONNECTION_STATES = {
+ DISCONNECTED: "disconnected",
+ CONNECTING: "connecting",
+ CONNECTED: "connected",
+ VERIFYING: "verifying",
+ FAILED: "failed",
+ UNSTABLE: "unstable",
+ RETRYING: "retrying",
+ WAITING: "waiting",
+ DISCONNECTING: "disconnecting",
+} as const;
+
+export type ConnectionState =
+ (typeof CONNECTION_STATES)[keyof typeof CONNECTION_STATES];
+
+export type ErrorType =
+ | "CONNECTION_FAILED"
+ | "AUTHENTICATION_FAILED"
+ | "TIMEOUT"
+ | "NETWORK_ERROR"
+ | "UNKNOWN";
+
+// ============================================================================
+// AUTHENTICATION TYPES
+// ============================================================================
+
+export type AuthType = "password" | "key" | "credential";
+
+export type KeyType = "rsa" | "ecdsa" | "ed25519";
+
+// ============================================================================
+// API RESPONSE TYPES
+// ============================================================================
+
+export interface ApiResponse {
+ data?: T;
+ error?: string;
+ message?: string;
+ status?: number;
+}
+
+// ============================================================================
+// COMPONENT PROP TYPES
+// ============================================================================
+
+export interface CredentialsManagerProps {
+ onEditCredential?: (credential: Credential) => void;
+}
+
+export interface CredentialEditorProps {
+ editingCredential?: Credential | null;
+ onFormSubmit?: () => void;
+}
+
+export interface CredentialViewerProps {
+ credential: Credential;
+ onClose: () => void;
+ onEdit: () => void;
+}
+
+export interface CredentialSelectorProps {
+ value?: number | null;
+ onValueChange: (value: number | null) => void;
+}
+
+export interface HostManagerProps {
+ onSelectView?: (view: string) => void;
+ isTopbarOpen?: boolean;
+}
+
+export interface SSHManagerHostEditorProps {
+ editingHost?: SSHHost | null;
+ onFormSubmit?: () => void;
+}
+
+export interface SSHManagerHostViewerProps {
+ onEditHost?: (host: SSHHost) => void;
+}
+
+export interface HostProps {
+ host: SSHHost;
+ onHostConnect?: () => void;
+}
+
+export interface SSHTunnelProps {
+ filterHostKey?: string;
+}
+
+export interface SSHTunnelViewerProps {
+ hosts?: SSHHost[];
+ tunnelStatuses?: Record;
+ tunnelActions?: Record<
+ string,
+ (
+ action: "connect" | "disconnect" | "cancel",
+ host: SSHHost,
+ tunnelIndex: number,
+ ) => Promise
+ >;
+ onTunnelAction?: (
+ action: "connect" | "disconnect" | "cancel",
+ host: SSHHost,
+ tunnelIndex: number,
+ ) => Promise;
+}
+
+export interface FileManagerProps {
+ onSelectView?: (view: string) => void;
+ embedded?: boolean;
+ initialHost?: SSHHost | null;
+}
+
+export interface FileManagerLeftSidebarProps {
+ onSelectView?: (view: string) => void;
+ onOpenFile: (file: any) => void;
+ tabs: Tab[];
+ host: SSHHost;
+ onOperationComplete?: () => void;
+ onError?: (error: string) => void;
+ onSuccess?: (message: string) => void;
+ onPathChange?: (path: string) => void;
+ onDeleteItem?: (item: any) => void;
+}
+
+export interface FileManagerOperationsProps {
+ currentPath: string;
+ sshSessionId: string | null;
+ onOperationComplete?: () => void;
+ onError?: (error: string) => void;
+ onSuccess?: (message: string) => void;
+}
+
+export interface AlertCardProps {
+ alert: TermixAlert;
+ onDismiss: (alertId: string) => void;
+}
+
+export interface AlertManagerProps {
+ alerts: TermixAlert[];
+ onDismiss: (alertId: string) => void;
+ loggedIn: boolean;
+}
+
+export interface SSHTunnelObjectProps {
+ host: SSHHost;
+ tunnelStatuses: Record;
+ tunnelActions: Record;
+ onTunnelAction: (
+ action: "connect" | "disconnect" | "cancel",
+ host: SSHHost,
+ tunnelIndex: number,
+ ) => Promise;
+ compact?: boolean;
+ bare?: boolean;
+}
+
+export interface FolderStats {
+ totalHosts: number;
+ hostsByType: Array<{
+ type: string;
+ count: number;
+ }>;
+}
+
+// ============================================================================
+// BACKEND TYPES
+// ============================================================================
+
+export interface HostConfig {
+ host: SSHHost;
+ tunnels: TunnelConfig[];
+}
+
+export interface VerificationData {
+ conn: Client;
+ timeout: NodeJS.Timeout;
+ startTime: number;
+ attempts: number;
+ maxAttempts: number;
+}
+
+// ============================================================================
+// UTILITY TYPES
+// ============================================================================
+
+export type Optional = Omit & Partial>;
+
+export type RequiredFields = T & Required>;
+
+export type PartialExcept = Partial & Pick;
diff --git a/src/ui/Admin/AdminSettings.tsx b/src/ui/Admin/AdminSettings.tsx
deleted file mode 100644
index ca1ab4f0..00000000
--- a/src/ui/Admin/AdminSettings.tsx
+++ /dev/null
@@ -1,450 +0,0 @@
-import React from "react";
-import {useSidebar} from "@/components/ui/sidebar";
-import {Separator} from "@/components/ui/separator.tsx";
-import {Button} from "@/components/ui/button.tsx";
-import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
-import {Checkbox} from "@/components/ui/checkbox.tsx";
-import {Input} from "@/components/ui/input.tsx";
-import {Label} from "@/components/ui/label.tsx";
-import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table.tsx";
-import {Shield, Trash2, Users} from "lucide-react";
-import {toast} from "sonner";
-import {useTranslation} from "react-i18next";
-import {
- getOIDCConfig,
- getRegistrationAllowed,
- getUserList,
- updateRegistrationAllowed,
- updateOIDCConfig,
- makeUserAdmin,
- removeAdminStatus,
- deleteUser
-} from "@/ui/main-axios.ts";
-
-function getCookie(name: string) {
- return document.cookie.split('; ').reduce((r, v) => {
- const parts = v.split('=');
- return parts[0] === name ? decodeURIComponent(parts[1]) : r;
- }, "");
-}
-
-interface AdminSettingsProps {
- isTopbarOpen?: boolean;
-}
-
-export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.ReactElement {
- const {t} = useTranslation();
- const {state: sidebarState} = useSidebar();
-
- const [allowRegistration, setAllowRegistration] = React.useState(true);
- const [regLoading, setRegLoading] = React.useState(false);
-
- const [oidcConfig, setOidcConfig] = React.useState({
- client_id: '',
- client_secret: '',
- issuer_url: '',
- authorization_url: '',
- token_url: '',
- identifier_path: 'sub',
- name_path: 'name',
- scopes: 'openid email profile',
- userinfo_url: ''
- });
- const [oidcLoading, setOidcLoading] = React.useState(false);
- const [oidcError, setOidcError] = React.useState(null);
-
- const [users, setUsers] = React.useState>([]);
- const [usersLoading, setUsersLoading] = React.useState(false);
- const [newAdminUsername, setNewAdminUsername] = React.useState("");
- const [makeAdminLoading, setMakeAdminLoading] = React.useState(false);
- const [makeAdminError, setMakeAdminError] = React.useState(null);
-
- React.useEffect(() => {
- const jwt = getCookie("jwt");
- if (!jwt) return;
- getOIDCConfig()
- .then(res => {
- if (res) setOidcConfig(res);
- })
- .catch(() => {
- });
- fetchUsers();
- }, []);
-
- React.useEffect(() => {
- getRegistrationAllowed()
- .then(res => {
- if (typeof res?.allowed === 'boolean') {
- setAllowRegistration(res.allowed);
- }
- })
- .catch(() => {
- });
- }, []);
-
- const fetchUsers = async () => {
- const jwt = getCookie("jwt");
- if (!jwt) return;
- setUsersLoading(true);
- try {
- const response = await getUserList();
- setUsers(response.users);
- } finally {
- setUsersLoading(false);
- }
- };
-
- const handleToggleRegistration = async (checked: boolean) => {
- setRegLoading(true);
- const jwt = getCookie("jwt");
- try {
- await updateRegistrationAllowed(checked);
- setAllowRegistration(checked);
- } finally {
- setRegLoading(false);
- }
- };
-
- const handleOIDCConfigSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setOidcLoading(true);
- setOidcError(null);
-
- const required = ['client_id', 'client_secret', 'issuer_url', 'authorization_url', 'token_url'];
- const missing = required.filter(f => !oidcConfig[f as keyof typeof oidcConfig]);
- if (missing.length > 0) {
- setOidcError(t('admin.missingRequiredFields', { fields: missing.join(', ') }));
- setOidcLoading(false);
- return;
- }
-
- const jwt = getCookie("jwt");
- try {
- await updateOIDCConfig(oidcConfig);
- toast.success(t('admin.oidcConfigurationUpdated'));
- } catch (err: any) {
- setOidcError(err?.response?.data?.error || t('admin.failedToUpdateOidcConfig'));
- } finally {
- setOidcLoading(false);
- }
- };
-
- const handleOIDCConfigChange = (field: string, value: string) => {
- setOidcConfig(prev => ({...prev, [field]: value}));
- };
-
- const handleMakeUserAdmin = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!newAdminUsername.trim()) return;
- setMakeAdminLoading(true);
- setMakeAdminError(null);
- const jwt = getCookie("jwt");
- try {
- await makeUserAdmin(newAdminUsername.trim());
- toast.success(t('admin.userIsNowAdmin', { username: newAdminUsername }));
- setNewAdminUsername("");
- fetchUsers();
- } catch (err: any) {
- setMakeAdminError(err?.response?.data?.error || t('admin.failedToMakeUserAdmin'));
- } finally {
- setMakeAdminLoading(false);
- }
- };
-
- const handleRemoveAdminStatus = async (username: string) => {
- if (!confirm(t('admin.removeAdminStatus', { username }))) return;
- const jwt = getCookie("jwt");
- try {
- await removeAdminStatus(username);
- toast.success(t('admin.adminStatusRemoved', { username }));
- fetchUsers();
- } catch (err: any) {
- console.error('Failed to remove admin status:', err);
- toast.error(t('admin.failedToRemoveAdminStatus'));
- }
- };
-
- const handleDeleteUser = async (username: string) => {
- if (!confirm(t('admin.deleteUser', { username }))) return;
- const jwt = getCookie("jwt");
- try {
- await deleteUser(username);
- toast.success(t('admin.userDeletedSuccessfully', { username }));
- fetchUsers();
- } catch (err: any) {
- console.error('Failed to delete user:', err);
- toast.error(t('admin.failedToDeleteUser'));
- }
- };
-
- const topMarginPx = isTopbarOpen ? 74 : 26;
- const leftMarginPx = sidebarState === 'collapsed' ? 26 : 8;
- const bottomMarginPx = 8;
- const wrapperStyle: React.CSSProperties = {
- marginLeft: leftMarginPx,
- marginRight: 17,
- marginTop: topMarginPx,
- marginBottom: bottomMarginPx,
- height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`
- };
-
- return (
-
-
-
-
{t('admin.title')}
-
-
-
-
-
-
-
-
- {t('admin.general')}
-
-
-
- OIDC
-
-
-
- {t('admin.users')}
-
-
-
- {t('admin.adminManagement')}
-
-
-
-
-
-
{t('admin.userRegistration')}
-
-
- {t('admin.allowNewAccountRegistration')}
-
-
-
-
-
-
-
{t('admin.externalAuthentication')}
-
{t('admin.configureExternalProvider')}
-
- {oidcError && (
-
- {t('common.error')}
- {oidcError}
-
- )}
-
-
-
-
-
-
-
-
-
{t('admin.userManagement')}
- {usersLoading ? t('admin.loading') : t('admin.refresh')}
-
- {usersLoading ? (
-
{t('admin.loadingUsers')}
- ) : (
-
-
-
-
- {t('admin.username')}
- {t('admin.type')}
- {t('admin.actions')}
-
-
-
- {users.map((user) => (
-
-
- {user.username}
- {user.is_admin && (
- {t('admin.adminBadge')}
- )}
-
- {user.is_oidc ? t('admin.external') : t('admin.local')}
-
- handleDeleteUser(user.username)}
- className="text-red-600 hover:text-red-700 hover:bg-red-50"
- disabled={user.is_admin}>
-
-
-
-
- ))}
-
-
-
- )}
-
-
-
-
-
-
{t('admin.adminManagement')}
-
-
{t('admin.makeUserAdmin')}
-
-
-
-
-
{t('admin.currentAdmins')}
-
-
-
-
- {t('admin.username')}
- {t('admin.type')}
- {t('admin.actions')}
-
-
-
- {users.filter(u => u.is_admin).map((admin) => (
-
-
- {admin.username}
- {t('admin.adminBadge')}
-
- {admin.is_oidc ? t('admin.external') : t('admin.local')}
-
- handleRemoveAdminStatus(admin.username)}
- className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
-
- {t('admin.removeAdminButton')}
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default AdminSettings;
\ No newline at end of file
diff --git a/src/ui/Apps/File Manager/FIleManagerTopNavbar.tsx b/src/ui/Apps/File Manager/FIleManagerTopNavbar.tsx
deleted file mode 100644
index 84fb12c6..00000000
--- a/src/ui/Apps/File Manager/FIleManagerTopNavbar.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from "react";
-import { FileManagerTabList } from "./FileManagerTabList.tsx";
-
-interface FileManagerTopNavbarProps {
- tabs: {id: string | number, title: string}[];
- activeTab: string | number;
- setActiveTab: (tab: string | number) => void;
- closeTab: (tab: string | number) => void;
- onHomeClick: () => void;
-}
-
-export function FIleManagerTopNavbar(props: FileManagerTopNavbarProps): React.ReactElement {
- const { tabs, activeTab, setActiveTab, closeTab, onHomeClick } = props;
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/File Manager/FileManager.tsx b/src/ui/Apps/File Manager/FileManager.tsx
deleted file mode 100644
index 85e33c00..00000000
--- a/src/ui/Apps/File Manager/FileManager.tsx
+++ /dev/null
@@ -1,692 +0,0 @@
-import React, {useState, useEffect, useRef} from "react";
-import {FileManagerLeftSidebar} from "@/ui/Apps/File Manager/FileManagerLeftSidebar.tsx";
-import {FileManagerTabList} from "@/ui/Apps/File Manager/FileManagerTabList.tsx";
-import {FileManagerHomeView} from "@/ui/Apps/File Manager/FileManagerHomeView.tsx";
-import {FileManagerFileEditor} from "@/ui/Apps/File Manager/FileManagerFileEditor.tsx";
-import {FileManagerOperations} from "@/ui/Apps/File Manager/FileManagerOperations.tsx";
-import {Button} from '@/components/ui/button.tsx';
-import {FIleManagerTopNavbar} from "@/ui/Apps/File Manager/FIleManagerTopNavbar.tsx";
-import {cn} from '@/lib/utils.ts';
-import {Save, RefreshCw, Settings, Trash2} from 'lucide-react';
-import {Separator} from '@/components/ui/separator.tsx';
-import {toast} from 'sonner';
-import {useTranslation} from 'react-i18next';
-import {
- getFileManagerRecent,
- getFileManagerPinned,
- getFileManagerShortcuts,
- addFileManagerRecent,
- removeFileManagerRecent,
- addFileManagerPinned,
- removeFileManagerPinned,
- addFileManagerShortcut,
- removeFileManagerShortcut,
- readSSHFile,
- writeSSHFile,
- getSSHStatus,
- connectSSH
-} from '@/ui/main-axios.ts';
-
-interface Tab {
- id: string | number;
- title: string;
- fileName: string;
- content: string;
- isSSH?: boolean;
- sshSessionId?: string;
- filePath?: string;
- loading?: boolean;
- dirty?: 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: any[];
- createdAt: string;
- updatedAt: string;
-}
-
-export function FileManager({onSelectView, embedded = false, initialHost = null}: {
- onSelectView?: (view: string) => void,
- embedded?: boolean,
- initialHost?: SSHHost | null
-}): React.ReactElement {
- const {t} = useTranslation();
- const [tabs, setTabs] = useState([]);
- const [activeTab, setActiveTab] = useState('home');
- const [recent, setRecent] = useState([]);
- const [pinned, setPinned] = useState([]);
- const [shortcuts, setShortcuts] = useState([]);
-
- const [currentHost, setCurrentHost] = useState(null);
- const [isSaving, setIsSaving] = useState(false);
-
- const [showOperations, setShowOperations] = useState(false);
- const [currentPath, setCurrentPath] = useState('/');
-
- const [deletingItem, setDeletingItem] = useState(null);
-
- const sidebarRef = useRef(null);
-
- useEffect(() => {
- if (initialHost && (!currentHost || currentHost.id !== initialHost.id)) {
- setCurrentHost(initialHost);
- setTimeout(() => {
- try {
- const path = initialHost.defaultPath || '/';
- if (sidebarRef.current && sidebarRef.current.openFolder) {
- sidebarRef.current.openFolder(initialHost, path);
- }
- } catch (e) {
- }
- }, 0);
- }
- }, [initialHost]);
-
- useEffect(() => {
- if (currentHost) {
- fetchHomeData();
- } else {
- setRecent([]);
- setPinned([]);
- setShortcuts([]);
- }
- }, [currentHost]);
-
- useEffect(() => {
- if (activeTab === 'home' && currentHost) {
- fetchHomeData();
- }
- }, [activeTab, currentHost]);
-
- useEffect(() => {
- if (activeTab === 'home' && currentHost) {
- const interval = setInterval(() => {
- fetchHomeData();
- }, 2000);
-
- return () => clearInterval(interval);
- }
- }, [activeTab, currentHost]);
-
- async function fetchHomeData() {
- if (!currentHost) return;
-
- try {
- const homeDataPromise = Promise.all([
- getFileManagerRecent(currentHost.id),
- getFileManagerPinned(currentHost.id),
- getFileManagerShortcuts(currentHost.id),
- ]);
-
- const timeoutPromise = new Promise((_, reject) =>
- setTimeout(() => reject(new Error(t('fileManager.fetchHomeDataTimeout'))), 15000)
- );
-
- const [recentRes, pinnedRes, shortcutsRes] = await Promise.race([homeDataPromise, timeoutPromise]) as [any, any, any];
-
- const recentWithPinnedStatus = (recentRes || []).map(file => ({
- ...file,
- type: 'file',
- isPinned: (pinnedRes || []).some(pinnedFile =>
- pinnedFile.path === file.path && pinnedFile.name === file.name
- )
- }));
-
- const pinnedWithType = (pinnedRes || []).map(file => ({
- ...file,
- type: 'file'
- }));
-
- setRecent(recentWithPinnedStatus);
- setPinned(pinnedWithType);
- setShortcuts((shortcutsRes || []).map(shortcut => ({
- ...shortcut,
- type: 'directory'
- })));
- } catch (err: any) {
- }
- }
-
- const formatErrorMessage = (err: any, defaultMessage: string): string => {
- if (typeof err === 'object' && err !== null && 'response' in err) {
- const axiosErr = err as any;
- if (axiosErr.response?.status === 403) {
- return `${t('fileManager.permissionDenied')}. ${defaultMessage}. ${t('fileManager.checkDockerLogs')}.`;
- } else if (axiosErr.response?.status === 500) {
- const backendError = axiosErr.response?.data?.error || t('fileManager.internalServerError');
- return `${t('fileManager.serverError')} (500): ${backendError}. ${t('fileManager.checkDockerLogs')}.`;
- } else if (axiosErr.response?.data?.error) {
- const backendError = axiosErr.response.data.error;
- return `${axiosErr.response?.status ? `${t('fileManager.error')} ${axiosErr.response.status}: ` : ''}${backendError}. ${t('fileManager.checkDockerLogs')}.`;
- } else {
- return `${t('fileManager.requestFailed')} ${axiosErr.response?.status || t('fileManager.unknown')}. ${t('fileManager.checkDockerLogs')}.`;
- }
- } else if (err instanceof Error) {
- return `${err.message}. ${t('fileManager.checkDockerLogs')}.`;
- } else {
- return `${defaultMessage}. ${t('fileManager.checkDockerLogs')}.`;
- }
- };
-
- const handleOpenFile = async (file: any) => {
- const tabId = file.path;
-
- if (!tabs.find(t => t.id === tabId)) {
- const currentSshSessionId = currentHost?.id.toString();
-
- setTabs([...tabs, {
- id: tabId,
- title: file.name,
- fileName: file.name,
- content: '',
- filePath: file.path,
- isSSH: true,
- sshSessionId: currentSshSessionId,
- loading: true
- }]);
- try {
- const res = await readSSHFile(currentSshSessionId, file.path);
- setTabs(tabs => tabs.map(t => t.id === tabId ? {
- ...t,
- content: res.content,
- loading: false,
- error: undefined
- } : t));
- await addFileManagerRecent({
- name: file.name,
- path: file.path,
- isSSH: true,
- sshSessionId: currentSshSessionId,
- hostId: currentHost?.id
- });
- fetchHomeData();
- } catch (err: any) {
- const errorMessage = formatErrorMessage(err, t('fileManager.cannotReadFile'));
- toast.error(errorMessage);
- setTabs(tabs => tabs.map(t => t.id === tabId ? {...t, loading: false} : t));
- }
- }
- setActiveTab(tabId);
- };
-
- const handleRemoveRecent = async (file: any) => {
- try {
- await removeFileManagerRecent({
- name: file.name,
- path: file.path,
- isSSH: true,
- sshSessionId: file.sshSessionId,
- hostId: currentHost?.id
- });
- fetchHomeData();
- } catch (err) {
- }
- };
-
- const handlePinFile = async (file: any) => {
- try {
- await addFileManagerPinned({
- name: file.name,
- path: file.path,
- isSSH: true,
- sshSessionId: file.sshSessionId,
- hostId: currentHost?.id
- });
- fetchHomeData();
- if (sidebarRef.current && sidebarRef.current.fetchFiles) {
- sidebarRef.current.fetchFiles();
- }
- } catch (err) {
- }
- };
-
- const handleUnpinFile = async (file: any) => {
- try {
- await removeFileManagerPinned({
- name: file.name,
- path: file.path,
- isSSH: true,
- sshSessionId: file.sshSessionId,
- hostId: currentHost?.id
- });
- fetchHomeData();
- if (sidebarRef.current && sidebarRef.current.fetchFiles) {
- sidebarRef.current.fetchFiles();
- }
- } catch (err) {
- }
- };
-
- const handleOpenShortcut = async (shortcut: any) => {
- if (sidebarRef.current?.isOpeningShortcut) {
- return;
- }
-
- if (sidebarRef.current && sidebarRef.current.openFolder) {
- try {
- sidebarRef.current.isOpeningShortcut = true;
-
- const normalizedPath = shortcut.path.startsWith('/') ? shortcut.path : `/${shortcut.path}`;
-
- await sidebarRef.current.openFolder(currentHost, normalizedPath);
- } catch (err) {
- } finally {
- if (sidebarRef.current) {
- sidebarRef.current.isOpeningShortcut = false;
- }
- }
- } else {
- }
- };
-
- const handleAddShortcut = async (folderPath: string) => {
- try {
- const name = folderPath.split('/').pop() || folderPath;
- await addFileManagerShortcut({
- name,
- path: folderPath,
- isSSH: true,
- sshSessionId: currentHost?.id.toString(),
- hostId: currentHost?.id
- });
- fetchHomeData();
- } catch (err) {
- }
- };
-
- const handleRemoveShortcut = async (shortcut: any) => {
- try {
- await removeFileManagerShortcut({
- name: shortcut.name,
- path: shortcut.path,
- isSSH: true,
- sshSessionId: currentHost?.id.toString(),
- hostId: currentHost?.id
- });
- fetchHomeData();
- } catch (err) {
- }
- };
-
- const closeTab = (tabId: string | number) => {
- const idx = tabs.findIndex(t => t.id === tabId);
- const newTabs = tabs.filter(t => t.id !== tabId);
- setTabs(newTabs);
- if (activeTab === tabId) {
- if (newTabs.length > 0) setActiveTab(newTabs[Math.max(0, idx - 1)].id);
- else setActiveTab('home');
- }
- if (currentHost) {
- fetchHomeData();
- }
- };
-
- const setTabContent = (tabId: string | number, content: string) => {
- setTabs(tabs => tabs.map(t => t.id === tabId ? {
- ...t,
- content,
- dirty: true,
- error: undefined,
- success: undefined
- } : t));
- };
-
- const handleSave = async (tab: Tab) => {
- if (isSaving) {
- return;
- }
-
- setIsSaving(true);
-
- try {
- if (!tab.sshSessionId) {
- throw new Error(t('fileManager.noSshSessionId'));
- }
-
- if (!tab.filePath) {
- throw new Error(t('fileManager.noFilePath'));
- }
-
- if (!currentHost?.id) {
- throw new Error(t('fileManager.noCurrentHost'));
- }
-
- try {
- const statusPromise = getSSHStatus(tab.sshSessionId);
- const statusTimeoutPromise = new Promise((_, reject) =>
- setTimeout(() => reject(new Error(t('fileManager.sshStatusCheckTimeout'))), 10000)
- );
-
- const status = await Promise.race([statusPromise, statusTimeoutPromise]) as { connected: boolean };
-
- if (!status.connected) {
- const connectPromise = connectSSH(tab.sshSessionId, {
- ip: currentHost.ip,
- port: currentHost.port,
- username: currentHost.username,
- password: currentHost.password,
- sshKey: currentHost.key,
- keyPassword: currentHost.keyPassword
- });
- const connectTimeoutPromise = new Promise((_, reject) =>
- setTimeout(() => reject(new Error(t('fileManager.sshReconnectionTimeout'))), 15000)
- );
-
- await Promise.race([connectPromise, connectTimeoutPromise]);
- }
- } catch (statusErr) {
- }
-
- const savePromise = writeSSHFile(tab.sshSessionId, tab.filePath, tab.content);
- const timeoutPromise = new Promise((_, reject) =>
- setTimeout(() => {
- reject(new Error(t('fileManager.saveOperationTimeout')));
- }, 30000)
- );
-
- const result = await Promise.race([savePromise, timeoutPromise]);
- setTabs(tabs => tabs.map(t => t.id === tab.id ? {
- ...t,
- loading: false
- } : t));
-
- toast.success(t('fileManager.fileSavedSuccessfully'));
-
- Promise.allSettled([
- (async () => {
- try {
- await addFileManagerRecent({
- name: tab.fileName,
- path: tab.filePath,
- isSSH: true,
- sshSessionId: tab.sshSessionId,
- hostId: currentHost.id
- });
- } catch (recentErr) {
- }
- })(),
- (async () => {
- try {
- await fetchHomeData();
- } catch (refreshErr) {
- }
- })()
- ]).then(() => {
- });
-
- } catch (err) {
- let errorMessage = formatErrorMessage(err, t('fileManager.cannotSaveFile'));
-
- if (errorMessage.includes('timed out') || errorMessage.includes('timeout')) {
- errorMessage = t('fileManager.saveTimeout');
- }
-
- toast.error(`${t('fileManager.failedToSaveFile')}: ${errorMessage}`);
- setTabs(tabs => tabs.map(t => t.id === tab.id ? {
- ...t,
- loading: false
- } : t));
- } finally {
- setIsSaving(false);
- }
- };
-
- const handleHostChange = (_host: SSHHost | null) => {
- };
-
- const handleOperationComplete = () => {
- if (sidebarRef.current && sidebarRef.current.fetchFiles) {
- sidebarRef.current.fetchFiles();
- }
- if (currentHost) {
- fetchHomeData();
- }
- };
-
- const handleSuccess = (message: string) => {
- toast.success(message);
- };
-
- const handleError = (error: string) => {
- toast.error(error);
- };
-
- const updateCurrentPath = (newPath: string) => {
- setCurrentPath(newPath);
- };
-
- const handleDeleteFromSidebar = (item: any) => {
- setDeletingItem(item);
- };
-
- const performDelete = async (item: any) => {
- if (!currentHost?.id) return;
-
- try {
- const {deleteSSHItem} = await import('@/ui/main-axios.ts');
- await deleteSSHItem(currentHost.id.toString(), item.path, item.type === 'directory');
- toast.success(`${item.type === 'directory' ? t('fileManager.folder') : t('fileManager.file')} ${t('fileManager.deletedSuccessfully')}`);
- setDeletingItem(null);
- handleOperationComplete();
- } catch (error: any) {
- handleError(error?.response?.data?.error || t('fileManager.failedToDeleteItem'));
- }
- };
-
- if (!currentHost) {
- return (
-
-
- {
- })}
- onOpenFile={handleOpenFile}
- tabs={tabs}
- ref={sidebarRef}
- host={initialHost as SSHHost}
- onOperationComplete={handleOperationComplete}
- onError={handleError}
- onSuccess={handleSuccess}
- onPathChange={updateCurrentPath}
- />
-
-
-
-
{t('fileManager.connectToServer')}
-
{t('fileManager.selectServerToEdit')}
-
-
-
- );
- }
-
- return (
-
-
- {
- })}
- onOpenFile={handleOpenFile}
- tabs={tabs}
- ref={sidebarRef}
- host={currentHost as SSHHost}
- onOperationComplete={handleOperationComplete}
- onError={handleError}
- onSuccess={handleSuccess}
- onPathChange={updateCurrentPath}
- onDeleteItem={handleDeleteFromSidebar}
- />
-
-
-
-
- ({id: t.id, title: t.title}))}
- activeTab={activeTab}
- setActiveTab={setActiveTab}
- closeTab={closeTab}
- onHomeClick={() => {
- setActiveTab('home');
- if (currentHost) {
- fetchHomeData();
- }
- }}
- />
-
-
-
setShowOperations(!showOperations)}
- className={cn(
- 'w-[30px] h-[30px]',
- showOperations ? 'bg-[#2d2d30] border-[#434345]' : ''
- )}
- title={t('fileManager.fileOperations')}
- >
-
-
-
-
{
- const tab = tabs.find(t => t.id === activeTab);
- if (tab && !isSaving) handleSave(tab);
- }}
- disabled={activeTab === 'home' || !tabs.find(t => t.id === activeTab)?.dirty || isSaving}
- className={cn(
- 'w-[30px] h-[30px]',
- activeTab === 'home' || !tabs.find(t => t.id === activeTab)?.dirty || isSaving ? 'opacity-60 cursor-not-allowed' : ''
- )}
- >
- {isSaving ? : }
-
-
-
-
-
-
-
- {activeTab === 'home' ? (
-
- ) : (
- (() => {
- const tab = tabs.find(t => t.id === activeTab);
- if (!tab) return null;
- return (
-
-
- setTabContent(tab.id, content)}
- />
-
-
- );
- })()
- )}
-
- {showOperations && (
-
-
-
- )}
-
-
-
- {deletingItem && (
-
-
-
-
-
-
-
- {t('fileManager.confirmDelete')}
-
-
- {t('fileManager.confirmDeleteMessage', { name: deletingItem.name })}
- {deletingItem.type === 'directory' && ` ${t('fileManager.deleteDirectoryWarning')}`}
-
-
- {t('fileManager.actionCannotBeUndone')}
-
-
- performDelete(deletingItem)}
- className="flex-1"
- >
- {t('common.delete')}
-
- setDeletingItem(null)}
- className="flex-1"
- >
- {t('common.cancel')}
-
-
-
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/File Manager/FileManagerFileEditor.tsx b/src/ui/Apps/File Manager/FileManagerFileEditor.tsx
deleted file mode 100644
index bf16d2ff..00000000
--- a/src/ui/Apps/File Manager/FileManagerFileEditor.tsx
+++ /dev/null
@@ -1,350 +0,0 @@
-import React, {useState, useEffect} from "react";
-import CodeMirror from "@uiw/react-codemirror";
-import {loadLanguage} from '@uiw/codemirror-extensions-langs';
-import {hyperLink} from '@uiw/codemirror-extensions-hyper-link';
-import {oneDark} from '@codemirror/theme-one-dark';
-import {EditorView} from '@codemirror/view';
-
-interface FileManagerCodeEditorProps {
- content: string;
- fileName: string;
- onContentChange: (value: string) => void;
-}
-
-export function FileManagerFileEditor({content, fileName, onContentChange}: FileManagerCodeEditorProps) {
- function getLanguageName(filename: string): string {
- if (!filename || typeof filename !== 'string') {
- return 'text';
- }
- const lastDotIndex = filename.lastIndexOf('.');
- if (lastDotIndex === -1) {
- return 'text';
- }
- const ext = filename.slice(lastDotIndex + 1).toLowerCase();
-
- switch (ext) {
- case 'ng':
- return 'angular';
- case 'apl':
- return 'apl';
- case 'asc':
- return 'asciiArmor';
- case 'ast':
- return 'asterisk';
- case 'bf':
- return 'brainfuck';
- case 'c':
- return 'c';
- case 'ceylon':
- return 'ceylon';
- case 'clj':
- return 'clojure';
- case 'cmake':
- return 'cmake';
- case 'cob':
- case 'cbl':
- return 'cobol';
- case 'coffee':
- return 'coffeescript';
- case 'lisp':
- return 'commonLisp';
- case 'cpp':
- case 'cc':
- case 'cxx':
- return 'cpp';
- case 'cr':
- return 'crystal';
- case 'cs':
- return 'csharp';
- case 'css':
- return 'css';
- case 'cypher':
- return 'cypher';
- case 'd':
- return 'd';
- case 'dart':
- return 'dart';
- case 'diff':
- case 'patch':
- return 'diff';
- case 'dockerfile':
- return 'dockerfile';
- case 'dtd':
- return 'dtd';
- case 'dylan':
- return 'dylan';
- case 'ebnf':
- return 'ebnf';
- case 'ecl':
- return 'ecl';
- case 'eiffel':
- return 'eiffel';
- case 'elm':
- return 'elm';
- case 'erl':
- return 'erlang';
- case 'factor':
- return 'factor';
- case 'fcl':
- return 'fcl';
- case 'fs':
- return 'forth';
- case 'f90':
- case 'for':
- return 'fortran';
- case 's':
- return 'gas';
- case 'feature':
- return 'gherkin';
- case 'go':
- return 'go';
- case 'groovy':
- return 'groovy';
- case 'hs':
- return 'haskell';
- case 'hx':
- return 'haxe';
- case 'html':
- case 'htm':
- return 'html';
- case 'http':
- return 'http';
- case 'idl':
- return 'idl';
- case 'java':
- return 'java';
- case 'js':
- case 'mjs':
- case 'cjs':
- return 'javascript';
- case 'jinja2':
- case 'j2':
- return 'jinja2';
- case 'json':
- return 'json';
- case 'jsx':
- return 'jsx';
- case 'jl':
- return 'julia';
- case 'kt':
- case 'kts':
- return 'kotlin';
- case 'less':
- return 'less';
- case 'lezer':
- return 'lezer';
- case 'liquid':
- return 'liquid';
- case 'litcoffee':
- return 'livescript';
- case 'lua':
- return 'lua';
- case 'md':
- return 'markdown';
- case 'nb':
- case 'mat':
- return 'mathematica';
- case 'mbox':
- return 'mbox';
- case 'mmd':
- return 'mermaid';
- case 'mrc':
- return 'mirc';
- case 'moo':
- return 'modelica';
- case 'mscgen':
- return 'mscgen';
- case 'm':
- return 'mumps';
- case 'sql':
- return 'mysql';
- case 'nc':
- return 'nesC';
- case 'nginx':
- return 'nginx';
- case 'nix':
- return 'nix';
- case 'nsi':
- return 'nsis';
- case 'nt':
- return 'ntriples';
- case 'mm':
- return 'objectiveCpp';
- case 'octave':
- return 'octave';
- case 'oz':
- return 'oz';
- case 'pas':
- return 'pascal';
- case 'pl':
- case 'pm':
- return 'perl';
- case 'pgsql':
- return 'pgsql';
- case 'php':
- return 'php';
- case 'pig':
- return 'pig';
- case 'ps1':
- return 'powershell';
- case 'properties':
- return 'properties';
- case 'proto':
- return 'protobuf';
- case 'pp':
- return 'puppet';
- case 'py':
- return 'python';
- case 'q':
- return 'q';
- case 'r':
- return 'r';
- case 'rb':
- return 'ruby';
- case 'rs':
- return 'rust';
- case 'sas':
- return 'sas';
- case 'sass':
- case 'scss':
- return 'sass';
- case 'scala':
- return 'scala';
- case 'scm':
- return 'scheme';
- case 'shader':
- return 'shader';
- case 'sh':
- case 'bash':
- return 'shell';
- case 'siv':
- return 'sieve';
- case 'st':
- return 'smalltalk';
- case 'sol':
- return 'solidity';
- case 'solr':
- return 'solr';
- case 'rq':
- return 'sparql';
- case 'xlsx':
- case 'ods':
- case 'csv':
- return 'spreadsheet';
- case 'nut':
- return 'squirrel';
- case 'tex':
- return 'stex';
- case 'styl':
- return 'stylus';
- case 'svelte':
- return 'svelte';
- case 'swift':
- return 'swift';
- case 'tcl':
- return 'tcl';
- case 'textile':
- return 'textile';
- case 'tiddlywiki':
- return 'tiddlyWiki';
- case 'tiki':
- return 'tiki';
- case 'toml':
- return 'toml';
- case 'troff':
- return 'troff';
- case 'tsx':
- return 'tsx';
- case 'ttcn':
- return 'ttcn';
- case 'ttl':
- case 'turtle':
- return 'turtle';
- case 'ts':
- return 'typescript';
- case 'vb':
- return 'vb';
- case 'vbs':
- return 'vbscript';
- case 'vm':
- return 'velocity';
- case 'v':
- return 'verilog';
- case 'vhd':
- case 'vhdl':
- return 'vhdl';
- case 'vue':
- return 'vue';
- case 'wat':
- return 'wast';
- case 'webidl':
- return 'webIDL';
- case 'xq':
- case 'xquery':
- return 'xQuery';
- case 'xml':
- return 'xml';
- case 'yacas':
- return 'yacas';
- case 'yaml':
- case 'yml':
- return 'yaml';
- case 'z80':
- return 'z80';
- default:
- return 'text';
- }
- }
-
- useEffect(() => {
- document.body.style.overflowX = 'hidden';
- return () => {
- document.body.style.overflowX = '';
- };
- }, []);
-
- return (
-
-
- onContentChange(value)}
- theme={undefined}
- height="100%"
- basicSetup={{lineNumbers: true}}
- style={{minHeight: '100%', minWidth: '100%', flex: 1}}
- />
-
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/File Manager/FileManagerHomeView.tsx b/src/ui/Apps/File Manager/FileManagerHomeView.tsx
deleted file mode 100644
index 7e735ca2..00000000
--- a/src/ui/Apps/File Manager/FileManagerHomeView.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import React from 'react';
-import {Button} from '@/components/ui/button.tsx';
-import {Trash2, Folder, File, Plus, Pin} from 'lucide-react';
-import {Tabs, TabsList, TabsTrigger, TabsContent} from '@/components/ui/tabs.tsx';
-import {Input} from '@/components/ui/input.tsx';
-import {useState} from 'react';
-import {useTranslation} from 'react-i18next';
-
-interface FileItem {
- name: string;
- path: string;
- isPinned?: boolean;
- type: 'file' | 'directory';
- sshSessionId?: string;
-}
-
-interface ShortcutItem {
- name: string;
- path: string;
-}
-
-interface FileManagerHomeViewProps {
- recent: FileItem[];
- pinned: FileItem[];
- shortcuts: ShortcutItem[];
- onOpenFile: (file: FileItem) => void;
- onRemoveRecent: (file: FileItem) => void;
- onPinFile: (file: FileItem) => void;
- onUnpinFile: (file: FileItem) => void;
- onOpenShortcut: (shortcut: ShortcutItem) => void;
- onRemoveShortcut: (shortcut: ShortcutItem) => void;
- onAddShortcut: (path: string) => void;
-}
-
-export function FileManagerHomeView({
- recent,
- pinned,
- shortcuts,
- onOpenFile,
- onRemoveRecent,
- onPinFile,
- onUnpinFile,
- onOpenShortcut,
- onRemoveShortcut,
- onAddShortcut
- }: FileManagerHomeViewProps) {
- const {t} = useTranslation();
- const [tab, setTab] = useState<'recent' | 'pinned' | 'shortcuts'>('recent');
- const [newShortcut, setNewShortcut] = useState('');
-
-
- const renderFileCard = (file: FileItem, onRemove: () => void, onPin?: () => void, isPinned = false) => (
-
-
onOpenFile(file)}
- >
- {file.type === 'directory' ?
-
:
-
- }
-
-
-
- {onPin && (
-
-
-
- )}
- {onRemove && (
-
-
-
- )}
-
-
- );
-
- const renderShortcutCard = (shortcut: ShortcutItem) => (
-
-
onOpenShortcut(shortcut)}
- >
-
-
-
-
- onRemoveShortcut(shortcut)}
- >
-
-
-
-
- );
-
- return (
-
-
setTab(v as 'recent' | 'pinned' | 'shortcuts')} className="w-full">
-
- {t('fileManager.recent')}
- {t('fileManager.pinned')}
- {t('fileManager.folderShortcuts')}
-
-
-
-
- {recent.length === 0 ? (
-
- {t('fileManager.noRecentFiles')}
-
- ) : recent.map((file) =>
- renderFileCard(
- file,
- () => onRemoveRecent(file),
- () => file.isPinned ? onUnpinFile(file) : onPinFile(file),
- file.isPinned
- )
- )}
-
-
-
-
-
- {pinned.length === 0 ? (
-
- {t('fileManager.noPinnedFiles')}
-
- ) : pinned.map((file) =>
- renderFileCard(
- file,
- undefined,
- () => onUnpinFile(file),
- true
- )
- )}
-
-
-
-
-
-
setNewShortcut(e.target.value)}
- className="flex-1 bg-[#23232a] border-2 border-[#303032] text-white placeholder:text-muted-foreground"
- onKeyDown={(e) => {
- if (e.key === 'Enter' && newShortcut.trim()) {
- onAddShortcut(newShortcut.trim());
- setNewShortcut('');
- }
- }}
- />
-
{
- if (newShortcut.trim()) {
- onAddShortcut(newShortcut.trim());
- setNewShortcut('');
- }
- }}
- >
-
- {t('common.add')}
-
-
-
- {shortcuts.length === 0 ? (
-
- {t('fileManager.noShortcuts')}
-
- ) : shortcuts.map((shortcut) =>
- renderShortcutCard(shortcut)
- )}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx b/src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx
deleted file mode 100644
index 290c4ab7..00000000
--- a/src/ui/Apps/File Manager/FileManagerLeftSidebar.tsx
+++ /dev/null
@@ -1,574 +0,0 @@
-import React, {useEffect, useState, useRef, forwardRef, useImperativeHandle} from 'react';
-import {Separator} from '@/components/ui/separator.tsx';
-import {CornerDownLeft, Folder, File, Server, ArrowUp, Pin, MoreVertical, Trash2, Edit3} from 'lucide-react';
-import {ScrollArea} from '@/components/ui/scroll-area.tsx';
-import {cn} from '@/lib/utils.ts';
-import {Input} from '@/components/ui/input.tsx';
-import {Button} from '@/components/ui/button.tsx';
-import {toast} from 'sonner';
-import {useTranslation} from 'react-i18next';
-import {
- listSSHFiles,
- renameSSHItem,
- deleteSSHItem,
- getFileManagerRecent,
- getFileManagerPinned,
- addFileManagerPinned,
- removeFileManagerPinned,
- readSSHFile,
- getSSHStatus,
- connectSSH
-} from '@/ui/main-axios.ts';
-
-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: any[];
- createdAt: string;
- updatedAt: string;
-}
-
-const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
- {onSelectView, onOpenFile, tabs, host, onOperationComplete, onError, onSuccess, onPathChange, onDeleteItem}: {
- onSelectView?: (view: string) => void;
- onOpenFile: (file: any) => void;
- tabs: any[];
- host: SSHHost;
- onOperationComplete?: () => void;
- onError?: (error: string) => void;
- onSuccess?: (message: string) => void;
- onPathChange?: (path: string) => void;
- onDeleteItem?: (item: any) => void;
- },
- ref
-) {
- const {t} = useTranslation();
- const [currentPath, setCurrentPath] = useState('/');
- const [files, setFiles] = useState([]);
- const pathInputRef = useRef(null);
-
- const [search, setSearch] = useState('');
- const [debouncedSearch, setDebouncedSearch] = useState('');
- const [fileSearch, setFileSearch] = useState('');
- const [debouncedFileSearch, setDebouncedFileSearch] = useState('');
- useEffect(() => {
- const handler = setTimeout(() => setDebouncedSearch(search), 200);
- return () => clearTimeout(handler);
- }, [search]);
- useEffect(() => {
- const handler = setTimeout(() => setDebouncedSearch(fileSearch), 200);
- return () => clearTimeout(handler);
- }, [fileSearch]);
-
- const [sshSessionId, setSshSessionId] = useState(null);
- const [filesLoading, setFilesLoading] = useState(false);
- const [searchQuery, setSearchQuery] = useState('');
- const [connectingSSH, setConnectingSSH] = useState(false);
- const [connectionCache, setConnectionCache] = useState>({});
- const [fetchingFiles, setFetchingFiles] = useState(false);
-
- const [contextMenu, setContextMenu] = useState<{
- visible: boolean;
- x: number;
- y: number;
- item: any;
- }>({
- visible: false,
- x: 0,
- y: 0,
- item: null
- });
-
- const [renamingItem, setRenamingItem] = useState<{
- item: any;
- newName: string;
- } | null>(null);
-
- useEffect(() => {
- const nextPath = host?.defaultPath || '/';
- setCurrentPath(nextPath);
- onPathChange?.(nextPath);
- (async () => {
- await connectToSSH(host);
- })();
- }, [host?.id]);
-
- async function connectToSSH(server: SSHHost): Promise {
- const sessionId = server.id.toString();
-
- const cached = connectionCache[sessionId];
- if (cached && Date.now() - cached.timestamp < 30000) {
- setSshSessionId(cached.sessionId);
- return cached.sessionId;
- }
-
- if (connectingSSH) {
- return null;
- }
-
- setConnectingSSH(true);
-
- try {
- if (!server.password && !server.key) {
- toast.error(t('common.noAuthCredentials'));
- return null;
- }
-
- const connectionConfig = {
- ip: server.ip,
- port: server.port,
- username: server.username,
- password: server.password,
- sshKey: server.key,
- keyPassword: server.keyPassword,
- };
-
- await connectSSH(sessionId, connectionConfig);
-
- setSshSessionId(sessionId);
-
- setConnectionCache(prev => ({
- ...prev,
- [sessionId]: {sessionId, timestamp: Date.now()}
- }));
-
- return sessionId;
- } catch (err: any) {
- toast.error(err?.response?.data?.error || t('fileManager.failedToConnectSSH'));
- setSshSessionId(null);
- return null;
- } finally {
- setConnectingSSH(false);
- }
- }
-
- async function fetchFiles() {
- if (fetchingFiles) {
- return;
- }
-
- setFetchingFiles(true);
- setFiles([]);
- setFilesLoading(true);
-
- try {
- let pinnedFiles: any[] = [];
- try {
- if (host) {
- pinnedFiles = await getFileManagerPinned(host.id);
- }
- } catch (err) {
- }
-
- if (host && sshSessionId) {
- let res: any[] = [];
-
- try {
- const status = await getSSHStatus(sshSessionId);
- if (!status.connected) {
- const newSessionId = await connectToSSH(host);
- if (newSessionId) {
- setSshSessionId(newSessionId);
- res = await listSSHFiles(newSessionId, currentPath);
- } else {
- throw new Error(t('fileManager.failedToReconnectSSH'));
- }
- } else {
- res = await listSSHFiles(sshSessionId, currentPath);
- }
- } catch (sessionErr) {
- const newSessionId = await connectToSSH(host);
- if (newSessionId) {
- setSshSessionId(newSessionId);
- res = await listSSHFiles(newSessionId, currentPath);
- } else {
- throw sessionErr;
- }
- }
-
- const processedFiles = (res || []).map((f: any) => {
- const filePath = currentPath + (currentPath.endsWith('/') ? '' : '/') + f.name;
- const isPinned = pinnedFiles.some(pinned => pinned.path === filePath);
- return {
- ...f,
- path: filePath,
- isPinned,
- isSSH: true,
- sshSessionId: sshSessionId
- };
- });
-
- setFiles(processedFiles);
- }
- } catch (err: any) {
- setFiles([]);
- toast.error(err?.response?.data?.error || err?.message || t('fileManager.failedToListFiles'));
- } finally {
- setFilesLoading(false);
- setFetchingFiles(false);
- }
- }
-
- useEffect(() => {
- if (host && sshSessionId && !connectingSSH && !fetchingFiles) {
- const timeoutId = setTimeout(() => {
- fetchFiles();
- }, 100);
- return () => clearTimeout(timeoutId);
- }
- }, [currentPath, host, sshSessionId]);
-
- useImperativeHandle(ref, () => ({
- openFolder: async (_server: SSHHost, path: string) => {
- if (connectingSSH || fetchingFiles) {
- return;
- }
-
- if (currentPath === path) {
- setTimeout(() => fetchFiles(), 100);
- return;
- }
-
- setFetchingFiles(false);
- setFilesLoading(false);
- setFiles([]);
-
- setCurrentPath(path);
- onPathChange?.(path);
- if (!sshSessionId) {
- const sessionId = await connectToSSH(host);
- if (sessionId) setSshSessionId(sessionId);
- }
- },
- fetchFiles: () => {
- if (host && sshSessionId) {
- fetchFiles();
- }
- },
- getCurrentPath: () => currentPath
- }));
-
- useEffect(() => {
- if (pathInputRef.current) {
- pathInputRef.current.scrollLeft = pathInputRef.current.scrollWidth;
- }
- }, [currentPath]);
-
- const filteredFiles = files.filter(file => {
- const q = debouncedFileSearch.trim().toLowerCase();
- if (!q) return true;
- return file.name.toLowerCase().includes(q);
- });
-
- const handleContextMenu = (e: React.MouseEvent, item: any) => {
- e.preventDefault();
-
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
-
- const menuWidth = 160;
- const menuHeight = 80;
-
- let x = e.clientX;
- let y = e.clientY;
-
- if (x + menuWidth > viewportWidth) {
- x = e.clientX - menuWidth;
- }
-
- if (y + menuHeight > viewportHeight) {
- y = e.clientY - menuHeight;
- }
-
- if (x < 0) {
- x = 0;
- }
-
- if (y < 0) {
- y = 0;
- }
-
- setContextMenu({
- visible: true,
- x,
- y,
- item
- });
- };
-
- const closeContextMenu = () => {
- setContextMenu({ visible: false, x: 0, y: 0, item: null });
- };
-
- const handleRename = async (item: any, newName: string) => {
- if (!sshSessionId || !newName.trim() || newName === item.name) {
- setRenamingItem(null);
- return;
- }
-
- try {
- await renameSSHItem(sshSessionId, item.path, newName.trim());
- toast.success(`${item.type === 'directory' ? t('common.folder') : t('common.file')} ${t('common.renamedSuccessfully')}`);
- setRenamingItem(null);
- if (onOperationComplete) {
- onOperationComplete();
- } else {
- fetchFiles();
- }
- } catch (error: any) {
- toast.error(error?.response?.data?.error || t('fileManager.failedToRenameItem'));
- }
- };
-
- const handleDelete = async (item: any) => {
- if (!sshSessionId) return;
-
- try {
- await deleteSSHItem(sshSessionId, item.path, item.type === 'directory');
- toast.success(`${item.type === 'directory' ? t('common.folder') : t('common.file')} ${t('common.deletedSuccessfully')}`);
- if (onOperationComplete) {
- onOperationComplete();
- } else {
- fetchFiles();
- }
- } catch (error: any) {
- toast.error(error?.response?.data?.error || t('fileManager.failedToDeleteItem'));
- }
- };
-
- const startRename = (item: any) => {
- setRenamingItem({ item, newName: item.name });
- closeContextMenu();
- };
-
- const startDelete = (item: any) => {
- onDeleteItem?.(item);
- closeContextMenu();
- };
-
- useEffect(() => {
- const handleClickOutside = () => closeContextMenu();
- document.addEventListener('click', handleClickOutside);
- return () => document.removeEventListener('click', handleClickOutside);
- }, []);
-
- const handlePathChange = (newPath: string) => {
- setCurrentPath(newPath);
- onPathChange?.(newPath);
- };
-
- return (
-
-
-
- {host && (
-
-
-
{
- let path = currentPath;
- if (path && path !== '/' && path !== '') {
- if (path.endsWith('/')) path = path.slice(0, -1);
- const lastSlash = path.lastIndexOf('/');
- if (lastSlash > 0) {
- handlePathChange(path.slice(0, lastSlash));
- } else {
- handlePathChange('/');
- }
- } else {
- handlePathChange('/');
- }
- }}
- >
-
-
-
handlePathChange(e.target.value)}
- className="flex-1 bg-[#18181b] border-2 border-[#434345] text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-[#5a5a5d]"
- />
-
-
- setFileSearch(e.target.value)}
- />
-
-
-
-
- {connectingSSH || filesLoading ? (
-
{t('common.loading')}
- ) : filteredFiles.length === 0 ? (
-
{t('fileManager.noFilesOrFoldersFound')}
- ) : (
-
- {filteredFiles.map((item: any) => {
- const isOpen = (tabs || []).some((t: any) => t.id === item.path);
- const isRenaming = renamingItem?.item?.path === item.path;
- const isDeleting = false;
-
- return (
-
!isOpen && handleContextMenu(e, item)}
- >
- {isRenaming ? (
-
- {item.type === 'directory' ?
- :
- }
- setRenamingItem(prev => prev ? {...prev, newName: e.target.value} : null)}
- className="flex-1 h-6 text-sm bg-[#23232a] border border-[#434345] text-white"
- autoFocus
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- handleRename(item, renamingItem.newName);
- } else if (e.key === 'Escape') {
- setRenamingItem(null);
- }
- }}
- onBlur={() => handleRename(item, renamingItem.newName)}
- />
-
- ) : (
- <>
-
!isOpen && (item.type === 'directory' ? handlePathChange(item.path) : onOpenFile({
- name: item.name,
- path: item.path,
- isSSH: item.isSSH,
- sshSessionId: item.sshSessionId
- }))}
- >
- {item.type === 'directory' ?
- :
- }
- {item.name}
-
-
- {item.type === 'file' && (
-
{
- e.stopPropagation();
- try {
- if (item.isPinned) {
- await removeFileManagerPinned({
- name: item.name,
- path: item.path,
- hostId: host?.id,
- isSSH: true,
- sshSessionId: host?.id.toString()
- });
- setFiles(files.map(f =>
- f.path === item.path ? { ...f, isPinned: false } : f
- ));
- } else {
- await addFileManagerPinned({
- name: item.name,
- path: item.path,
- hostId: host?.id,
- isSSH: true,
- sshSessionId: host?.id.toString()
- });
- setFiles(files.map(f =>
- f.path === item.path ? { ...f, isPinned: true } : f
- ));
- }
- } catch (err) {
- }
- }}
- >
-
-
- )}
- {!isOpen && (
-
{
- e.stopPropagation();
- handleContextMenu(e, item);
- }}
- >
-
-
- )}
-
- >
- )}
-
- );
- })}
-
- )}
-
-
-
-
- )}
-
-
-
- {contextMenu.visible && contextMenu.item && (
-
- startRename(contextMenu.item)}
- >
-
- Rename
-
- startDelete(contextMenu.item)}
- >
-
- Delete
-
-
- )}
-
- );
-});
-
-export {FileManagerLeftSidebar};
\ No newline at end of file
diff --git a/src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx b/src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx
deleted file mode 100644
index c7392b46..00000000
--- a/src/ui/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import React from 'react';
-import {Button} from '@/components/ui/button.tsx';
-import {Card} from '@/components/ui/card.tsx';
-import {Separator} from '@/components/ui/separator.tsx';
-import {Plus, Folder, File, Star, Trash2, Edit, Link2, Server, Pin} from 'lucide-react';
-import {useTranslation} from 'react-i18next';
-
-interface SSHConnection {
- id: string;
- name: string;
- ip: string;
- port: number;
- username: string;
- isPinned?: boolean;
-}
-
-interface FileItem {
- name: string;
- type: 'file' | 'directory' | 'link';
- path: string;
- isStarred?: boolean;
-}
-
-interface FileManagerLeftSidebarVileViewerProps {
- sshConnections: SSHConnection[];
- onAddSSH: () => void;
- onConnectSSH: (conn: SSHConnection) => void;
- onEditSSH: (conn: SSHConnection) => void;
- onDeleteSSH: (conn: SSHConnection) => void;
- onPinSSH: (conn: SSHConnection) => void;
- currentPath: string;
- files: FileItem[];
- onOpenFile: (file: FileItem) => void;
- onOpenFolder: (folder: FileItem) => void;
- onStarFile: (file: FileItem) => void;
- onDeleteFile: (file: FileItem) => void;
- isLoading?: boolean;
- error?: string;
- isSSHMode: boolean;
- onSwitchToLocal: () => void;
- onSwitchToSSH: (conn: SSHConnection) => void;
- currentSSH?: SSHConnection;
-}
-
-export function FileManagerLeftSidebarFileViewer({
- sshConnections,
- onAddSSH,
- onConnectSSH,
- onEditSSH,
- onDeleteSSH,
- onPinSSH,
- currentPath,
- files,
- onOpenFile,
- onOpenFolder,
- onStarFile,
- onDeleteFile,
- isLoading,
- error,
- isSSHMode,
- onSwitchToLocal,
- onSwitchToSSH,
- currentSSH,
- }: FileManagerLeftSidebarVileViewerProps) {
- const {t} = useTranslation();
-
- return (
-
-
-
- {isSSHMode ? t('common.sshPath') : t('common.localPath')}
- {currentPath}
-
- {isLoading ? (
-
{t('common.loading')}
- ) : error ? (
-
{error}
- ) : (
-
- {files.map((item) => (
-
- item.type === 'directory' ? onOpenFolder(item) : onOpenFile(item)}>
- {item.type === 'directory' ? :
- }
- {item.name}
-
-
-
onStarFile(item)}>
-
-
-
onDeleteFile(item)}>
-
-
-
-
- ))}
- {files.length === 0 &&
-
No files or folders found.
}
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/File Manager/FileManagerOperations.tsx b/src/ui/Apps/File Manager/FileManagerOperations.tsx
deleted file mode 100644
index 4d71fc02..00000000
--- a/src/ui/Apps/File Manager/FileManagerOperations.tsx
+++ /dev/null
@@ -1,626 +0,0 @@
-import React, {useState, useRef, useEffect} from 'react';
-import {Button} from '@/components/ui/button.tsx';
-import {Input} from '@/components/ui/input.tsx';
-import {Card} from '@/components/ui/card.tsx';
-import {Separator} from '@/components/ui/separator.tsx';
-import {
- Upload,
- FilePlus,
- FolderPlus,
- Trash2,
- Edit3,
- X,
- Check,
- AlertCircle,
- FileText,
- Folder
-} from 'lucide-react';
-import {cn} from '@/lib/utils.ts';
-import {useTranslation} from 'react-i18next';
-
-interface FileManagerOperationsProps {
- currentPath: string;
- sshSessionId: string | null;
- onOperationComplete: () => void;
- onError: (error: string) => void;
- onSuccess: (message: string) => void;
-}
-
-export function FileManagerOperations({
- currentPath,
- sshSessionId,
- onOperationComplete,
- onError,
- onSuccess
- }: FileManagerOperationsProps) {
- const {t} = useTranslation();
- const [showUpload, setShowUpload] = useState(false);
- const [showCreateFile, setShowCreateFile] = useState(false);
- const [showCreateFolder, setShowCreateFolder] = useState(false);
- const [showDelete, setShowDelete] = useState(false);
- const [showRename, setShowRename] = useState(false);
-
- const [uploadFile, setUploadFile] = useState(null);
- const [newFileName, setNewFileName] = useState('');
- const [newFolderName, setNewFolderName] = useState('');
- const [deletePath, setDeletePath] = useState('');
- const [deleteIsDirectory, setDeleteIsDirectory] = useState(false);
- const [renamePath, setRenamePath] = useState('');
- const [renameIsDirectory, setRenameIsDirectory] = useState(false);
- const [newName, setNewName] = useState('');
-
- const [isLoading, setIsLoading] = useState(false);
- const [showTextLabels, setShowTextLabels] = useState(true);
- const fileInputRef = useRef(null);
- const containerRef = useRef(null);
-
- useEffect(() => {
- const checkContainerWidth = () => {
- if (containerRef.current) {
- const width = containerRef.current.offsetWidth;
- setShowTextLabels(width > 240);
- }
- };
-
- checkContainerWidth();
-
- const resizeObserver = new ResizeObserver(checkContainerWidth);
- if (containerRef.current) {
- resizeObserver.observe(containerRef.current);
- }
-
- return () => {
- resizeObserver.disconnect();
- };
- }, []);
-
- const handleFileUpload = async () => {
- if (!uploadFile || !sshSessionId) return;
-
- setIsLoading(true);
- try {
- const content = await uploadFile.text();
- const {uploadSSHFile} = await import('@/ui/main-axios.ts');
-
- await uploadSSHFile(sshSessionId, currentPath, uploadFile.name, content);
- onSuccess(t('fileManager.fileUploadedSuccessfully', { name: uploadFile.name }));
- setShowUpload(false);
- setUploadFile(null);
- onOperationComplete();
- } catch (error: any) {
- onError(error?.response?.data?.error || t('fileManager.failedToUploadFile'));
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleCreateFile = async () => {
- if (!newFileName.trim() || !sshSessionId) return;
-
- setIsLoading(true);
- try {
- const {createSSHFile} = await import('@/ui/main-axios.ts');
-
- await createSSHFile(sshSessionId, currentPath, newFileName.trim());
- onSuccess(t('fileManager.fileCreatedSuccessfully', { name: newFileName.trim() }));
- setShowCreateFile(false);
- setNewFileName('');
- onOperationComplete();
- } catch (error: any) {
- onError(error?.response?.data?.error || t('fileManager.failedToCreateFile'));
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleCreateFolder = async () => {
- if (!newFolderName.trim() || !sshSessionId) return;
-
- setIsLoading(true);
- try {
- const {createSSHFolder} = await import('@/ui/main-axios.ts');
-
- await createSSHFolder(sshSessionId, currentPath, newFolderName.trim());
- onSuccess(t('fileManager.folderCreatedSuccessfully', { name: newFolderName.trim() }));
- setShowCreateFolder(false);
- setNewFolderName('');
- onOperationComplete();
- } catch (error: any) {
- onError(error?.response?.data?.error || t('fileManager.failedToCreateFolder'));
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleDelete = async () => {
- if (!deletePath || !sshSessionId) return;
-
- setIsLoading(true);
- try {
- const {deleteSSHItem} = await import('@/ui/main-axios.ts');
-
- await deleteSSHItem(sshSessionId, deletePath, deleteIsDirectory);
- onSuccess(t('fileManager.itemDeletedSuccessfully', { type: deleteIsDirectory ? t('fileManager.folder') : t('fileManager.file') }));
- setShowDelete(false);
- setDeletePath('');
- setDeleteIsDirectory(false);
- onOperationComplete();
- } catch (error: any) {
- onError(error?.response?.data?.error || t('fileManager.failedToDeleteItem'));
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleRename = async () => {
- if (!renamePath || !newName.trim() || !sshSessionId) return;
-
- setIsLoading(true);
- try {
- const {renameSSHItem} = await import('@/ui/main-axios.ts');
-
- await renameSSHItem(sshSessionId, renamePath, newName.trim());
- onSuccess(t('fileManager.itemRenamedSuccessfully', { type: renameIsDirectory ? t('fileManager.folder') : t('fileManager.file') }));
- setShowRename(false);
- setRenamePath('');
- setRenameIsDirectory(false);
- setNewName('');
- onOperationComplete();
- } catch (error: any) {
- onError(error?.response?.data?.error || t('fileManager.failedToRenameItem'));
- } finally {
- setIsLoading(false);
- }
- };
-
- const openFileDialog = () => {
- fileInputRef.current?.click();
- };
-
- const handleFileSelect = (event: React.ChangeEvent) => {
- const file = event.target.files?.[0];
- if (file) {
- setUploadFile(file);
- }
- };
-
- const resetStates = () => {
- setShowUpload(false);
- setShowCreateFile(false);
- setShowCreateFolder(false);
- setShowDelete(false);
- setShowRename(false);
- setUploadFile(null);
- setNewFileName('');
- setNewFolderName('');
- setDeletePath('');
- setDeleteIsDirectory(false);
- setRenamePath('');
- setRenameIsDirectory(false);
- setNewName('');
- };
-
- if (!sshSessionId) {
- return (
-
-
-
{t('fileManager.connectToSsh')}
-
- );
- }
-
- return (
-
-
- setShowUpload(true)}
- className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
- title={t('fileManager.uploadFile')}
- >
-
- {showTextLabels && {t('fileManager.uploadFile')} }
-
- setShowCreateFile(true)}
- className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
- title={t('fileManager.newFile')}
- >
-
- {showTextLabels && {t('fileManager.newFile')} }
-
- setShowCreateFolder(true)}
- className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
- title={t('fileManager.newFolder')}
- >
-
- {showTextLabels && {t('fileManager.newFolder')} }
-
- setShowRename(true)}
- className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30]"
- title={t('fileManager.rename')}
- >
-
- {showTextLabels && {t('fileManager.rename')} }
-
- setShowDelete(true)}
- className="h-10 bg-[#18181b] border-2 border-[#303032] hover:border-[#434345] hover:bg-[#2d2d30] col-span-2"
- title={t('fileManager.deleteItem')}
- >
-
- {showTextLabels && {t('fileManager.deleteItem')} }
-
-
-
-
-
-
-
- {t('fileManager.currentPath')}:
- {currentPath}
-
-
-
-
-
-
- {showUpload && (
-
-
-
-
-
- {t('fileManager.uploadFileTitle')}
-
-
- {t('fileManager.maxFileSize')}
-
-
-
setShowUpload(false)}
- className="h-8 w-8 p-0 flex-shrink-0 ml-2"
- >
-
-
-
-
-
-
- {uploadFile ? (
-
-
-
{uploadFile.name}
-
- {(uploadFile.size / 1024).toFixed(2)} KB
-
-
setUploadFile(null)}
- className="w-full text-sm h-8"
- >
- {t('fileManager.removeFile')}
-
-
- ) : (
-
-
-
{t('fileManager.clickToSelectFile')}
-
- {t('fileManager.chooseFile')}
-
-
- )}
-
-
-
-
-
-
- {isLoading ? t('fileManager.uploading') : t('fileManager.uploadFile')}
-
- setShowUpload(false)}
- disabled={isLoading}
- className="w-full text-sm h-9"
- >
- {t('common.cancel')}
-
-
-
-
- )}
-
- {showCreateFile && (
-
-
-
-
-
- {t('fileManager.createNewFile')}
-
-
-
setShowCreateFile(false)}
- className="h-8 w-8 p-0 flex-shrink-0 ml-2"
- >
-
-
-
-
-
-
-
- {t('fileManager.fileName')}
-
- setNewFileName(e.target.value)}
- placeholder={t('placeholders.fileName')}
- className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
- onKeyDown={(e) => e.key === 'Enter' && handleCreateFile()}
- />
-
-
-
-
- {isLoading ? t('fileManager.creating') : t('fileManager.createFile')}
-
- setShowCreateFile(false)}
- disabled={isLoading}
- className="w-full text-sm h-9"
- >
- {t('common.cancel')}
-
-
-
-
- )}
-
- {showCreateFolder && (
-
-
-
-
-
- {t('fileManager.createNewFolder')}
-
-
-
setShowCreateFolder(false)}
- className="h-8 w-8 p-0 flex-shrink-0 ml-2"
- >
-
-
-
-
-
-
-
- {t('fileManager.folderName')}
-
- setNewFolderName(e.target.value)}
- placeholder={t('placeholders.folderName')}
- className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
- onKeyDown={(e) => e.key === 'Enter' && handleCreateFolder()}
- />
-
-
-
-
- {isLoading ? t('fileManager.creating') : t('fileManager.createFolder')}
-
- setShowCreateFolder(false)}
- disabled={isLoading}
- className="w-full text-sm h-9"
- >
- {t('common.cancel')}
-
-
-
-
- )}
-
- {showDelete && (
-
-
-
-
-
- {t('fileManager.deleteItem')}
-
-
-
setShowDelete(false)}
- className="h-8 w-8 p-0 flex-shrink-0 ml-2"
- >
-
-
-
-
-
-
-
-
-
{t('fileManager.warningCannotUndo')}
-
-
-
-
-
- {t('fileManager.itemPath')}
-
- setDeletePath(e.target.value)}
- placeholder={t('placeholders.fullPath')}
- className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
- />
-
-
-
- setDeleteIsDirectory(e.target.checked)}
- className="rounded border-[#434345] bg-[#23232a] mt-0.5 flex-shrink-0"
- />
-
- {t('fileManager.thisIsDirectory')}
-
-
-
-
-
- {isLoading ? t('fileManager.deleting') : t('fileManager.deleteItem')}
-
- setShowDelete(false)}
- disabled={isLoading}
- className="w-full text-sm h-9"
- >
- {t('common.cancel')}
-
-
-
-
- )}
-
- {showRename && (
-
-
-
-
-
- {t('fileManager.renameItem')}
-
-
-
setShowRename(false)}
- className="h-8 w-8 p-0 flex-shrink-0 ml-2"
- >
-
-
-
-
-
-
-
- {t('fileManager.currentPathLabel')}
-
- setRenamePath(e.target.value)}
- placeholder={t('placeholders.currentPath')}
- className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
- />
-
-
-
-
- {t('fileManager.newName')}
-
- setNewName(e.target.value)}
- placeholder={t('placeholders.newName')}
- className="bg-[#23232a] border-2 border-[#434345] text-white text-sm"
- onKeyDown={(e) => e.key === 'Enter' && handleRename()}
- />
-
-
-
- setRenameIsDirectory(e.target.checked)}
- className="rounded border-[#434345] bg-[#23232a] mt-0.5 flex-shrink-0"
- />
-
- {t('fileManager.thisIsDirectoryRename')}
-
-
-
-
-
- {isLoading ? t('fileManager.renaming') : t('fileManager.renameItem')}
-
- setShowRename(false)}
- disabled={isLoading}
- className="w-full text-sm h-9"
- >
- {t('common.cancel')}
-
-
-
-
- )}
-
- );
-}
diff --git a/src/ui/Apps/File Manager/FileManagerTabList.tsx b/src/ui/Apps/File Manager/FileManagerTabList.tsx
deleted file mode 100644
index e46a7e22..00000000
--- a/src/ui/Apps/File Manager/FileManagerTabList.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react';
-import {Button} from '@/components/ui/button.tsx';
-import {X, Home} from 'lucide-react';
-
-interface FileManagerTab {
- id: string | number;
- title: string;
-}
-
-interface FileManagerTabList {
- tabs: FileManagerTab[];
- activeTab: string | number;
- setActiveTab: (tab: string | number) => void;
- closeTab: (tab: string | number) => void;
- onHomeClick: () => void;
-}
-
-export function FileManagerTabList({tabs, activeTab, setActiveTab, closeTab, onHomeClick}: FileManagerTabList) {
- return (
-
-
-
-
- {tabs.map((tab) => {
- const isActive = tab.id === activeTab;
- return (
-
- setActiveTab(tab.id)}
- variant="outline"
- className={`h-8 rounded-r-none !px-2 border-1 border-[#303032] ${isActive ? '!bg-[#1d1d1f] !text-white !border-[#2d2d30] !hover:bg-[#1d1d1f] !active:bg-[#1d1d1f] !focus:bg-[#1d1d1f] !hover:text-white !active:text-white !focus:text-white' : ''}`}
- >
- {tab.title}
-
-
- closeTab(tab.id)}
- variant="outline"
- className="h-8 rounded-l-none p-0 !w-9 border-1 border-[#303032]"
- >
-
-
-
- );
- })}
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/Host Manager/HostManager.tsx b/src/ui/Apps/Host Manager/HostManager.tsx
deleted file mode 100644
index 9a53545e..00000000
--- a/src/ui/Apps/Host Manager/HostManager.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import React, {useState} from "react";
-import {HostManagerHostViewer} from "@/ui/Apps/Host Manager/HostManagerHostViewer.tsx"
-import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
-import {Separator} from "@/components/ui/separator.tsx";
-import {HostManagerHostEditor} from "@/ui/Apps/Host Manager/HostManagerHostEditor.tsx";
-import {useSidebar} from "@/components/ui/sidebar.tsx";
-import {useTranslation} from "react-i18next";
-
-interface HostManagerProps {
- onSelectView: (view: string) => void;
- isTopbarOpen?: 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: any[];
- createdAt: string;
- updatedAt: string;
-}
-
-export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): React.ReactElement {
- const {t} = useTranslation();
- const [activeTab, setActiveTab] = useState("host_viewer");
- const [editingHost, setEditingHost] = useState(null);
- const {state: sidebarState} = useSidebar();
-
- const handleEditHost = (host: SSHHost) => {
- setEditingHost(host);
- setActiveTab("add_host");
- };
-
- const handleFormSubmit = () => {
- setEditingHost(null);
- setActiveTab("host_viewer");
- };
-
- const handleTabChange = (value: string) => {
- setActiveTab(value);
- if (value === "host_viewer") {
- setEditingHost(null);
- }
- };
-
- const topMarginPx = isTopbarOpen ? 74 : 26;
- const leftMarginPx = sidebarState === 'collapsed' ? 26 : 8;
- const bottomMarginPx = 8;
-
- return (
-
-
-
-
-
- {t('hosts.hostViewer')}
-
- {editingHost ? t('hosts.editHost') : t('hosts.addHost')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx b/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx
deleted file mode 100644
index a00a15f0..00000000
--- a/src/ui/Apps/Host Manager/HostManagerHostEditor.tsx
+++ /dev/null
@@ -1,1038 +0,0 @@
-import {zodResolver} from "@hookform/resolvers/zod"
-import {Controller, useForm} from "react-hook-form"
-import {z} from "zod"
-
-import {Button} from "@/components/ui/button.tsx"
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form.tsx";
-import {Input} from "@/components/ui/input.tsx";
-import {ScrollArea} from "@/components/ui/scroll-area"
-import {Separator} from "@/components/ui/separator.tsx";
-import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
-import React, {useEffect, useRef, useState} from "react";
-import {Switch} from "@/components/ui/switch.tsx";
-import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
-import {toast} from "sonner";
-import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
-import {useTranslation} from "react-i18next";
-
-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: any[];
- createdAt: string;
- updatedAt: string;
-}
-
-interface SSHManagerHostEditorProps {
- editingHost?: SSHHost | null;
- onFormSubmit?: () => void;
-}
-
-export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHostEditorProps) {
- const {t} = useTranslation();
- const [hosts, setHosts] = useState([]);
- const [folders, setFolders] = useState([]);
- const [sshConfigurations, setSshConfigurations] = useState([]);
- const [loading, setLoading] = useState(true);
-
- const [authTab, setAuthTab] = useState<'password' | 'key'>('password');
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- setLoading(true);
- const hostsData = await getSSHHosts();
- setHosts(hostsData);
-
- const uniqueFolders = [...new Set(
- hostsData
- .filter(host => host.folder && host.folder.trim() !== '')
- .map(host => host.folder)
- )].sort();
-
- const uniqueConfigurations = [...new Set(
- hostsData
- .filter(host => host.name && host.name.trim() !== '')
- .map(host => host.name)
- )].sort();
-
- setFolders(uniqueFolders);
- setSshConfigurations(uniqueConfigurations);
- } catch (error) {
- } finally {
- setLoading(false);
- }
- };
-
- fetchData();
- }, []);
-
- const formSchema = z.object({
- name: z.string().optional(),
- ip: z.string().min(1),
- port: z.coerce.number().min(1).max(65535),
- username: z.string().min(1),
- folder: z.string().optional(),
- tags: z.array(z.string().min(1)).default([]),
- pin: z.boolean().default(false),
- authType: z.enum(['password', 'key']),
- password: z.string().optional(),
- key: z.instanceof(File).optional().nullable(),
- keyPassword: z.string().optional(),
- keyType: z.enum([
- 'auto',
- 'ssh-rsa',
- 'ssh-ed25519',
- 'ecdsa-sha2-nistp256',
- 'ecdsa-sha2-nistp384',
- 'ecdsa-sha2-nistp521',
- 'ssh-dss',
- 'ssh-rsa-sha2-256',
- 'ssh-rsa-sha2-512',
- ]).optional(),
- enableTerminal: z.boolean().default(true),
- enableTunnel: z.boolean().default(true),
- tunnelConnections: z.array(z.object({
- sourcePort: z.coerce.number().min(1).max(65535),
- endpointPort: z.coerce.number().min(1).max(65535),
- endpointHost: z.string().min(1),
- maxRetries: z.coerce.number().min(0).max(100).default(3),
- retryInterval: z.coerce.number().min(1).max(3600).default(10),
- autoStart: z.boolean().default(false),
- })).default([]),
- enableFileManager: z.boolean().default(true),
- defaultPath: z.string().optional(),
- }).superRefine((data, ctx) => {
- if (data.authType === 'password') {
- if (!data.password || data.password.trim() === '') {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('hosts.passwordRequired'),
- path: ['password']
- });
- }
- } else if (data.authType === 'key') {
- if (!data.key) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('hosts.sshKeyRequired'),
- path: ['key']
- });
- }
- if (!data.keyType) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('hosts.keyTypeRequired'),
- path: ['keyType']
- });
- }
- }
-
- data.tunnelConnections.forEach((connection, index) => {
- if (connection.endpointHost && !sshConfigurations.includes(connection.endpointHost)) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t('hosts.mustSelectValidSshConfig'),
- path: ['tunnelConnections', index, 'endpointHost']
- });
- }
- });
- });
-
- type FormData = z.infer;
-
- const form = useForm({
- resolver: zodResolver(formSchema) as any,
- defaultValues: {
- name: editingHost?.name || "",
- ip: editingHost?.ip || "",
- port: editingHost?.port || 22,
- username: editingHost?.username || "",
- folder: editingHost?.folder || "",
- tags: editingHost?.tags || [],
- pin: editingHost?.pin || false,
- authType: (editingHost?.authType as 'password' | 'key') || "password",
- password: "",
- key: null,
- keyPassword: "",
- keyType: "auto",
- enableTerminal: editingHost?.enableTerminal !== false,
- enableTunnel: editingHost?.enableTunnel !== false,
- enableFileManager: editingHost?.enableFileManager !== false,
- defaultPath: editingHost?.defaultPath || "/",
- tunnelConnections: editingHost?.tunnelConnections || [],
- }
- });
-
- useEffect(() => {
- if (editingHost) {
- const defaultAuthType = editingHost.key ? 'key' : 'password';
-
- setAuthTab(defaultAuthType);
-
- form.reset({
- name: editingHost.name || "",
- ip: editingHost.ip || "",
- port: editingHost.port || 22,
- username: editingHost.username || "",
- folder: editingHost.folder || "",
- tags: editingHost.tags || [],
- pin: editingHost.pin || false,
- authType: defaultAuthType,
- password: editingHost.password || "",
- key: editingHost.key ? new File([editingHost.key], "key.pem") : null,
- keyPassword: editingHost.keyPassword || "",
- keyType: (editingHost.keyType as any) || "auto",
- enableTerminal: editingHost.enableTerminal !== false,
- enableTunnel: editingHost.enableTunnel !== false,
- enableFileManager: editingHost.enableFileManager !== false,
- defaultPath: editingHost.defaultPath || "/",
- tunnelConnections: editingHost.tunnelConnections || [],
- });
- } else {
- setAuthTab('password');
-
- form.reset({
- name: "",
- ip: "",
- port: 22,
- username: "",
- folder: "",
- tags: [],
- pin: false,
- authType: "password",
- password: "",
- key: null,
- keyPassword: "",
- keyType: "auto",
- enableTerminal: true,
- enableTunnel: true,
- enableFileManager: true,
- defaultPath: "/",
- tunnelConnections: [],
- });
- }
- }, [editingHost, form]);
-
- const onSubmit = async (data: any) => {
- try {
- const formData = data as FormData;
-
- if (!formData.name || formData.name.trim() === '') {
- formData.name = `${formData.username}@${formData.ip}`;
- }
-
- if (editingHost) {
- await updateSSHHost(editingHost.id, formData);
- toast.success(t('hosts.hostUpdatedSuccessfully', { name: formData.name }));
- } else {
- await createSSHHost(formData);
- toast.success(t('hosts.hostAddedSuccessfully', { name: formData.name }));
- }
-
- if (onFormSubmit) {
- onFormSubmit();
- }
-
- window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
- } catch (error) {
- toast.error(t('hosts.failedToSaveHost'));
- }
- };
-
- const [tagInput, setTagInput] = useState("");
-
- const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
- const folderInputRef = useRef(null);
- const folderDropdownRef = useRef(null);
-
- const folderValue = form.watch('folder');
- const filteredFolders = React.useMemo(() => {
- if (!folderValue) return folders;
- return folders.filter(f => f.toLowerCase().includes(folderValue.toLowerCase()));
- }, [folderValue, folders]);
-
- const handleFolderClick = (folder: string) => {
- form.setValue('folder', folder);
- setFolderDropdownOpen(false);
- };
-
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (
- folderDropdownRef.current &&
- !folderDropdownRef.current.contains(event.target as Node) &&
- folderInputRef.current &&
- !folderInputRef.current.contains(event.target as Node)
- ) {
- setFolderDropdownOpen(false);
- }
- }
-
- if (folderDropdownOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- } else {
- document.removeEventListener('mousedown', handleClickOutside);
- }
-
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [folderDropdownOpen]);
-
- const keyTypeOptions = [
- {value: 'auto', label: t('hosts.autoDetect')},
- {value: 'ssh-rsa', label: t('hosts.rsa')},
- {value: 'ssh-ed25519', label: t('hosts.ed25519')},
- {value: 'ecdsa-sha2-nistp256', label: t('hosts.ecdsaNistP256')},
- {value: 'ecdsa-sha2-nistp384', label: t('hosts.ecdsaNistP384')},
- {value: 'ecdsa-sha2-nistp521', label: t('hosts.ecdsaNistP521')},
- {value: 'ssh-dss', label: t('hosts.dsa')},
- {value: 'ssh-rsa-sha2-256', label: t('hosts.rsaSha2256')},
- {value: 'ssh-rsa-sha2-512', label: t('hosts.rsaSha2512')},
- ];
-
- const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false);
- const keyTypeButtonRef = useRef(null);
- const keyTypeDropdownRef = useRef(null);
-
- useEffect(() => {
- function onClickOutside(event: MouseEvent) {
- if (
- keyTypeDropdownOpen &&
- keyTypeDropdownRef.current &&
- !keyTypeDropdownRef.current.contains(event.target as Node) &&
- keyTypeButtonRef.current &&
- !keyTypeButtonRef.current.contains(event.target as Node)
- ) {
- setKeyTypeDropdownOpen(false);
- }
- }
-
- document.addEventListener("mousedown", onClickOutside);
- return () => document.removeEventListener("mousedown", onClickOutside);
- }, [keyTypeDropdownOpen]);
-
- const [sshConfigDropdownOpen, setSshConfigDropdownOpen] = useState<{ [key: number]: boolean }>({});
- const sshConfigInputRefs = useRef<{ [key: number]: HTMLInputElement | null }>({});
- const sshConfigDropdownRefs = useRef<{ [key: number]: HTMLDivElement | null }>({});
-
- const getFilteredSshConfigs = (index: number) => {
- const value = form.watch(`tunnelConnections.${index}.endpointHost`);
-
- const currentHostName = form.watch('name') || `${form.watch('username')}@${form.watch('ip')}`;
-
- let filtered = sshConfigurations.filter(config => config !== currentHostName);
-
- if (value) {
- filtered = filtered.filter(config =>
- config.toLowerCase().includes(value.toLowerCase())
- );
- }
-
- return filtered;
- };
-
- const handleSshConfigClick = (config: string, index: number) => {
- form.setValue(`tunnelConnections.${index}.endpointHost`, config);
- setSshConfigDropdownOpen(prev => ({...prev, [index]: false}));
- };
-
- useEffect(() => {
- function handleSshConfigClickOutside(event: MouseEvent) {
- const openDropdowns = Object.keys(sshConfigDropdownOpen).filter(key => sshConfigDropdownOpen[parseInt(key)]);
-
- openDropdowns.forEach((indexStr: string) => {
- const index = parseInt(indexStr);
- if (
- sshConfigDropdownRefs.current[index] &&
- !sshConfigDropdownRefs.current[index]?.contains(event.target as Node) &&
- sshConfigInputRefs.current[index] &&
- !sshConfigInputRefs.current[index]?.contains(event.target as Node)
- ) {
- setSshConfigDropdownOpen(prev => ({...prev, [index]: false}));
- }
- });
- }
-
- const hasOpenDropdowns = Object.values(sshConfigDropdownOpen).some(open => open);
-
- if (hasOpenDropdowns) {
- document.addEventListener('mousedown', handleSshConfigClickOutside);
- } else {
- document.removeEventListener('mousedown', handleSshConfigClickOutside);
- }
-
- return () => {
- document.removeEventListener('mousedown', handleSshConfigClickOutside);
- };
- }, [sshConfigDropdownOpen]);
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx b/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx
deleted file mode 100644
index 33574649..00000000
--- a/src/ui/Apps/Host Manager/HostManagerHostViewer.tsx
+++ /dev/null
@@ -1,489 +0,0 @@
-import React, {useState, useEffect, useMemo} from "react";
-import {Card, CardContent} from "@/components/ui/card";
-import {Button} from "@/components/ui/button";
-import {Badge} from "@/components/ui/badge";
-import {ScrollArea} from "@/components/ui/scroll-area";
-import {Input} from "@/components/ui/input";
-import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from "@/components/ui/accordion";
-import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
-import {getSSHHosts, deleteSSHHost, bulkImportSSHHosts} from "@/ui/main-axios.ts";
-import {toast} from "sonner";
-import {useTranslation} from "react-i18next";
-import {
- Edit,
- Trash2,
- Server,
- Folder,
- Tag,
- Pin,
- Terminal,
- Network,
- FileEdit,
- Search,
- Upload,
- Info
-} from "lucide-react";
-import {Separator} from "@/components/ui/separator.tsx";
-
-interface SSHHost {
- id: number;
- name: string;
- ip: string;
- port: number;
- username: string;
- folder: string;
- tags: string[];
- pin: boolean;
- authType: string;
- enableTerminal: boolean;
- enableTunnel: boolean;
- enableFileManager: boolean;
- defaultPath: string;
- tunnelConnections: any[];
- createdAt: string;
- updatedAt: string;
-}
-
-interface SSHManagerHostViewerProps {
- onEditHost?: (host: SSHHost) => void;
-}
-
-export function HostManagerHostViewer({onEditHost}: SSHManagerHostViewerProps) {
- const {t} = useTranslation();
- const [hosts, setHosts] = useState([]);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [searchQuery, setSearchQuery] = useState("");
- const [importing, setImporting] = useState(false);
-
- useEffect(() => {
- fetchHosts();
- }, []);
-
- const fetchHosts = async () => {
- try {
- setLoading(true);
- const data = await getSSHHosts();
- setHosts(data);
- setError(null);
- } catch (err) {
- setError(t('hosts.failedToLoadHosts'));
- } finally {
- setLoading(false);
- }
- };
-
- const handleDelete = async (hostId: number, hostName: string) => {
- if (window.confirm(t('hosts.confirmDelete', { name: hostName }))) {
- try {
- await deleteSSHHost(hostId);
- toast.success(t('hosts.hostDeletedSuccessfully', { name: hostName }));
- await fetchHosts();
- window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
- } catch (err) {
- toast.error(t('hosts.failedToDeleteHost'));
- }
- }
- };
-
- const handleEdit = (host: SSHHost) => {
- if (onEditHost) {
- onEditHost(host);
- }
- };
-
- const handleJsonImport = async (event: React.ChangeEvent) => {
- const file = event.target.files?.[0];
- if (!file) return;
-
- try {
- setImporting(true);
- const text = await file.text();
- const data = JSON.parse(text);
-
- if (!Array.isArray(data.hosts) && !Array.isArray(data)) {
- throw new Error(t('hosts.jsonMustContainHosts'));
- }
-
- const hostsArray = Array.isArray(data.hosts) ? data.hosts : data;
-
- if (hostsArray.length === 0) {
- throw new Error(t('hosts.noHostsInJson'));
- }
-
- if (hostsArray.length > 100) {
- throw new Error(t('hosts.maxHostsAllowed'));
- }
-
- const result = await bulkImportSSHHosts(hostsArray);
-
- if (result.success > 0) {
- toast.success(t('hosts.importCompleted', { success: result.success, failed: result.failed }));
- if (result.errors.length > 0) {
- toast.error(`Import errors: ${result.errors.join(', ')}`);
- }
- await fetchHosts();
- window.dispatchEvent(new CustomEvent('ssh-hosts:changed'));
- } else {
- toast.error(t('hosts.importFailed') + `: ${result.errors.join(', ')}`);
- }
-
- } catch (err) {
- const errorMessage = err instanceof Error ? err.message : t('hosts.failedToImportJson');
- toast.error(t('hosts.importError') + `: ${errorMessage}`);
- } finally {
- setImporting(false);
- event.target.value = '';
- }
- };
-
- const filteredAndSortedHosts = useMemo(() => {
- let filtered = hosts;
-
- if (searchQuery.trim()) {
- const query = searchQuery.toLowerCase();
- filtered = hosts.filter(host => {
- const searchableText = [
- host.name || '',
- host.username,
- host.ip,
- host.folder || '',
- ...(host.tags || []),
- host.authType,
- host.defaultPath || ''
- ].join(' ').toLowerCase();
- return searchableText.includes(query);
- });
- }
-
- return filtered.sort((a, b) => {
- if (a.pin && !b.pin) return -1;
- if (!a.pin && b.pin) return 1;
-
- const aName = a.name || a.username;
- const bName = b.name || b.username;
- return aName.localeCompare(bName);
- });
- }, [hosts, searchQuery]);
-
- const hostsByFolder = useMemo(() => {
- const grouped: { [key: string]: SSHHost[] } = {};
-
- filteredAndSortedHosts.forEach(host => {
- const folder = host.folder || t('hosts.uncategorized');
- if (!grouped[folder]) {
- grouped[folder] = [];
- }
- grouped[folder].push(host);
- });
-
- const sortedFolders = Object.keys(grouped).sort((a, b) => {
- if (a === t('hosts.uncategorized')) return -1;
- if (b === t('hosts.uncategorized')) return 1;
- return a.localeCompare(b);
- });
-
- const sortedGrouped: { [key: string]: SSHHost[] } = {};
- sortedFolders.forEach(folder => {
- sortedGrouped[folder] = grouped[folder];
- });
-
- return sortedGrouped;
- }, [filteredAndSortedHosts]);
-
- if (loading) {
- return (
-
-
-
-
{t('hosts.loadingHosts')}
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
{error}
-
- {t('hosts.retry')}
-
-
-
- );
- }
-
- if (hosts.length === 0) {
- return (
-
-
-
-
{t('hosts.noHosts')}
-
- {t('hosts.noHostsMessage')}
-
-
-
- );
- }
-
- return (
-
-
-
-
{t('hosts.sshHosts')}
-
- {t('hosts.hostsCount', { count: filteredAndSortedHosts.length })}
-
-
-
-
-
-
- document.getElementById('json-import-input')?.click()}
- disabled={importing}
- >
- {importing ? t('hosts.importing') : t('hosts.importJson')}
-
-
-
-
-
{t('hosts.importJsonTitle')}
-
- {t('hosts.importJsonDesc')}
-
-
-
-
-
-
-
{
- const sampleData = {
- hosts: [
- {
- name: "Web Server - Production",
- ip: "192.168.1.100",
- port: 22,
- username: "admin",
- authType: "password",
- password: "your_secure_password_here",
- folder: "Production",
- tags: ["web", "production", "nginx"],
- pin: true,
- enableTerminal: true,
- enableTunnel: false,
- enableFileManager: true,
- defaultPath: "/var/www"
- },
- {
- name: "Database Server",
- ip: "192.168.1.101",
- port: 22,
- username: "dbadmin",
- authType: "key",
- key: "-----BEGIN OPENSSH PRIVATE KEY-----\nYour SSH private key content here\n-----END OPENSSH PRIVATE KEY-----",
- keyPassword: "optional_key_passphrase",
- keyType: "ssh-ed25519",
- folder: "Production",
- tags: ["database", "production", "postgresql"],
- pin: false,
- enableTerminal: true,
- enableTunnel: true,
- enableFileManager: false,
- tunnelConnections: [
- {
- sourcePort: 5432,
- endpointPort: 5432,
- endpointHost: "Web Server - Production",
- maxRetries: 3,
- retryInterval: 10,
- autoStart: true
- }
- ]
- }
- ]
- };
-
- const blob = new Blob([JSON.stringify(sampleData, null, 2)], {type: 'application/json'});
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = 'sample-ssh-hosts.json';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }}
- >
- {t('hosts.downloadSample')}
-
-
-
{
- window.open('https://docs.termix.site/json-import', '_blank');
- }}
- >
- {t('hosts.formatGuide')}
-
-
-
-
-
- {t('hosts.refresh')}
-
-
-
-
-
-
-
-
- setSearchQuery(e.target.value)}
- className="pl-10"
- />
-
-
-
-
- {Object.entries(hostsByFolder).map(([folder, folderHosts]) => (
-
-
-
-
-
-
- {folder}
-
- {folderHosts.length}
-
-
-
-
-
- {folderHosts.map((host) => (
-
handleEdit(host)}
- >
-
-
-
- {host.pin &&
}
-
- {host.name || `${host.username}@${host.ip}`}
-
-
-
- {host.ip}:{host.port}
-
-
- {host.username}
-
-
-
- {
- e.stopPropagation();
- handleEdit(host);
- }}
- className="h-5 w-5 p-0"
- >
-
-
- {
- e.stopPropagation();
- handleDelete(host.id, host.name || `${host.username}@${host.ip}`);
- }}
- className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
- >
-
-
-
-
-
-
- {host.tags && host.tags.length > 0 && (
-
- {host.tags.slice(0, 6).map((tag, index) => (
-
-
- {tag}
-
- ))}
- {host.tags.length > 6 && (
-
- +{host.tags.length - 6}
-
- )}
-
- )}
-
-
- {host.enableTerminal && (
-
-
- {t('hosts.terminalBadge')}
-
- )}
- {host.enableTunnel && (
-
-
- {t('hosts.tunnelBadge')}
- {host.tunnelConnections && host.tunnelConnections.length > 0 && (
- ({host.tunnelConnections.length})
- )}
-
- )}
- {host.enableFileManager && (
-
-
- {t('hosts.fileManagerBadge')}
-
- )}
-
-
-
- ))}
-
-
-
-
-
- ))}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/Server/Server.tsx b/src/ui/Apps/Server/Server.tsx
deleted file mode 100644
index 413c0a78..00000000
--- a/src/ui/Apps/Server/Server.tsx
+++ /dev/null
@@ -1,289 +0,0 @@
-import React from "react";
-import {useSidebar} from "@/components/ui/sidebar";
-import {Status, StatusIndicator} from "@/components/ui/shadcn-io/status";
-import {Separator} from "@/components/ui/separator.tsx";
-import {Button} from "@/components/ui/button.tsx";
-import {Progress} from "@/components/ui/progress"
-import {Cpu, HardDrive, MemoryStick} from "lucide-react";
-import {Tunnel} from "@/ui/Apps/Tunnel/Tunnel.tsx";
-import {getServerStatusById, getServerMetricsById, type ServerMetrics} from "@/ui/main-axios.ts";
-import {useTabs} from "@/ui/Navigation/Tabs/TabContext.tsx";
-import {useTranslation} from 'react-i18next';
-
-interface ServerProps {
- hostConfig?: any;
- title?: string;
- isVisible?: boolean;
- isTopbarOpen?: boolean;
- embedded?: boolean;
-}
-
-export function Server({
- hostConfig,
- title,
- isVisible = true,
- isTopbarOpen = true,
- embedded = false
- }: ServerProps): React.ReactElement {
- const {t} = useTranslation();
- const {state: sidebarState} = useSidebar();
- const {addTab, tabs} = useTabs() as any;
- const [serverStatus, setServerStatus] = React.useState<'online' | 'offline'>('offline');
- const [metrics, setMetrics] = React.useState(null);
- const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
-
- React.useEffect(() => {
- setCurrentHostConfig(hostConfig);
- }, [hostConfig]);
-
- React.useEffect(() => {
- const fetchLatestHostConfig = async () => {
- if (hostConfig?.id) {
- try {
- const {getSSHHosts} = await import('@/ui/main-axios.ts');
- const hosts = await getSSHHosts();
- const updatedHost = hosts.find(h => h.id === hostConfig.id);
- if (updatedHost) {
- setCurrentHostConfig(updatedHost);
- }
- } catch (error) {
- }
- }
- };
-
- fetchLatestHostConfig();
-
- const handleHostsChanged = async () => {
- if (hostConfig?.id) {
- try {
- const {getSSHHosts} = await import('@/ui/main-axios.ts');
- const hosts = await getSSHHosts();
- const updatedHost = hosts.find(h => h.id === hostConfig.id);
- if (updatedHost) {
- setCurrentHostConfig(updatedHost);
- }
- } catch (error) {
- }
- }
- };
-
- window.addEventListener('ssh-hosts:changed', handleHostsChanged);
- return () => window.removeEventListener('ssh-hosts:changed', handleHostsChanged);
- }, [hostConfig?.id]);
-
- React.useEffect(() => {
- let cancelled = false;
- let intervalId: number | undefined;
-
- const fetchStatus = async () => {
- try {
- const res = await getServerStatusById(currentHostConfig?.id);
- if (!cancelled) {
- setServerStatus(res?.status === 'online' ? 'online' : 'offline');
- }
- } catch {
- if (!cancelled) setServerStatus('offline');
- }
- };
-
- const fetchMetrics = async () => {
- if (!currentHostConfig?.id) return;
- try {
- const data = await getServerMetricsById(currentHostConfig.id);
- if (!cancelled) setMetrics(data);
- } catch {
- if (!cancelled) setMetrics(null);
- }
- };
-
- if (currentHostConfig?.id && isVisible) {
- fetchStatus();
- fetchMetrics();
- intervalId = window.setInterval(() => {
- if (isVisible) {
- fetchStatus();
- fetchMetrics();
- }
- }, 30000);
- }
-
- return () => {
- cancelled = true;
- if (intervalId) window.clearInterval(intervalId);
- };
- }, [currentHostConfig?.id, isVisible]);
-
- const topMarginPx = isTopbarOpen ? 74 : 16;
- const leftMarginPx = sidebarState === 'collapsed' ? 16 : 8;
- const bottomMarginPx = 8;
-
- const isFileManagerAlreadyOpen = React.useMemo(() => {
- if (!currentHostConfig) return false;
- return tabs.some((tab: any) =>
- tab.type === 'file_manager' &&
- tab.hostConfig?.id === currentHostConfig.id
- );
- }, [tabs, currentHostConfig]);
-
- const wrapperStyle: React.CSSProperties = embedded
- ? {opacity: isVisible ? 1 : 0, height: '100%', width: '100%'}
- : {
- opacity: isVisible ? 1 : 0,
- marginLeft: leftMarginPx,
- marginRight: 17,
- marginTop: topMarginPx,
- marginBottom: bottomMarginPx,
- height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
- };
-
- const containerClass = embedded
- ? "h-full w-full text-white overflow-hidden bg-transparent"
- : "bg-[#18181b] text-white rounded-lg border-2 border-[#303032] overflow-hidden";
-
- return (
-
-
-
- {/* Top Header */}
-
-
-
- {currentHostConfig?.folder} / {title}
-
-
-
-
-
-
- {
- if (currentHostConfig?.id) {
- try {
- const res = await getServerStatusById(currentHostConfig.id);
- setServerStatus(res?.status === 'online' ? 'online' : 'offline');
- const data = await getServerMetricsById(currentHostConfig.id);
- setMetrics(data);
- } catch {
- setServerStatus('offline');
- setMetrics(null);
- }
- }
- }}
- title={t('serverStats.refreshStatusAndMetrics')}
- >
- {t('serverStats.refreshStatus')}
-
- {currentHostConfig?.enableFileManager && (
- {
- if (!currentHostConfig || isFileManagerAlreadyOpen) return;
- const titleBase = currentHostConfig?.name && currentHostConfig.name.trim() !== ''
- ? currentHostConfig.name.trim()
- : `${currentHostConfig.username}@${currentHostConfig.ip}`;
- addTab({
- type: 'file_manager',
- title: titleBase,
- hostConfig: currentHostConfig,
- });
- }}
- >
- {t('nav.fileManager')}
-
- )}
-
-
-
-
- {/* Stats */}
-
- {/* CPU */}
-
-
-
- {(() => {
- const pct = metrics?.cpu?.percent;
- const cores = metrics?.cpu?.cores;
- const la = metrics?.cpu?.load;
- const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A';
- const coresText = (typeof cores === 'number') ? t('serverStats.cpuCores', {count: cores}) : t('serverStats.naCpus');
- const laText = (la && la.length === 3)
- ? t('serverStats.loadAverage', {avg1: la[0].toFixed(2), avg5: la[1].toFixed(2), avg15: la[2].toFixed(2)})
- : t('serverStats.loadAverageNA');
- return `${t('serverStats.cpuUsage')} - ${pctText} ${t('serverStats.of')} ${coresText} (${laText})`;
- })()}
-
-
-
-
-
-
-
- {/* Memory */}
-
-
-
- {(() => {
- const pct = metrics?.memory?.percent;
- const used = metrics?.memory?.usedGiB;
- const total = metrics?.memory?.totalGiB;
- const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A';
- const usedText = (typeof used === 'number') ? `${used} GiB` : 'N/A';
- const totalText = (typeof total === 'number') ? `${total} GiB` : 'N/A';
- return `${t('serverStats.memoryUsage')} - ${pctText} (${usedText} ${t('serverStats.of')} ${totalText})`;
- })()}
-
-
-
-
-
-
-
- {/* Root Storage */}
-
-
-
- {(() => {
- const pct = metrics?.disk?.percent;
- const used = metrics?.disk?.usedHuman;
- const total = metrics?.disk?.totalHuman;
- const pctText = (typeof pct === 'number') ? `${pct}%` : 'N/A';
- const usedText = used ?? 'N/A';
- const totalText = total ?? 'N/A';
- return `${t('serverStats.rootStorageSpace')} - ${pctText} (${usedText} ${t('serverStats.of')} ${totalText})`;
- })()}
-
-
-
-
-
-
- {/* SSH Tunnels */}
- {(currentHostConfig?.tunnelConnections && currentHostConfig.tunnelConnections.length > 0) && (
-
-
-
- )}
-
-
- {t('serverStats.feedbackMessage')}{" "}
-
- GitHub
-
- !
-
-
-
- );
-}
diff --git a/src/ui/Apps/Terminal/Terminal.tsx b/src/ui/Apps/Terminal/Terminal.tsx
deleted file mode 100644
index 1ae72427..00000000
--- a/src/ui/Apps/Terminal/Terminal.tsx
+++ /dev/null
@@ -1,418 +0,0 @@
-import {useEffect, useRef, useState, useImperativeHandle, forwardRef} from 'react';
-import {useXTerm} from 'react-xtermjs';
-import {FitAddon} from '@xterm/addon-fit';
-import {ClipboardAddon} from '@xterm/addon-clipboard';
-import {Unicode11Addon} from '@xterm/addon-unicode11';
-import {WebLinksAddon} from '@xterm/addon-web-links';
-import {useTranslation} from 'react-i18next';
-
-interface SSHTerminalProps {
- hostConfig: any;
- isVisible: boolean;
- title?: string;
- showTitle?: boolean;
- splitScreen?: boolean;
-}
-
-export const Terminal = forwardRef(function SSHTerminal(
- {hostConfig, isVisible, splitScreen = false},
- ref
-) {
- const {t} = useTranslation();
- const {instance: terminal, ref: xtermRef} = useXTerm();
- const fitAddonRef = useRef(null);
- const webSocketRef = useRef(null);
- const resizeTimeout = useRef(null);
- const wasDisconnectedBySSH = useRef(false);
- const pingIntervalRef = useRef(null);
- const [visible, setVisible] = useState(false);
- const isVisibleRef = useRef(false);
-
-
- const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
- const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
- const notifyTimerRef = useRef(null);
- const DEBOUNCE_MS = 140;
-
- useEffect(() => {
- isVisibleRef.current = isVisible;
- }, [isVisible]);
-
- function hardRefresh() {
- try {
- if (terminal && typeof (terminal as any).refresh === 'function') {
- (terminal as any).refresh(0, terminal.rows - 1);
- }
- } catch (_) {
- }
- }
-
- function scheduleNotify(cols: number, rows: number) {
- if (!(cols > 0 && rows > 0)) return;
- pendingSizeRef.current = {cols, rows};
- if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
- notifyTimerRef.current = setTimeout(() => {
- const next = pendingSizeRef.current;
- const last = lastSentSizeRef.current;
- if (!next) return;
- if (last && last.cols === next.cols && last.rows === next.rows) return;
- if (webSocketRef.current?.readyState === WebSocket.OPEN) {
- webSocketRef.current.send(JSON.stringify({type: 'resize', data: next}));
- lastSentSizeRef.current = next;
- }
- }, DEBOUNCE_MS);
- }
-
- useImperativeHandle(ref, () => ({
- disconnect: () => {
- if (pingIntervalRef.current) {
- clearInterval(pingIntervalRef.current);
- pingIntervalRef.current = null;
- }
- webSocketRef.current?.close();
- },
- fit: () => {
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- },
- sendInput: (data: string) => {
- if (webSocketRef.current?.readyState === 1) {
- webSocketRef.current.send(JSON.stringify({type: 'input', data}));
- }
- },
- notifyResize: () => {
- try {
- const cols = terminal?.cols ?? undefined;
- const rows = terminal?.rows ?? undefined;
- if (typeof cols === 'number' && typeof rows === 'number') {
- scheduleNotify(cols, rows);
- hardRefresh();
- }
- } catch (_) {
- }
- },
- refresh: () => hardRefresh(),
- }), [terminal]);
-
- useEffect(() => {
- window.addEventListener('resize', handleWindowResize);
- return () => window.removeEventListener('resize', handleWindowResize);
- }, []);
-
- function handleWindowResize() {
- if (!isVisibleRef.current) return;
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- }
-
- function getCookie(name: string) {
- return document.cookie.split('; ').reduce((r, v) => {
- const parts = v.split('=');
- return parts[0] === name ? decodeURIComponent(parts[1]) : r;
- }, "");
- }
-
- function getUseRightClickCopyPaste() {
- return getCookie("rightClickCopyPaste") === "true"
- }
-
-
-
- function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
- ws.addEventListener('open', () => {
-
- ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
- terminal.onData((data) => {
- ws.send(JSON.stringify({type: 'input', data}));
- });
-
- pingIntervalRef.current = setInterval(() => {
- if (ws.readyState === WebSocket.OPEN) {
- ws.send(JSON.stringify({type: 'ping'}));
- }
- }, 30000);
-
-
- });
-
- ws.addEventListener('message', (event) => {
- try {
- const msg = JSON.parse(event.data);
- if (msg.type === 'data') terminal.write(msg.data);
- else if (msg.type === 'error') terminal.writeln(`\r\n[${t('terminal.error')}] ${msg.message}`);
- else if (msg.type === 'connected') {
- } else if (msg.type === 'disconnected') {
- wasDisconnectedBySSH.current = true;
- terminal.writeln(`\r\n[${msg.message || t('terminal.disconnected')}]`);
- }
- } catch (error) {
- }
- });
-
- ws.addEventListener('close', () => {
- if (!wasDisconnectedBySSH.current) {
- terminal.writeln(`\r\n[${t('terminal.connectionClosed')}]`);
- }
- });
-
- ws.addEventListener('error', () => {
- terminal.writeln(`\r\n[${t('terminal.connectionError')}]`);
- });
- }
-
- async function writeTextToClipboard(text: string): Promise {
- try {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- await navigator.clipboard.writeText(text);
- return;
- }
- } catch (_) {
- }
- const textarea = document.createElement('textarea');
- textarea.value = text;
- textarea.style.position = 'fixed';
- textarea.style.left = '-9999px';
- document.body.appendChild(textarea);
- textarea.focus();
- textarea.select();
- try {
- document.execCommand('copy');
- } finally {
- document.body.removeChild(textarea);
- }
- }
-
- async function readTextFromClipboard(): Promise {
- try {
- if (navigator.clipboard && navigator.clipboard.readText) {
- return await navigator.clipboard.readText();
- }
- } catch (_) {
- }
- return '';
- }
-
- useEffect(() => {
- if (!terminal || !xtermRef.current || !hostConfig) return;
-
- terminal.options = {
- cursorBlink: true,
- cursorStyle: 'bar',
- scrollback: 10000,
- fontSize: 14,
- fontFamily: '"JetBrains Mono Nerd Font", "MesloLGS NF", "FiraCode Nerd Font", "Cascadia Code", "JetBrains Mono", Consolas, "Courier New", monospace',
- theme: {background: '#18181b', foreground: '#f7f7f7'},
- allowTransparency: true,
- convertEol: true,
- windowsMode: false,
- macOptionIsMeta: false,
- macOptionClickForcesSelection: false,
- rightClickSelectsWord: false,
- fastScrollModifier: 'alt',
- fastScrollSensitivity: 5,
- allowProposedApi: true,
- };
-
- const fitAddon = new FitAddon();
- const clipboardAddon = new ClipboardAddon();
- const unicode11Addon = new Unicode11Addon();
- const webLinksAddon = new WebLinksAddon();
-
- fitAddonRef.current = fitAddon;
- terminal.loadAddon(fitAddon);
- terminal.loadAddon(clipboardAddon);
- terminal.loadAddon(unicode11Addon);
- terminal.loadAddon(webLinksAddon);
- terminal.open(xtermRef.current);
-
- const element = xtermRef.current;
- const handleContextMenu = async (e: MouseEvent) => {
- if (!getUseRightClickCopyPaste()) return;
- e.preventDefault();
- e.stopPropagation();
- try {
- if (terminal.hasSelection()) {
- const selection = terminal.getSelection();
- if (selection) {
- await writeTextToClipboard(selection);
- terminal.clearSelection();
- }
- } else {
- const pasteText = await readTextFromClipboard();
- if (pasteText) terminal.paste(pasteText);
- }
- } catch (_) {
- }
- };
- element?.addEventListener('contextmenu', handleContextMenu);
-
- const resizeObserver = new ResizeObserver(() => {
- if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
- resizeTimeout.current = setTimeout(() => {
- if (!isVisibleRef.current) return;
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- }, 100);
- });
-
- resizeObserver.observe(xtermRef.current);
-
- const readyFonts = (document as any).fonts?.ready instanceof Promise ? (document as any).fonts.ready : Promise.resolve();
- readyFonts.then(() => {
- setTimeout(() => {
- fitAddon.fit();
- setTimeout(() => {
- fitAddon.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- setVisible(true);
- if (terminal && !splitScreen) {
- terminal.focus();
- }
- }, 0);
-
- const cols = terminal.cols;
- const rows = terminal.rows;
-
- const isDev = process.env.NODE_ENV === 'development' &&
- (window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
-
- const wsUrl = isDev
- ? 'ws://localhost:8082'
- : `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ssh/websocket/`;
-
- const ws = new WebSocket(wsUrl);
- webSocketRef.current = ws;
- wasDisconnectedBySSH.current = false;
-
- setupWebSocketListeners(ws, cols, rows);
- }, 300);
- });
-
- return () => {
- resizeObserver.disconnect();
- element?.removeEventListener('contextmenu', handleContextMenu);
- if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
- if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
- if (pingIntervalRef.current) {
- clearInterval(pingIntervalRef.current);
- pingIntervalRef.current = null;
- }
- webSocketRef.current?.close();
- };
- }, [xtermRef, terminal, hostConfig]);
-
- useEffect(() => {
- if (isVisible && fitAddonRef.current) {
- setTimeout(() => {
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- if (terminal && !splitScreen) {
- terminal.focus();
- }
- }, 0);
-
- if (terminal && !splitScreen) {
- setTimeout(() => {
- terminal.focus();
- }, 100);
- }
- }
- }, [isVisible, splitScreen, terminal]);
-
- useEffect(() => {
- if (!fitAddonRef.current) return;
- setTimeout(() => {
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- if (terminal && !splitScreen && isVisible) {
- terminal.focus();
- }
- }, 0);
- }, [splitScreen, isVisible, terminal]);
-
- return (
- {
- if (terminal && !splitScreen) {
- terminal.focus();
- }
- }}
- />
- );
-});
-
-const style = document.createElement('style');
-style.innerHTML = `
-@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
-
-/* Load NerdFonts locally */
-@font-face {
- font-family: 'JetBrains Mono Nerd Font';
- src: url('/fonts/JetBrainsMonoNerdFont-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'JetBrains Mono Nerd Font';
- src: url('/fonts/JetBrainsMonoNerdFont-Bold.ttf') format('truetype');
- font-weight: bold;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'JetBrains Mono Nerd Font';
- src: url('/fonts/JetBrainsMonoNerdFont-Italic.ttf') format('truetype');
- font-weight: normal;
- font-style: italic;
- font-display: swap;
-}
-
-.xterm .xterm-viewport::-webkit-scrollbar {
- width: 8px;
- background: transparent;
-}
-.xterm .xterm-viewport::-webkit-scrollbar-thumb {
- background: rgba(180,180,180,0.7);
- border-radius: 4px;
-}
-.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
- background: rgba(120,120,120,0.9);
-}
-.xterm .xterm-viewport {
- scrollbar-width: thin;
- scrollbar-color: rgba(180,180,180,0.7) transparent;
-}
-
-.xterm {
- font-feature-settings: "liga" 1, "calt" 1;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.xterm .xterm-screen {
- font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font', 'Cascadia Code', 'JetBrains Mono', Consolas, "Courier New", monospace !important;
- font-variant-ligatures: contextual;
-}
-
-.xterm .xterm-screen .xterm-char {
- font-feature-settings: "liga" 1, "calt" 1;
-}
-
-.xterm .xterm-screen .xterm-char[data-char-code^="\\uE"] {
- font-family: 'JetBrains Mono Nerd Font', 'MesloLGS NF', 'FiraCode Nerd Font' !important;
-}
-`;
-document.head.appendChild(style);
diff --git a/src/ui/Apps/Tunnel/Tunnel.tsx b/src/ui/Apps/Tunnel/Tunnel.tsx
deleted file mode 100644
index e900f0eb..00000000
--- a/src/ui/Apps/Tunnel/Tunnel.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import React, {useState, useEffect, useCallback} from "react";
-import {TunnelViewer} from "@/ui/Apps/Tunnel/TunnelViewer.tsx";
-import {getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel} from "@/ui/main-axios.ts";
-
-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 TunnelStatus {
- status: string;
- reason?: string;
- errorType?: string;
- retryCount?: number;
- maxRetries?: number;
- nextRetryIn?: number;
- retryExhausted?: boolean;
-}
-
-interface SSHTunnelProps {
- filterHostKey?: string;
-}
-
-export function Tunnel({filterHostKey}: SSHTunnelProps): React.ReactElement {
- const [allHosts, setAllHosts] = useState
([]);
- const [visibleHosts, setVisibleHosts] = useState([]);
- const [tunnelStatuses, setTunnelStatuses] = useState>({});
- const [tunnelActions, setTunnelActions] = useState>({});
-
- const prevVisibleHostRef = React.useRef(null);
-
- const haveTunnelConnectionsChanged = (a: TunnelConnection[] = [], b: TunnelConnection[] = []): boolean => {
- if (a.length !== b.length) return true;
- for (let i = 0; i < a.length; i++) {
- const x = a[i];
- const y = b[i];
- if (
- x.sourcePort !== y.sourcePort ||
- x.endpointPort !== y.endpointPort ||
- x.endpointHost !== y.endpointHost ||
- x.maxRetries !== y.maxRetries ||
- x.retryInterval !== y.retryInterval ||
- x.autoStart !== y.autoStart
- ) {
- return true;
- }
- }
- return false;
- };
-
- const fetchHosts = useCallback(async () => {
- const hostsData = await getSSHHosts();
- setAllHosts(hostsData);
- const nextVisible = filterHostKey
- ? hostsData.filter(h => {
- const key = (h.name && h.name.trim() !== '') ? h.name : `${h.username}@${h.ip}`;
- return key === filterHostKey;
- })
- : hostsData;
-
- const prev = prevVisibleHostRef.current;
- const curr = nextVisible[0] ?? null;
- let changed = false;
- if (!prev && curr) changed = true;
- else if (prev && !curr) changed = true;
- else if (prev && curr) {
- if (
- prev.id !== curr.id ||
- prev.name !== curr.name ||
- prev.ip !== curr.ip ||
- prev.port !== curr.port ||
- prev.username !== curr.username ||
- haveTunnelConnectionsChanged(prev.tunnelConnections, curr.tunnelConnections)
- ) {
- changed = true;
- }
- }
-
- if (changed) {
- setVisibleHosts(nextVisible);
- prevVisibleHostRef.current = curr;
- }
- }, [filterHostKey]);
-
- const fetchTunnelStatuses = useCallback(async () => {
- const statusData = await getTunnelStatuses();
- setTunnelStatuses(statusData);
- }, []);
-
- useEffect(() => {
- fetchHosts();
- const interval = setInterval(fetchHosts, 5000);
-
- const handleHostsChanged = () => {
- fetchHosts();
- };
- window.addEventListener('ssh-hosts:changed', handleHostsChanged as EventListener);
-
- return () => {
- clearInterval(interval);
- window.removeEventListener('ssh-hosts:changed', handleHostsChanged as EventListener);
- };
- }, [fetchHosts]);
-
- useEffect(() => {
- fetchTunnelStatuses();
- const interval = setInterval(fetchTunnelStatuses, 500);
- return () => clearInterval(interval);
- }, [fetchTunnelStatuses]);
-
- const handleTunnelAction = async (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => {
- const tunnel = host.tunnelConnections[tunnelIndex];
- const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`;
-
- setTunnelActions(prev => ({...prev, [tunnelName]: true}));
-
- try {
- if (action === 'connect') {
- const endpointHost = allHosts.find(h =>
- h.name === tunnel.endpointHost ||
- `${h.username}@${h.ip}` === tunnel.endpointHost
- );
-
- if (!endpointHost) {
- throw new Error('Endpoint host not found');
- }
-
- const tunnelConfig = {
- name: tunnelName,
- hostName: host.name || `${host.username}@${host.ip}`,
- sourceIP: host.ip,
- sourceSSHPort: host.port,
- sourceUsername: host.username,
- sourcePassword: host.authType === 'password' ? host.password : undefined,
- sourceAuthMethod: host.authType,
- sourceSSHKey: host.authType === 'key' ? host.key : undefined,
- sourceKeyPassword: host.authType === 'key' ? host.keyPassword : undefined,
- sourceKeyType: host.authType === 'key' ? host.keyType : undefined,
- endpointIP: endpointHost.ip,
- endpointSSHPort: endpointHost.port,
- endpointUsername: endpointHost.username,
- endpointPassword: endpointHost.authType === 'password' ? endpointHost.password : undefined,
- endpointAuthMethod: endpointHost.authType,
- endpointSSHKey: endpointHost.authType === 'key' ? endpointHost.key : undefined,
- endpointKeyPassword: endpointHost.authType === 'key' ? endpointHost.keyPassword : undefined,
- endpointKeyType: endpointHost.authType === 'key' ? endpointHost.keyType : undefined,
- sourcePort: tunnel.sourcePort,
- endpointPort: tunnel.endpointPort,
- maxRetries: tunnel.maxRetries,
- retryInterval: tunnel.retryInterval * 1000,
- autoStart: tunnel.autoStart,
- isPinned: host.pin
- };
-
- await connectTunnel(tunnelConfig);
- } else if (action === 'disconnect') {
- await disconnectTunnel(tunnelName);
- } else if (action === 'cancel') {
- await cancelTunnel(tunnelName);
- }
-
- await fetchTunnelStatuses();
- } catch (err) {
- } finally {
- setTunnelActions(prev => ({...prev, [tunnelName]: false}));
- }
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/Tunnel/TunnelObject.tsx b/src/ui/Apps/Tunnel/TunnelObject.tsx
deleted file mode 100644
index 52105a53..00000000
--- a/src/ui/Apps/Tunnel/TunnelObject.tsx
+++ /dev/null
@@ -1,490 +0,0 @@
-import React from "react";
-import {Button} from "@/components/ui/button.tsx";
-import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card.tsx";
-import {Separator} from "@/components/ui/separator.tsx";
-import {useTranslation} from 'react-i18next';
-import {
- Loader2,
- Pin,
- Terminal,
- Network,
- FileEdit,
- Tag,
- Play,
- Square,
- AlertCircle,
- Clock,
- Wifi,
- WifiOff,
- Zap,
- X
-} from "lucide-react";
-import {Badge} from "@/components/ui/badge.tsx";
-
-const CONNECTION_STATES = {
- DISCONNECTED: "disconnected",
- CONNECTING: "connecting",
- CONNECTED: "connected",
- VERIFYING: "verifying",
- FAILED: "failed",
- UNSTABLE: "unstable",
- RETRYING: "retrying",
- WAITING: "waiting",
- DISCONNECTING: "disconnecting"
-};
-
-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;
- enableTerminal: boolean;
- enableTunnel: boolean;
- enableFileManager: boolean;
- defaultPath: string;
- tunnelConnections: TunnelConnection[];
- createdAt: string;
- updatedAt: string;
-}
-
-interface TunnelStatus {
- status: string;
- reason?: string;
- errorType?: string;
- retryCount?: number;
- maxRetries?: number;
- nextRetryIn?: number;
- retryExhausted?: boolean;
-}
-
-interface SSHTunnelObjectProps {
- host: SSHHost;
- tunnelStatuses: Record;
- tunnelActions: Record;
- onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise;
- compact?: boolean;
- bare?: boolean;
-}
-
-export function TunnelObject({
- host,
- tunnelStatuses,
- tunnelActions,
- onTunnelAction,
- compact = false,
- bare = false
- }: SSHTunnelObjectProps): React.ReactElement {
- const {t} = useTranslation();
-
- const getTunnelStatus = (tunnelIndex: number): TunnelStatus | undefined => {
- const tunnel = host.tunnelConnections[tunnelIndex];
- const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`;
- return tunnelStatuses[tunnelName];
- };
-
- const getTunnelStatusDisplay = (status: TunnelStatus | undefined) => {
- if (!status) return {
- icon: ,
- text: t('tunnels.unknown'),
- color: 'text-muted-foreground',
- bgColor: 'bg-muted/50',
- borderColor: 'border-border'
- };
-
- const statusValue = status.status || 'DISCONNECTED';
-
- switch (statusValue.toUpperCase()) {
- case 'CONNECTED':
- return {
- icon: ,
- text: t('tunnels.connected'),
- color: 'text-green-600 dark:text-green-400',
- bgColor: 'bg-green-500/10 dark:bg-green-400/10',
- borderColor: 'border-green-500/20 dark:border-green-400/20'
- };
- case 'CONNECTING':
- return {
- icon: ,
- text: t('tunnels.connecting'),
- color: 'text-blue-600 dark:text-blue-400',
- bgColor: 'bg-blue-500/10 dark:bg-blue-400/10',
- borderColor: 'border-blue-500/20 dark:border-blue-400/20'
- };
- case 'DISCONNECTING':
- return {
- icon: ,
- text: t('tunnels.disconnecting'),
- color: 'text-orange-600 dark:text-orange-400',
- bgColor: 'bg-orange-500/10 dark:bg-orange-400/10',
- borderColor: 'border-orange-500/20 dark:border-orange-400/20'
- };
- case 'DISCONNECTED':
- return {
- icon: ,
- text: t('tunnels.disconnected'),
- color: 'text-muted-foreground',
- bgColor: 'bg-muted/30',
- borderColor: 'border-border'
- };
- case 'WAITING':
- return {
- icon: ,
- color: 'text-blue-600 dark:text-blue-400',
- bgColor: 'bg-blue-500/10 dark:bg-blue-400/10',
- borderColor: 'border-blue-500/20 dark:border-blue-400/20'
- };
- case 'ERROR':
- case 'FAILED':
- return {
- icon: ,
- text: status.reason || t('tunnels.error'),
- color: 'text-red-600 dark:text-red-400',
- bgColor: 'bg-red-500/10 dark:bg-red-400/10',
- borderColor: 'border-red-500/20 dark:border-red-400/20'
- };
- default:
- return {
- icon: ,
- text: statusValue,
- color: 'text-muted-foreground',
- bgColor: 'bg-muted/30',
- borderColor: 'border-border'
- };
- }
- };
-
- if (bare) {
- return (
-
-
- {host.tunnelConnections && host.tunnelConnections.length > 0 ? (
-
- {host.tunnelConnections.map((tunnel, tunnelIndex) => {
- const status = getTunnelStatus(tunnelIndex);
- const statusDisplay = getTunnelStatusDisplay(status);
- const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`;
- const isActionLoading = tunnelActions[tunnelName];
- const statusValue = status?.status?.toUpperCase() || 'DISCONNECTED';
- const isConnected = statusValue === 'CONNECTED';
- const isConnecting = statusValue === 'CONNECTING';
- const isDisconnecting = statusValue === 'DISCONNECTING';
- const isRetrying = statusValue === 'RETRYING';
- const isWaiting = statusValue === 'WAITING';
-
- return (
-
-
-
-
- {statusDisplay.icon}
-
-
-
- {t('tunnels.port')} {tunnel.sourcePort} → {tunnel.endpointHost}:{tunnel.endpointPort}
-
-
- {statusDisplay.text}
-
-
-
-
- {!isActionLoading ? (
-
- {isConnected ? (
- <>
-
onTunnelAction('disconnect', host, tunnelIndex)}
- className="h-7 px-2 text-red-600 dark:text-red-400 border-red-500/30 dark:border-red-400/30 hover:bg-red-500/10 dark:hover:bg-red-400/10 hover:border-red-500/50 dark:hover:border-red-400/50 text-xs"
- >
-
- {t('tunnels.disconnect')}
-
- >
- ) : isRetrying || isWaiting ? (
-
onTunnelAction('cancel', host, tunnelIndex)}
- className="h-7 px-2 text-orange-600 dark:text-orange-400 border-orange-500/30 dark:border-orange-400/30 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 hover:border-orange-500/50 dark:hover:border-orange-400/50 text-xs"
- >
-
- {t('tunnels.cancel')}
-
- ) : (
-
onTunnelAction('connect', host, tunnelIndex)}
- disabled={isConnecting || isDisconnecting}
- className="h-7 px-2 text-green-600 dark:text-green-400 border-green-500/30 dark:border-green-400/30 hover:bg-green-500/10 dark:hover:bg-green-400/10 hover:border-green-500/50 dark:hover:border-green-400/50 text-xs"
- >
-
- {t('tunnels.connect')}
-
- )}
-
- ) : (
-
-
- {isConnected ? t('tunnels.disconnecting') : isRetrying || isWaiting ? t('tunnels.canceling') : t('tunnels.connecting')}
-
- )}
-
-
-
- {(statusValue === 'ERROR' || statusValue === 'FAILED') && status?.reason && (
-
-
{t('tunnels.error')}:
- {status.reason}
- {status.reason && status.reason.includes('Max retries exhausted') && (
- <>
-
- >
- )}
-
- )}
-
- {(statusValue === 'RETRYING' || statusValue === 'WAITING') && status?.retryCount && status?.maxRetries && (
-
-
- {statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
-
-
- {t('tunnels.attempt', { current: status.retryCount, max: status.maxRetries })}
- {status.nextRetryIn && (
- • {t('tunnels.nextRetryIn', { seconds: status.nextRetryIn })}
- )}
-
-
- )}
-
- );
- })}
-
- ) : (
-
-
-
{t('tunnels.noTunnelConnections')}
-
- )}
-
-
- );
- }
-
- return (
-
-
- {!compact && (
-
-
- {host.pin &&
}
-
-
- {host.name || `${host.username}@${host.ip}`}
-
-
- {host.ip}:{host.port} • {host.username}
-
-
-
-
- )}
-
- {!compact && host.tags && host.tags.length > 0 && (
-
- {host.tags.slice(0, 3).map((tag, index) => (
-
-
- {tag}
-
- ))}
- {host.tags.length > 3 && (
-
- +{host.tags.length - 3}
-
- )}
-
- )}
-
- {!compact &&
}
-
-
- {!compact && (
-
-
- {t('tunnels.tunnelConnections')} ({host.tunnelConnections.length})
-
- )}
- {host.tunnelConnections && host.tunnelConnections.length > 0 ? (
-
- {host.tunnelConnections.map((tunnel, tunnelIndex) => {
- const status = getTunnelStatus(tunnelIndex);
- const statusDisplay = getTunnelStatusDisplay(status);
- const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnel.sourcePort}_${tunnel.endpointPort}`;
- const isActionLoading = tunnelActions[tunnelName];
- const statusValue = status?.status?.toUpperCase() || 'DISCONNECTED';
- const isConnected = statusValue === 'CONNECTED';
- const isConnecting = statusValue === 'CONNECTING';
- const isDisconnecting = statusValue === 'DISCONNECTING';
- const isRetrying = statusValue === 'RETRYING';
- const isWaiting = statusValue === 'WAITING';
-
- return (
-
-
-
-
- {statusDisplay.icon}
-
-
-
- {t('tunnels.port')} {tunnel.sourcePort} → {tunnel.endpointHost}:{tunnel.endpointPort}
-
-
- {statusDisplay.text}
-
-
-
-
- {!isActionLoading && (
-
- {isConnected ? (
- <>
-
onTunnelAction('disconnect', host, tunnelIndex)}
- className="h-7 px-2 text-red-600 dark:text-red-400 border-red-500/30 dark:border-red-400/30 hover:bg-red-500/10 dark:hover:bg-red-400/10 hover:border-red-500/50 dark:hover:border-red-400/50 text-xs"
- >
-
- {t('tunnels.disconnect')}
-
- >
- ) : isRetrying || isWaiting ? (
-
onTunnelAction('cancel', host, tunnelIndex)}
- className="h-7 px-2 text-orange-600 dark:text-orange-400 border-orange-500/30 dark:border-orange-400/30 hover:bg-orange-500/10 dark:hover:bg-orange-400/10 hover:border-orange-500/50 dark:hover:border-orange-400/50 text-xs"
- >
-
- {t('tunnels.cancel')}
-
- ) : (
-
onTunnelAction('connect', host, tunnelIndex)}
- disabled={isConnecting || isDisconnecting}
- className="h-7 px-2 text-green-600 dark:text-green-400 border-green-500/30 dark:border-green-400/30 hover:bg-green-500/10 dark:hover:bg-green-400/10 hover:border-green-500/50 dark:hover:border-green-400/50 text-xs"
- >
-
- {t('tunnels.connect')}
-
- )}
-
- )}
- {isActionLoading && (
-
-
- {isConnected ? t('tunnels.disconnecting') : isRetrying || isWaiting ? t('tunnels.canceling') : t('tunnels.connecting')}
-
- )}
-
-
-
- {(statusValue === 'ERROR' || statusValue === 'FAILED') && status?.reason && (
-
-
{t('tunnels.error')}:
- {status.reason}
- {status.reason && status.reason.includes('Max retries exhausted') && (
- <>
-
- >
- )}
-
- )}
-
- {(statusValue === 'RETRYING' || statusValue === 'WAITING') && status?.retryCount && status?.maxRetries && (
-
-
- {statusValue === 'WAITING' ? t('tunnels.waitingForRetry') : t('tunnels.retryingConnection')}
-
-
- {t('tunnels.attempt', { current: status.retryCount, max: status.maxRetries })}
- {status.nextRetryIn && (
- • {t('tunnels.nextRetryIn', { seconds: status.nextRetryIn })}
- )}
-
-
- )}
-
- );
- })}
-
- ) : (
-
-
-
{t('tunnels.noTunnelConnections')}
-
- )}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Apps/Tunnel/TunnelViewer.tsx b/src/ui/Apps/Tunnel/TunnelViewer.tsx
deleted file mode 100644
index dcec9123..00000000
--- a/src/ui/Apps/Tunnel/TunnelViewer.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React from "react";
-import {TunnelObject} from "./TunnelObject.tsx";
-import {useTranslation} from 'react-i18next';
-
-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;
- enableTerminal: boolean;
- enableTunnel: boolean;
- enableFileManager: boolean;
- defaultPath: string;
- tunnelConnections: TunnelConnection[];
- createdAt: string;
- updatedAt: string;
-}
-
-interface TunnelStatus {
- status: string;
- reason?: string;
- errorType?: string;
- retryCount?: number;
- maxRetries?: number;
- nextRetryIn?: number;
- retryExhausted?: boolean;
-}
-
-interface SSHTunnelViewerProps {
- hosts: SSHHost[];
- tunnelStatuses: Record;
- tunnelActions: Record;
- onTunnelAction: (action: 'connect' | 'disconnect' | 'cancel', host: SSHHost, tunnelIndex: number) => Promise;
-}
-
-export function TunnelViewer({
- hosts = [],
- tunnelStatuses = {},
- tunnelActions = {},
- onTunnelAction
- }: SSHTunnelViewerProps): React.ReactElement {
- const {t} = useTranslation();
- const activeHost: SSHHost | undefined = Array.isArray(hosts) && hosts.length > 0 ? hosts[0] : undefined;
-
- if (!activeHost || !activeHost.tunnelConnections || activeHost.tunnelConnections.length === 0) {
- return (
-
-
{t('tunnels.noSshTunnels')}
-
- {t('tunnels.createFirstTunnelMessage')}
-
-
- );
- }
-
- return (
-
-
-
{t('tunnels.title')}
-
-
-
- {activeHost.tunnelConnections.map((t, idx) => (
- onTunnelAction(action, activeHost, idx)}
- compact
- bare
- />
- ))}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/ui/Desktop/Admin/AdminSettings.tsx b/src/ui/Desktop/Admin/AdminSettings.tsx
new file mode 100644
index 00000000..8b2f8cc9
--- /dev/null
+++ b/src/ui/Desktop/Admin/AdminSettings.tsx
@@ -0,0 +1,690 @@
+import React from "react";
+import { useSidebar } from "@/components/ui/sidebar";
+import { Separator } from "@/components/ui/separator.tsx";
+import { Button } from "@/components/ui/button.tsx";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
+import { Checkbox } from "@/components/ui/checkbox.tsx";
+import { Input } from "@/components/ui/input.tsx";
+import { PasswordInput } from "@/components/ui/password-input.tsx";
+import { Label } from "@/components/ui/label.tsx";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@/components/ui/tabs.tsx";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table.tsx";
+import { Shield, Trash2, Users } from "lucide-react";
+import { toast } from "sonner";
+import { useTranslation } from "react-i18next";
+import { useConfirmation } from "@/hooks/use-confirmation.ts";
+import {
+ getOIDCConfig,
+ getRegistrationAllowed,
+ getUserList,
+ updateRegistrationAllowed,
+ updateOIDCConfig,
+ disableOIDCConfig,
+ makeUserAdmin,
+ removeAdminStatus,
+ deleteUser,
+ getCookie,
+ isElectron,
+} from "@/ui/main-axios.ts";
+
+interface AdminSettingsProps {
+ isTopbarOpen?: boolean;
+}
+
+export function AdminSettings({
+ isTopbarOpen = true,
+}: AdminSettingsProps): React.ReactElement {
+ const { t } = useTranslation();
+ const { confirmWithToast } = useConfirmation();
+ const { state: sidebarState } = useSidebar();
+
+ const [allowRegistration, setAllowRegistration] = React.useState(true);
+ const [regLoading, setRegLoading] = React.useState(false);
+
+ const [oidcConfig, setOidcConfig] = React.useState({
+ client_id: "",
+ client_secret: "",
+ issuer_url: "",
+ authorization_url: "",
+ token_url: "",
+ identifier_path: "sub",
+ name_path: "name",
+ scopes: "openid email profile",
+ userinfo_url: "",
+ });
+ const [oidcLoading, setOidcLoading] = React.useState(false);
+ const [oidcError, setOidcError] = React.useState(null);
+
+ const [users, setUsers] = React.useState<
+ Array<{
+ id: string;
+ username: string;
+ is_admin: boolean;
+ is_oidc: boolean;
+ }>
+ >([]);
+ const [usersLoading, setUsersLoading] = React.useState(false);
+ const [newAdminUsername, setNewAdminUsername] = React.useState("");
+ const [makeAdminLoading, setMakeAdminLoading] = React.useState(false);
+ const [makeAdminError, setMakeAdminError] = React.useState(
+ null,
+ );
+
+ React.useEffect(() => {
+ const jwt = getCookie("jwt");
+ if (!jwt) return;
+
+ if (isElectron()) {
+ const serverUrl = (window as any).configuredServerUrl;
+ if (!serverUrl) {
+ return;
+ }
+ }
+
+ getOIDCConfig()
+ .then((res) => {
+ if (res) setOidcConfig(res);
+ })
+ .catch((err) => {
+ if (!err.message?.includes("No server configured")) {
+ toast.error(t("admin.failedToFetchOidcConfig"));
+ }
+ });
+ fetchUsers();
+ }, []);
+
+ React.useEffect(() => {
+ if (isElectron()) {
+ const serverUrl = (window as any).configuredServerUrl;
+ if (!serverUrl) {
+ return;
+ }
+ }
+
+ getRegistrationAllowed()
+ .then((res) => {
+ if (typeof res?.allowed === "boolean") {
+ setAllowRegistration(res.allowed);
+ }
+ })
+ .catch((err) => {
+ if (!err.message?.includes("No server configured")) {
+ toast.error(t("admin.failedToFetchRegistrationStatus"));
+ }
+ });
+ }, []);
+
+ const fetchUsers = async () => {
+ const jwt = getCookie("jwt");
+ if (!jwt) return;
+
+ if (isElectron()) {
+ const serverUrl = (window as any).configuredServerUrl;
+ if (!serverUrl) {
+ return;
+ }
+ }
+
+ setUsersLoading(true);
+ try {
+ const response = await getUserList();
+ setUsers(response.users);
+ } catch (err) {
+ if (!err.message?.includes("No server configured")) {
+ toast.error(t("admin.failedToFetchUsers"));
+ }
+ } finally {
+ setUsersLoading(false);
+ }
+ };
+
+ const handleToggleRegistration = async (checked: boolean) => {
+ setRegLoading(true);
+ const jwt = getCookie("jwt");
+ try {
+ await updateRegistrationAllowed(checked);
+ setAllowRegistration(checked);
+ } finally {
+ setRegLoading(false);
+ }
+ };
+
+ const handleOIDCConfigSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setOidcLoading(true);
+ setOidcError(null);
+
+ const required = [
+ "client_id",
+ "client_secret",
+ "issuer_url",
+ "authorization_url",
+ "token_url",
+ ];
+ const missing = required.filter(
+ (f) => !oidcConfig[f as keyof typeof oidcConfig],
+ );
+ if (missing.length > 0) {
+ setOidcError(
+ t("admin.missingRequiredFields", { fields: missing.join(", ") }),
+ );
+ setOidcLoading(false);
+ return;
+ }
+
+ const jwt = getCookie("jwt");
+ try {
+ await updateOIDCConfig(oidcConfig);
+ toast.success(t("admin.oidcConfigurationUpdated"));
+ } catch (err: any) {
+ setOidcError(
+ err?.response?.data?.error || t("admin.failedToUpdateOidcConfig"),
+ );
+ } finally {
+ setOidcLoading(false);
+ }
+ };
+
+ const handleOIDCConfigChange = (field: string, value: string) => {
+ setOidcConfig((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const handleMakeUserAdmin = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!newAdminUsername.trim()) return;
+ setMakeAdminLoading(true);
+ setMakeAdminError(null);
+ const jwt = getCookie("jwt");
+ try {
+ await makeUserAdmin(newAdminUsername.trim());
+ toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername }));
+ setNewAdminUsername("");
+ fetchUsers();
+ } catch (err: any) {
+ setMakeAdminError(
+ err?.response?.data?.error || t("admin.failedToMakeUserAdmin"),
+ );
+ } finally {
+ setMakeAdminLoading(false);
+ }
+ };
+
+ const handleRemoveAdminStatus = async (username: string) => {
+ confirmWithToast(t("admin.removeAdminStatus", { username }), async () => {
+ const jwt = getCookie("jwt");
+ try {
+ await removeAdminStatus(username);
+ toast.success(t("admin.adminStatusRemoved", { username }));
+ fetchUsers();
+ } catch (err: any) {
+ toast.error(t("admin.failedToRemoveAdminStatus"));
+ }
+ });
+ };
+
+ const handleDeleteUser = async (username: string) => {
+ confirmWithToast(
+ t("admin.deleteUser", { username }),
+ async () => {
+ const jwt = getCookie("jwt");
+ try {
+ await deleteUser(username);
+ toast.success(t("admin.userDeletedSuccessfully", { username }));
+ fetchUsers();
+ } catch (err: any) {
+ toast.error(t("admin.failedToDeleteUser"));
+ }
+ },
+ "destructive",
+ );
+ };
+
+ const topMarginPx = isTopbarOpen ? 74 : 26;
+ const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
+ const bottomMarginPx = 8;
+ const wrapperStyle: React.CSSProperties = {
+ marginLeft: leftMarginPx,
+ marginRight: 17,
+ marginTop: topMarginPx,
+ marginBottom: bottomMarginPx,
+ height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
+ };
+
+ return (
+
+
+
+
{t("admin.title")}
+
+
+
+
+
+
+
+
+ {t("admin.general")}
+
+
+
+ OIDC
+
+
+
+ {t("admin.users")}
+
+
+
+ {t("admin.adminManagement")}
+
+
+
+
+
+
+ {t("admin.userRegistration")}
+
+
+
+ {t("admin.allowNewAccountRegistration")}
+
+
+
+
+
+
+
+ {t("admin.externalAuthentication")}
+
+
+
+ {t("admin.configureExternalProvider")}
+
+
+ window.open("https://docs.termix.site/oidc", "_blank")
+ }
+ >
+ {t("common.documentation")}
+
+
+
+ {oidcError && (
+
+ {t("common.error")}
+ {oidcError}
+
+ )}
+
+
+
+
+
+
+
+
+
+ {t("admin.userManagement")}
+
+
+ {usersLoading ? t("admin.loading") : t("admin.refresh")}
+
+
+ {usersLoading ? (
+
+ {t("admin.loadingUsers")}
+
+ ) : (
+
+
+
+
+
+ {t("admin.username")}
+
+
+ {t("admin.type")}
+
+
+ {t("admin.actions")}
+
+
+
+
+ {users.map((user) => (
+
+
+ {user.username}
+ {user.is_admin && (
+
+ {t("admin.adminBadge")}
+
+ )}
+
+
+ {user.is_oidc
+ ? t("admin.external")
+ : t("admin.local")}
+
+
+ handleDeleteUser(user.username)}
+ className="text-red-600 hover:text-red-700 hover:bg-red-50"
+ disabled={user.is_admin}
+ >
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+ {t("admin.adminManagement")}
+
+
+
{t("admin.makeUserAdmin")}
+
+
+
+
+
{t("admin.currentAdmins")}
+
+
+
+
+
+ {t("admin.username")}
+
+
+ {t("admin.type")}
+
+
+ {t("admin.actions")}
+
+
+
+
+ {users
+ .filter((u) => u.is_admin)
+ .map((admin) => (
+
+
+ {admin.username}
+
+ {t("admin.adminBadge")}
+
+
+
+ {admin.is_oidc
+ ? t("admin.external")
+ : t("admin.local")}
+
+
+
+ handleRemoveAdminStatus(admin.username)
+ }
+ className="text-orange-600 hover:text-orange-700 hover:bg-orange-50"
+ >
+
+ {t("admin.removeAdminButton")}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default AdminSettings;
diff --git a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx
new file mode 100644
index 00000000..f0401878
--- /dev/null
+++ b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx
@@ -0,0 +1,849 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Controller, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { PasswordInput } from "@/components/ui/password-input";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Separator } from "@/components/ui/separator";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import React, { useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
+import {
+ createCredential,
+ updateCredential,
+ getCredentials,
+ getCredentialDetails,
+} from "@/ui/main-axios";
+import { useTranslation } from "react-i18next";
+import type {
+ Credential,
+ CredentialEditorProps,
+ CredentialData,
+} from "../../../../types/index.js";
+
+export function CredentialEditor({
+ editingCredential,
+ onFormSubmit,
+}: CredentialEditorProps) {
+ const { t } = useTranslation();
+ const [credentials, setCredentials] = useState([]);
+ const [folders, setFolders] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [fullCredentialDetails, setFullCredentialDetails] =
+ useState(null);
+
+ const [authTab, setAuthTab] = useState<"password" | "key">("password");
+ const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">(
+ "upload",
+ );
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const credentialsData = await getCredentials();
+ setCredentials(credentialsData);
+
+ const uniqueFolders = [
+ ...new Set(
+ credentialsData
+ .filter(
+ (credential) =>
+ credential.folder && credential.folder.trim() !== "",
+ )
+ .map((credential) => credential.folder!),
+ ),
+ ].sort() as string[];
+
+ setFolders(uniqueFolders);
+ } catch (error) {
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ const fetchCredentialDetails = async () => {
+ if (editingCredential) {
+ try {
+ const fullDetails = await getCredentialDetails(editingCredential.id);
+ setFullCredentialDetails(fullDetails);
+ } catch (error) {
+ toast.error(t("credentials.failedToFetchCredentialDetails"));
+ }
+ } else {
+ setFullCredentialDetails(null);
+ }
+ };
+
+ fetchCredentialDetails();
+ }, [editingCredential, t]);
+
+ const formSchema = z
+ .object({
+ name: z.string().min(1),
+ description: z.string().optional(),
+ folder: z.string().optional(),
+ tags: z.array(z.string().min(1)).default([]),
+ authType: z.enum(["password", "key"]),
+ username: z.string().min(1),
+ password: z.string().optional(),
+ key: z.any().optional().nullable(),
+ keyPassword: z.string().optional(),
+ keyType: z
+ .enum([
+ "auto",
+ "ssh-rsa",
+ "ssh-ed25519",
+ "ecdsa-sha2-nistp256",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp521",
+ "ssh-dss",
+ "ssh-rsa-sha2-256",
+ "ssh-rsa-sha2-512",
+ ])
+ .optional(),
+ })
+ .superRefine((data, ctx) => {
+ if (data.authType === "password") {
+ if (!data.password || data.password.trim() === "") {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("credentials.passwordRequired"),
+ path: ["password"],
+ });
+ }
+ } else if (data.authType === "key") {
+ if (!data.key && !editingCredential) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("credentials.sshKeyRequired"),
+ path: ["key"],
+ });
+ }
+ }
+ });
+
+ type FormData = z.infer;
+
+ const form = useForm({
+ resolver: zodResolver(formSchema) as any,
+ defaultValues: {
+ name: "",
+ description: "",
+ folder: "",
+ tags: [],
+ authType: "password",
+ username: "",
+ password: "",
+ key: null,
+ keyPassword: "",
+ keyType: "auto",
+ },
+ });
+
+ useEffect(() => {
+ if (editingCredential && fullCredentialDetails) {
+ const defaultAuthType = fullCredentialDetails.authType;
+ setAuthTab(defaultAuthType);
+
+ setTimeout(() => {
+ const formData = {
+ name: fullCredentialDetails.name || "",
+ description: fullCredentialDetails.description || "",
+ folder: fullCredentialDetails.folder || "",
+ tags: fullCredentialDetails.tags || [],
+ authType: defaultAuthType as "password" | "key",
+ username: fullCredentialDetails.username || "",
+ password: "",
+ key: null,
+ keyPassword: "",
+ keyType: "auto" as const,
+ };
+
+ if (defaultAuthType === "password") {
+ formData.password = fullCredentialDetails.password || "";
+ } else if (defaultAuthType === "key") {
+ formData.key = "existing_key";
+ formData.keyPassword = fullCredentialDetails.keyPassword || "";
+ formData.keyType =
+ (fullCredentialDetails.keyType as any) || ("auto" as const);
+ }
+
+ form.reset(formData);
+ setTagInput("");
+ }, 100);
+ } else if (!editingCredential) {
+ setAuthTab("password");
+ form.reset({
+ name: "",
+ description: "",
+ folder: "",
+ tags: [],
+ authType: "password",
+ username: "",
+ password: "",
+ key: null,
+ keyPassword: "",
+ keyType: "auto",
+ });
+ setTagInput("");
+ }
+ }, [editingCredential?.id, fullCredentialDetails, form]);
+
+ const onSubmit = async (data: FormData) => {
+ try {
+ if (!data.name || data.name.trim() === "") {
+ data.name = data.username;
+ }
+
+ const submitData: CredentialData = {
+ name: data.name,
+ description: data.description,
+ folder: data.folder,
+ tags: data.tags,
+ authType: data.authType,
+ username: data.username,
+ keyType: data.keyType,
+ };
+
+ submitData.password = null;
+ submitData.key = null;
+ submitData.keyPassword = null;
+ submitData.keyType = null;
+
+ if (data.authType === "password") {
+ submitData.password = data.password;
+ } else if (data.authType === "key") {
+ if (data.key instanceof File) {
+ const keyContent = await data.key.text();
+ submitData.key = keyContent;
+ } else if (data.key === "existing_key") {
+ delete submitData.key;
+ } else {
+ submitData.key = data.key;
+ }
+ submitData.keyPassword = data.keyPassword;
+ submitData.keyType = data.keyType;
+ }
+
+ if (editingCredential) {
+ await updateCredential(editingCredential.id, submitData);
+ toast.success(
+ t("credentials.credentialUpdatedSuccessfully", { name: data.name }),
+ );
+ } else {
+ await createCredential(submitData);
+ toast.success(
+ t("credentials.credentialAddedSuccessfully", { name: data.name }),
+ );
+ }
+
+ if (onFormSubmit) {
+ onFormSubmit();
+ }
+
+ window.dispatchEvent(new CustomEvent("credentials:changed"));
+
+ form.reset();
+ } catch (error) {
+ toast.error(t("credentials.failedToSaveCredential"));
+ }
+ };
+
+ const [tagInput, setTagInput] = useState("");
+
+ const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
+ const folderInputRef = useRef(null);
+ const folderDropdownRef = useRef(null);
+
+ const folderValue = form.watch("folder");
+ const filteredFolders = React.useMemo(() => {
+ if (!folderValue) return folders;
+ return folders.filter((f) =>
+ f.toLowerCase().includes(folderValue.toLowerCase()),
+ );
+ }, [folderValue, folders]);
+
+ const handleFolderClick = (folder: string) => {
+ form.setValue("folder", folder);
+ setFolderDropdownOpen(false);
+ };
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ folderDropdownRef.current &&
+ !folderDropdownRef.current.contains(event.target as Node) &&
+ folderInputRef.current &&
+ !folderInputRef.current.contains(event.target as Node)
+ ) {
+ setFolderDropdownOpen(false);
+ }
+ }
+
+ if (folderDropdownOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ } else {
+ document.removeEventListener("mousedown", handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [folderDropdownOpen]);
+
+ const keyTypeOptions = [
+ { value: "auto", label: t("hosts.autoDetect") },
+ { value: "ssh-rsa", label: t("hosts.rsa") },
+ { value: "ssh-ed25519", label: t("hosts.ed25519") },
+ { value: "ecdsa-sha2-nistp256", label: t("hosts.ecdsaNistP256") },
+ { value: "ecdsa-sha2-nistp384", label: t("hosts.ecdsaNistP384") },
+ { value: "ecdsa-sha2-nistp521", label: t("hosts.ecdsaNistP521") },
+ { value: "ssh-dss", label: t("hosts.dsa") },
+ { value: "ssh-rsa-sha2-256", label: t("hosts.rsaSha2256") },
+ { value: "ssh-rsa-sha2-512", label: t("hosts.rsaSha2512") },
+ ];
+
+ const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false);
+ const keyTypeButtonRef = useRef(null);
+ const keyTypeDropdownRef = useRef(null);
+
+ useEffect(() => {
+ function onClickOutside(event: MouseEvent) {
+ if (
+ keyTypeDropdownOpen &&
+ keyTypeDropdownRef.current &&
+ !keyTypeDropdownRef.current.contains(event.target as Node) &&
+ keyTypeButtonRef.current &&
+ !keyTypeButtonRef.current.contains(event.target as Node)
+ ) {
+ setKeyTypeDropdownOpen(false);
+ }
+ }
+
+ document.addEventListener("mousedown", onClickOutside);
+ return () => document.removeEventListener("mousedown", onClickOutside);
+ }, [keyTypeDropdownOpen]);
+
+ return (
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/Credentials/CredentialSelector.tsx b/src/ui/Desktop/Apps/Credentials/CredentialSelector.tsx
new file mode 100644
index 00000000..4e9b6843
--- /dev/null
+++ b/src/ui/Desktop/Apps/Credentials/CredentialSelector.tsx
@@ -0,0 +1,226 @@
+import React, { useState, useEffect, useRef } from "react";
+import { Button } from "@/components/ui/button.tsx";
+import { Input } from "@/components/ui/input.tsx";
+import { FormControl, FormItem, FormLabel } from "@/components/ui/form.tsx";
+import { getCredentials } from "@/ui/main-axios.ts";
+import { useTranslation } from "react-i18next";
+import type { Credential } from "../../../../types";
+
+interface CredentialSelectorProps {
+ value?: number | null;
+ onValueChange: (credentialId: number | null) => void;
+ onCredentialSelect?: (credential: Credential | null) => void;
+}
+
+export function CredentialSelector({
+ value,
+ onValueChange,
+ onCredentialSelect,
+}: CredentialSelectorProps) {
+ const { t } = useTranslation();
+ const [credentials, setCredentials] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+ const [searchQuery, setSearchQuery] = useState("");
+
+ const buttonRef = useRef(null);
+ const dropdownRef = useRef(null);
+
+ useEffect(() => {
+ const fetchCredentials = async () => {
+ try {
+ setLoading(true);
+ const data = await getCredentials();
+ const credentialsArray = Array.isArray(data)
+ ? data
+ : data.credentials || data.data || [];
+ setCredentials(credentialsArray);
+ } catch (error) {
+ const { toast } = await import("sonner");
+ toast.error(t("credentials.failedToFetchCredentials"));
+ setCredentials([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchCredentials();
+ }, []);
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ dropdownRef.current &&
+ !dropdownRef.current.contains(event.target as Node) &&
+ buttonRef.current &&
+ !buttonRef.current.contains(event.target as Node)
+ ) {
+ setDropdownOpen(false);
+ }
+ }
+
+ if (dropdownOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ } else {
+ document.removeEventListener("mousedown", handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [dropdownOpen]);
+
+ const selectedCredential = credentials.find((c) => c.id === value);
+
+ const filteredCredentials = credentials.filter((credential) => {
+ if (!searchQuery) return true;
+ const searchLower = searchQuery.toLowerCase();
+ return (
+ credential.name.toLowerCase().includes(searchLower) ||
+ credential.username.toLowerCase().includes(searchLower) ||
+ (credential.folder &&
+ credential.folder.toLowerCase().includes(searchLower))
+ );
+ });
+
+ const handleCredentialSelect = (credential: Credential) => {
+ onValueChange(credential.id);
+ if (onCredentialSelect) {
+ onCredentialSelect(credential);
+ }
+ setDropdownOpen(false);
+ setSearchQuery("");
+ };
+
+ const handleClear = () => {
+ onValueChange(null);
+ if (onCredentialSelect) {
+ onCredentialSelect(null);
+ }
+ setDropdownOpen(false);
+ setSearchQuery("");
+ };
+
+ return (
+
+ {t("hosts.selectCredential")}
+
+
+
setDropdownOpen(!dropdownOpen)}
+ >
+ {loading ? (
+ t("common.loading")
+ ) : value === "existing_credential" ? (
+
+
+
+ {t("hosts.existingCredential")}
+
+
+
+ ) : selectedCredential ? (
+
+
+ {selectedCredential.name}
+
+ ({selectedCredential.username} •{" "}
+ {selectedCredential.authType})
+
+
+
+ ) : (
+ t("hosts.selectCredentialPlaceholder")
+ )}
+
+
+
+
+
+ {dropdownOpen && (
+
+
+ setSearchQuery(e.target.value)}
+ className="h-8"
+ />
+
+
+
+ {loading ? (
+
+ {t("common.loading")}
+
+ ) : filteredCredentials.length === 0 ? (
+
+ {searchQuery
+ ? t("credentials.noCredentialsMatchFilters")
+ : t("credentials.noCredentialsYet")}
+
+ ) : (
+
+ {value && (
+
+ {t("common.clear")}
+
+ )}
+ {filteredCredentials.map((credential) => (
+
handleCredentialSelect(credential)}
+ >
+
+
+
+ {credential.name}
+
+
+
+ {credential.username} • {credential.authType}
+ {credential.description &&
+ ` • ${credential.description}`}
+
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx b/src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx
new file mode 100644
index 00000000..2338f3c9
--- /dev/null
+++ b/src/ui/Desktop/Apps/Credentials/CredentialViewer.tsx
@@ -0,0 +1,533 @@
+import React, { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Sheet,
+ SheetContent,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+import {
+ Key,
+ User,
+ Calendar,
+ Hash,
+ Folder,
+ Edit3,
+ Copy,
+ Shield,
+ Clock,
+ Server,
+ Eye,
+ EyeOff,
+ AlertTriangle,
+ CheckCircle,
+ FileText,
+} from "lucide-react";
+import { getCredentialDetails, getCredentialHosts } from "@/ui/main-axios";
+import { toast } from "sonner";
+import { useTranslation } from "react-i18next";
+import type {
+ Credential,
+ HostInfo,
+ CredentialViewerProps,
+} from "../../../types/index.js";
+
+const CredentialViewer: React.FC = ({
+ credential,
+ onClose,
+ onEdit,
+}) => {
+ const { t } = useTranslation();
+ const [credentialDetails, setCredentialDetails] = useState(
+ null,
+ );
+ const [hostsUsing, setHostsUsing] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showSensitive, setShowSensitive] = useState>(
+ {},
+ );
+ const [activeTab, setActiveTab] = useState<"overview" | "security" | "usage">(
+ "overview",
+ );
+
+ useEffect(() => {
+ fetchCredentialDetails();
+ fetchHostsUsing();
+ }, [credential.id]);
+
+ const fetchCredentialDetails = async () => {
+ try {
+ const response = await getCredentialDetails(credential.id);
+ setCredentialDetails(response);
+ } catch (error) {
+ toast.error(t("credentials.failedToFetchCredentialDetails"));
+ }
+ };
+
+ const fetchHostsUsing = async () => {
+ try {
+ const response = await getCredentialHosts(credential.id);
+ setHostsUsing(response);
+ } catch (error) {
+ toast.error(t("credentials.failedToFetchHostsUsing"));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const toggleSensitiveVisibility = (field: string) => {
+ setShowSensitive((prev) => ({
+ ...prev,
+ [field]: !prev[field],
+ }));
+ };
+
+ const copyToClipboard = async (text: string, fieldName: string) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ toast.success(t("copiedToClipboard", { field: fieldName }));
+ } catch (error) {
+ toast.error(t("credentials.failedToCopy"));
+ }
+ };
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleString();
+ };
+
+ const getAuthIcon = (authType: string) => {
+ return authType === "password" ? (
+
+ ) : (
+
+ );
+ };
+
+ const renderSensitiveField = (
+ value: string | undefined,
+ fieldName: string,
+ label: string,
+ isMultiline = false,
+ ) => {
+ if (!value) return null;
+
+ const isVisible = showSensitive[fieldName];
+
+ return (
+
+
+
+ {label}
+
+
+ toggleSensitiveVisibility(fieldName)}
+ >
+ {isVisible ? (
+
+ ) : (
+
+ )}
+
+ copyToClipboard(value, label)}
+ >
+
+
+
+
+
+ {isVisible ? (
+
+ {value}
+
+ ) : (
+
+ {"•".repeat(isMultiline ? 50 : 20)}
+
+ )}
+
+
+ );
+ };
+
+ if (loading || !credentialDetails) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {getAuthIcon(credentialDetails.authType)}
+
+
+
+ {credentialDetails.name}
+
+
+ {credentialDetails.description}
+
+
+
+
+ {credentialDetails.authType}
+
+ {credentialDetails.keyType && (
+
+ {credentialDetails.keyType}
+
+ )}
+
+
+
+
+
+ {/* Tab Navigation */}
+
+ setActiveTab("overview")}
+ className="flex-1 h-10"
+ >
+
+ {t("credentials.overview")}
+
+ setActiveTab("security")}
+ className="flex-1 h-10"
+ >
+
+ {t("credentials.security")}
+
+ setActiveTab("usage")}
+ className="flex-1 h-10"
+ >
+
+ {t("credentials.usage")}
+
+
+
+ {/* Tab Content */}
+ {activeTab === "overview" && (
+
+
+
+
+ {t("credentials.basicInformation")}
+
+
+
+
+
+
+
+
+
+ {t("common.username")}
+
+
+ {credentialDetails.username}
+
+
+
+
+ {credentialDetails.folder && (
+
+
+
+
+ {t("common.folder")}
+
+
+ {credentialDetails.folder}
+
+
+
+ )}
+
+ {credentialDetails.tags.length > 0 && (
+
+
+
+
+ {t("hosts.tags")}
+
+
+ {credentialDetails.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+
+ {t("credentials.created")}
+
+
+ {formatDate(credentialDetails.createdAt)}
+
+
+
+
+
+
+
+
+ {t("credentials.lastModified")}
+
+
+ {formatDate(credentialDetails.updatedAt)}
+
+
+
+
+
+
+
+
+
+ {t("credentials.usageStatistics")}
+
+
+
+
+
+ {credentialDetails.usageCount}
+
+
+ {t("credentials.timesUsed")}
+
+
+
+ {credentialDetails.lastUsed && (
+
+
+
+
+ {t("credentials.lastUsed")}
+
+
+ {formatDate(credentialDetails.lastUsed)}
+
+
+
+ )}
+
+
+
+
+
+ {t("credentials.connectedHosts")}
+
+
{hostsUsing.length}
+
+
+
+
+
+ )}
+
+ {activeTab === "security" && (
+
+
+
+
+ {t("credentials.securityDetails")}
+
+
+ {t("credentials.securityDetailsDescription")}
+
+
+
+
+
+
+
+ {t("credentials.credentialSecured")}
+
+
+ {t("credentials.credentialSecuredDescription")}
+
+
+
+
+ {credentialDetails.authType === "password" && (
+
+
+ {t("credentials.passwordAuthentication")}
+
+ {renderSensitiveField(
+ credentialDetails.password,
+ "password",
+ t("common.password"),
+ )}
+
+ )}
+
+ {credentialDetails.authType === "key" && (
+
+
+ {t("credentials.keyAuthentication")}
+
+
+
+
+
+ {t("credentials.keyType")}
+
+
+ {credentialDetails.keyType?.toUpperCase() ||
+ t("unknown").toUpperCase()}
+
+
+
+
+ {renderSensitiveField(
+ credentialDetails.key,
+ "key",
+ t("credentials.privateKey"),
+ true,
+ )}
+
+ {credentialDetails.keyPassword &&
+ renderSensitiveField(
+ credentialDetails.keyPassword,
+ "keyPassword",
+ t("credentials.keyPassphrase"),
+ )}
+
+ )}
+
+
+
+
+
+ {t("credentials.securityReminder")}
+
+
+ {t("credentials.securityReminderText")}
+
+
+
+
+
+ )}
+
+ {activeTab === "usage" && (
+
+
+
+
+ {t("credentials.hostsUsingCredential")}
+ {hostsUsing.length}
+
+
+
+ {hostsUsing.length === 0 ? (
+
+
+
{t("credentials.noHostsUsingCredential")}
+
+ ) : (
+
+
+ {hostsUsing.map((host) => (
+
+
+
+
+
+
+
+ {host.name || `${host.ip}:${host.port}`}
+
+
+ {host.ip}:{host.port}
+
+
+
+
+ {formatDate(host.createdAt)}
+
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+
+
+
+ {t("common.close")}
+
+
+
+ {t("credentials.editCredential")}
+
+
+
+
+ );
+};
+
+export default CredentialViewer;
diff --git a/src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx b/src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx
new file mode 100644
index 00000000..b66dcacb
--- /dev/null
+++ b/src/ui/Desktop/Apps/Credentials/CredentialsManager.tsx
@@ -0,0 +1,692 @@
+import React, { useState, useEffect, useMemo, useRef } from "react";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Input } from "@/components/ui/input";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import {
+ Search,
+ Key,
+ Folder,
+ Edit,
+ Trash2,
+ Shield,
+ Pin,
+ Tag,
+ Info,
+ FolderMinus,
+ Pencil,
+ X,
+ Check,
+} from "lucide-react";
+import {
+ getCredentials,
+ deleteCredential,
+ updateCredential,
+ renameCredentialFolder,
+} from "@/ui/main-axios";
+import { toast } from "sonner";
+import { useTranslation } from "react-i18next";
+import { useConfirmation } from "@/hooks/use-confirmation.ts";
+import CredentialViewer from "./CredentialViewer";
+import type {
+ Credential,
+ CredentialsManagerProps,
+} from "../../../../types/index.js";
+
+export function CredentialsManager({
+ onEditCredential,
+}: CredentialsManagerProps) {
+ const { t } = useTranslation();
+ const { confirmWithToast } = useConfirmation();
+ const [credentials, setCredentials] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [searchQuery, setSearchQuery] = useState("");
+ const [showViewer, setShowViewer] = useState(false);
+ const [viewingCredential, setViewingCredential] = useState(
+ null,
+ );
+ const [draggedCredential, setDraggedCredential] = useState(
+ null,
+ );
+ const [dragOverFolder, setDragOverFolder] = useState(null);
+ const [editingFolder, setEditingFolder] = useState(null);
+ const [editingFolderName, setEditingFolderName] = useState("");
+ const [operationLoading, setOperationLoading] = useState(false);
+ const dragCounter = useRef(0);
+
+ useEffect(() => {
+ fetchCredentials();
+ }, []);
+
+ const fetchCredentials = async () => {
+ try {
+ setLoading(true);
+ const data = await getCredentials();
+ setCredentials(data);
+ setError(null);
+ } catch (err) {
+ setError(t("credentials.failedToFetchCredentials"));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleEdit = (credential: Credential) => {
+ if (onEditCredential) {
+ onEditCredential(credential);
+ }
+ };
+
+ const handleDelete = async (credentialId: number, credentialName: string) => {
+ confirmWithToast(
+ t("credentials.confirmDeleteCredential", { name: credentialName }),
+ async () => {
+ try {
+ await deleteCredential(credentialId);
+ toast.success(
+ t("credentials.credentialDeletedSuccessfully", {
+ name: credentialName,
+ }),
+ );
+ await fetchCredentials();
+ window.dispatchEvent(new CustomEvent("credentials:changed"));
+ } catch (err: any) {
+ if (err.response?.data?.details) {
+ toast.error(
+ `${err.response.data.error}\n${err.response.data.details}`,
+ );
+ } else {
+ toast.error(t("credentials.failedToDeleteCredential"));
+ }
+ }
+ },
+ "destructive",
+ );
+ };
+
+ const handleRemoveFromFolder = async (credential: Credential) => {
+ confirmWithToast(
+ t("credentials.confirmRemoveFromFolder", {
+ name: credential.name || credential.username,
+ folder: credential.folder,
+ }),
+ async () => {
+ try {
+ setOperationLoading(true);
+ const updatedCredential = { ...credential, folder: "" };
+ await updateCredential(credential.id, updatedCredential);
+ toast.success(
+ t("credentials.removedFromFolder", {
+ name: credential.name || credential.username,
+ }),
+ );
+ await fetchCredentials();
+ window.dispatchEvent(new CustomEvent("credentials:changed"));
+ } catch (err) {
+ toast.error(t("credentials.failedToRemoveFromFolder"));
+ } finally {
+ setOperationLoading(false);
+ }
+ },
+ );
+ };
+
+ const handleFolderRename = async (oldName: string) => {
+ if (!editingFolderName.trim() || editingFolderName === oldName) {
+ setEditingFolder(null);
+ setEditingFolderName("");
+ return;
+ }
+
+ try {
+ setOperationLoading(true);
+ await renameCredentialFolder(oldName, editingFolderName.trim());
+ toast.success(
+ t("credentials.folderRenamed", {
+ oldName,
+ newName: editingFolderName.trim(),
+ }),
+ );
+ await fetchCredentials();
+ window.dispatchEvent(new CustomEvent("credentials:changed"));
+ setEditingFolder(null);
+ setEditingFolderName("");
+ } catch (err) {
+ toast.error(t("credentials.failedToRenameFolder"));
+ } finally {
+ setOperationLoading(false);
+ }
+ };
+
+ const startFolderEdit = (folderName: string) => {
+ setEditingFolder(folderName);
+ setEditingFolderName(folderName);
+ };
+
+ const cancelFolderEdit = () => {
+ setEditingFolder(null);
+ setEditingFolderName("");
+ };
+
+ const handleDragStart = (e: React.DragEvent, credential: Credential) => {
+ setDraggedCredential(credential);
+ e.dataTransfer.effectAllowed = "move";
+ e.dataTransfer.setData("text/plain", "");
+ };
+
+ const handleDragEnd = () => {
+ setDraggedCredential(null);
+ setDragOverFolder(null);
+ dragCounter.current = 0;
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "move";
+ };
+
+ const handleDragEnter = (e: React.DragEvent, folderName: string) => {
+ e.preventDefault();
+ dragCounter.current++;
+ setDragOverFolder(folderName);
+ };
+
+ const handleDragLeave = (e: React.DragEvent) => {
+ dragCounter.current--;
+ if (dragCounter.current === 0) {
+ setDragOverFolder(null);
+ }
+ };
+
+ const handleDrop = async (e: React.DragEvent, targetFolder: string) => {
+ e.preventDefault();
+ dragCounter.current = 0;
+ setDragOverFolder(null);
+
+ if (!draggedCredential) return;
+
+ const newFolder =
+ targetFolder === t("credentials.uncategorized") ? "" : targetFolder;
+
+ if (draggedCredential.folder === newFolder) {
+ setDraggedCredential(null);
+ return;
+ }
+
+ try {
+ setOperationLoading(true);
+ const updatedCredential = { ...draggedCredential, folder: newFolder };
+ await updateCredential(draggedCredential.id, updatedCredential);
+ toast.success(
+ t("credentials.movedToFolder", {
+ name: draggedCredential.name || draggedCredential.username,
+ folder: targetFolder,
+ }),
+ );
+ await fetchCredentials();
+ window.dispatchEvent(new CustomEvent("credentials:changed"));
+ } catch (err) {
+ toast.error(t("credentials.failedToMoveToFolder"));
+ } finally {
+ setOperationLoading(false);
+ setDraggedCredential(null);
+ }
+ };
+
+ const filteredAndSortedCredentials = useMemo(() => {
+ let filtered = credentials;
+
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase();
+ filtered = credentials.filter((credential) => {
+ const searchableText = [
+ credential.name || "",
+ credential.username,
+ credential.description || "",
+ ...(credential.tags || []),
+ credential.authType,
+ credential.keyType || "",
+ ]
+ .join(" ")
+ .toLowerCase();
+ return searchableText.includes(query);
+ });
+ }
+
+ return filtered.sort((a, b) => {
+ const aName = a.name || a.username;
+ const bName = b.name || b.username;
+ return aName.localeCompare(bName);
+ });
+ }, [credentials, searchQuery]);
+
+ const credentialsByFolder = useMemo(() => {
+ const grouped: { [key: string]: Credential[] } = {};
+
+ filteredAndSortedCredentials.forEach((credential) => {
+ const folder = credential.folder || t("credentials.uncategorized");
+ if (!grouped[folder]) {
+ grouped[folder] = [];
+ }
+ grouped[folder].push(credential);
+ });
+
+ const sortedFolders = Object.keys(grouped).sort((a, b) => {
+ if (a === t("credentials.uncategorized")) return -1;
+ if (b === t("credentials.uncategorized")) return 1;
+ return a.localeCompare(b);
+ });
+
+ const sortedGrouped: { [key: string]: Credential[] } = {};
+ sortedFolders.forEach((folder) => {
+ sortedGrouped[folder] = grouped[folder];
+ });
+
+ return sortedGrouped;
+ }, [filteredAndSortedCredentials, t]);
+
+ if (loading) {
+ return (
+
+
+
+
+ {t("credentials.loadingCredentials")}
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
{error}
+
+ {t("credentials.retry")}
+
+
+
+ );
+ }
+
+ if (credentials.length === 0) {
+ return (
+
+
+
+
+ {t("credentials.sshCredentials")}
+
+
+ {t("credentials.credentialsCount", { count: 0 })}
+
+
+
+
+ {t("credentials.refresh")}
+
+
+
+
+
+
+
+
+ {t("credentials.noCredentials")}
+
+
+ {t("credentials.noCredentialsMessage")}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {t("credentials.sshCredentials")}
+
+
+ {t("credentials.credentialsCount", {
+ count: filteredAndSortedCredentials.length,
+ })}
+
+
+
+
+ {t("credentials.refresh")}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+ {Object.entries(credentialsByFolder).map(
+ ([folder, folderCredentials]) => (
+
handleDragEnter(e, folder)}
+ onDragLeave={handleDragLeave}
+ onDrop={(e) => handleDrop(e, folder)}
+ >
+
+
+
+
+
+ {editingFolder === folder ? (
+
e.stopPropagation()}
+ >
+
+ setEditingFolderName(e.target.value)
+ }
+ onKeyDown={(e) => {
+ if (e.key === "Enter")
+ handleFolderRename(folder);
+ if (e.key === "Escape") cancelFolderEdit();
+ }}
+ className="h-6 text-sm px-2 flex-1"
+ autoFocus
+ disabled={operationLoading}
+ />
+ {
+ e.stopPropagation();
+ handleFolderRename(folder);
+ }}
+ className="h-6 w-6 p-0"
+ disabled={operationLoading}
+ >
+
+
+ {
+ e.stopPropagation();
+ cancelFolderEdit();
+ }}
+ className="h-6 w-6 p-0"
+ disabled={operationLoading}
+ >
+
+
+
+ ) : (
+ <>
+
{
+ e.stopPropagation();
+ if (folder !== t("credentials.uncategorized")) {
+ startFolderEdit(folder);
+ }
+ }}
+ title={
+ folder !== t("credentials.uncategorized")
+ ? "Click to rename folder"
+ : ""
+ }
+ >
+ {folder}
+
+ {folder !== t("credentials.uncategorized") && (
+
{
+ e.stopPropagation();
+ startFolderEdit(folder);
+ }}
+ className="h-4 w-4 p-0 opacity-50 hover:opacity-100 transition-opacity"
+ title="Rename folder"
+ >
+
+
+ )}
+ >
+ )}
+
+ {folderCredentials.length}
+
+
+
+
+
+ {folderCredentials.map((credential) => (
+
+
+
+
+ handleDragStart(e, credential)
+ }
+ onDragEnd={handleDragEnd}
+ className={`bg-dark-bg-input border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-dark-hover-alt transition-all duration-200 p-3 group relative ${
+ draggedCredential?.id === credential.id
+ ? "opacity-50 scale-95"
+ : ""
+ }`}
+ onClick={() => handleEdit(credential)}
+ >
+
+
+
+
+ {credential.name ||
+ `${credential.username}`}
+
+
+
+ {credential.username}
+
+
+ {credential.authType === "password"
+ ? t("credentials.password")
+ : t("credentials.sshKey")}
+
+
+
+ {credential.folder &&
+ credential.folder !== "" && (
+
+
+ {
+ e.stopPropagation();
+ handleRemoveFromFolder(
+ credential,
+ );
+ }}
+ className="h-5 w-5 p-0 text-orange-500 hover:text-orange-700 hover:bg-orange-500/10"
+ disabled={operationLoading}
+ >
+
+
+
+
+
+ Remove from folder "
+ {credential.folder}"
+
+
+
+ )}
+
+
+ {
+ e.stopPropagation();
+ handleEdit(credential);
+ }}
+ className="h-5 w-5 p-0 hover:bg-blue-500/10"
+ >
+
+
+
+
+ Edit credential
+
+
+
+
+ {
+ e.stopPropagation();
+ handleDelete(
+ credential.id,
+ credential.name ||
+ credential.username,
+ );
+ }}
+ className="h-5 w-5 p-0 text-red-500 hover:text-red-700 hover:bg-red-500/10"
+ >
+
+
+
+
+ Delete credential
+
+
+
+
+
+
+ {credential.tags &&
+ credential.tags.length > 0 && (
+
+ {credential.tags
+ .slice(0, 6)
+ .map((tag, index) => (
+
+
+ {tag}
+
+ ))}
+ {credential.tags.length > 6 && (
+
+ +{credential.tags.length - 6}
+
+ )}
+
+ )}
+
+
+
+ {credential.authType === "password" ? (
+
+ ) : (
+
+ )}
+ {credential.authType}
+
+ {credential.authType === "key" &&
+ credential.keyType && (
+
+ {credential.keyType}
+
+ )}
+
+
+
+
+
+
+
+ Click to edit credential
+
+
+ Drag to move between folders
+
+
+
+
+
+ ))}
+
+
+
+
+
+ ),
+ )}
+
+
+
+ {showViewer && viewingCredential && (
+
setShowViewer(false)}
+ onEdit={() => {
+ setShowViewer(false);
+ handleEdit(viewingCredential);
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx b/src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx
new file mode 100644
index 00000000..a98395da
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { FileManagerTabList } from "./FileManagerTabList.tsx";
+
+interface FileManagerTopNavbarProps {
+ tabs: { id: string | number; title: string }[];
+ activeTab: string | number;
+ setActiveTab: (tab: string | number) => void;
+ closeTab: (tab: string | number) => void;
+ onHomeClick: () => void;
+}
+
+export function FIleManagerTopNavbar(
+ props: FileManagerTopNavbarProps,
+): React.ReactElement {
+ const { tabs, activeTab, setActiveTab, closeTab, onHomeClick } = props;
+
+ return (
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManager.tsx b/src/ui/Desktop/Apps/File Manager/FileManager.tsx
new file mode 100644
index 00000000..c6c1b8c8
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FileManager.tsx
@@ -0,0 +1,713 @@
+import React, { useState, useEffect, useRef } from "react";
+import { FileManagerLeftSidebar } from "@/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx";
+import { FileManagerHomeView } from "@/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx";
+import { FileManagerFileEditor } from "@/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx";
+import { FileManagerOperations } from "@/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx";
+import { Button } from "@/components/ui/button.tsx";
+import { FIleManagerTopNavbar } from "@/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx";
+import { cn } from "@/lib/utils.ts";
+import { Save, RefreshCw, Settings, Trash2 } from "lucide-react";
+import { toast } from "sonner";
+import { useTranslation } from "react-i18next";
+import {
+ getFileManagerRecent,
+ getFileManagerPinned,
+ getFileManagerShortcuts,
+ addFileManagerRecent,
+ removeFileManagerRecent,
+ addFileManagerPinned,
+ removeFileManagerPinned,
+ addFileManagerShortcut,
+ removeFileManagerShortcut,
+ readSSHFile,
+ writeSSHFile,
+ getSSHStatus,
+ connectSSH,
+} from "@/ui/main-axios.ts";
+import type { SSHHost, Tab } from "../../../types/index.js";
+
+export function FileManager({
+ onSelectView,
+ initialHost = null,
+ onClose,
+}: {
+ onSelectView?: (view: string) => void;
+ embedded?: boolean;
+ initialHost?: SSHHost | null;
+ onClose?: () => void;
+}): React.ReactElement {
+ const { t } = useTranslation();
+ const [tabs, setTabs] = useState([]);
+ const [activeTab, setActiveTab] = useState("home");
+ const [recent, setRecent] = useState([]);
+ const [pinned, setPinned] = useState([]);
+ const [shortcuts, setShortcuts] = useState([]);
+
+ const [currentHost, setCurrentHost] = useState(null);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const [showOperations, setShowOperations] = useState(false);
+ const [currentPath, setCurrentPath] = useState("/");
+
+ const [deletingItem, setDeletingItem] = useState(null);
+
+ const sidebarRef = useRef(null);
+
+ useEffect(() => {
+ if (initialHost && (!currentHost || currentHost.id !== initialHost.id)) {
+ setCurrentHost(initialHost);
+ setTimeout(() => {
+ try {
+ const path = initialHost.defaultPath || "/";
+ if (sidebarRef.current && sidebarRef.current.openFolder) {
+ sidebarRef.current.openFolder(initialHost, path);
+ }
+ } catch (e) {}
+ }, 0);
+ }
+ }, [initialHost]);
+
+ useEffect(() => {
+ if (currentHost) {
+ fetchHomeData();
+ } else {
+ setRecent([]);
+ setPinned([]);
+ setShortcuts([]);
+ }
+ }, [currentHost]);
+
+ useEffect(() => {
+ if (activeTab === "home" && currentHost) {
+ const interval = setInterval(() => {
+ fetchHomeData();
+ }, 2000);
+
+ return () => clearInterval(interval);
+ }
+ }, [activeTab, currentHost]);
+
+ async function fetchHomeData() {
+ if (!currentHost) return;
+
+ try {
+ const homeDataPromise = Promise.all([
+ getFileManagerRecent(currentHost.id),
+ getFileManagerPinned(currentHost.id),
+ getFileManagerShortcuts(currentHost.id),
+ ]);
+
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error(t("fileManager.fetchHomeDataTimeout"))),
+ 15000,
+ ),
+ );
+
+ const [recentRes, pinnedRes, shortcutsRes] = (await Promise.race([
+ homeDataPromise,
+ timeoutPromise,
+ ])) as [any, any, any];
+
+ const recentWithPinnedStatus = (recentRes || []).map((file) => ({
+ ...file,
+ type: "file",
+ isPinned: (pinnedRes || []).some(
+ (pinnedFile) =>
+ pinnedFile.path === file.path && pinnedFile.name === file.name,
+ ),
+ }));
+
+ const pinnedWithType = (pinnedRes || []).map((file) => ({
+ ...file,
+ type: "file",
+ }));
+
+ setRecent(recentWithPinnedStatus);
+ setPinned(pinnedWithType);
+ setShortcuts(
+ (shortcutsRes || []).map((shortcut) => ({
+ ...shortcut,
+ type: "directory",
+ })),
+ );
+ } catch (err: any) {
+ const { toast } = await import("sonner");
+ toast.error(t("fileManager.failedToFetchHomeData"));
+
+ if (onClose) {
+ onClose();
+ }
+ }
+ }
+
+ const formatErrorMessage = (err: any, defaultMessage: string): string => {
+ if (typeof err === "object" && err !== null && "response" in err) {
+ const axiosErr = err as any;
+ if (axiosErr.response?.status === 403) {
+ return `${t("fileManager.permissionDenied")}. ${defaultMessage}. ${t("fileManager.checkDockerLogs")}.`;
+ } else if (axiosErr.response?.status === 500) {
+ const backendError =
+ axiosErr.response?.data?.error ||
+ t("fileManager.internalServerError");
+ return `${t("fileManager.serverError")} (500): ${backendError}. ${t("fileManager.checkDockerLogs")}.`;
+ } else if (axiosErr.response?.data?.error) {
+ const backendError = axiosErr.response.data.error;
+ return `${axiosErr.response?.status ? `${t("fileManager.error")} ${axiosErr.response.status}: ` : ""}${backendError}. ${t("fileManager.checkDockerLogs")}.`;
+ } else {
+ return `${t("fileManager.requestFailed")} ${axiosErr.response?.status || t("fileManager.unknown")}. ${t("fileManager.checkDockerLogs")}.`;
+ }
+ } else if (err instanceof Error) {
+ return `${err.message}. ${t("fileManager.checkDockerLogs")}.`;
+ } else {
+ return `${defaultMessage}. ${t("fileManager.checkDockerLogs")}.`;
+ }
+ };
+
+ const handleOpenFile = async (file: any) => {
+ const tabId = file.path;
+
+ if (!tabs.find((t) => t.id === tabId)) {
+ const currentSshSessionId = currentHost?.id.toString();
+
+ setTabs([
+ ...tabs,
+ {
+ id: tabId,
+ title: file.name,
+ fileName: file.name,
+ content: "",
+ filePath: file.path,
+ isSSH: true,
+ sshSessionId: currentSshSessionId,
+ loading: true,
+ },
+ ]);
+ try {
+ const res = await readSSHFile(currentSshSessionId, file.path);
+ setTabs((tabs) =>
+ tabs.map((t) =>
+ t.id === tabId
+ ? {
+ ...t,
+ content: res.content,
+ loading: false,
+ error: undefined,
+ }
+ : t,
+ ),
+ );
+ await addFileManagerRecent({
+ name: file.name,
+ path: file.path,
+ isSSH: true,
+ sshSessionId: currentSshSessionId,
+ hostId: currentHost?.id,
+ });
+ } catch (err: any) {
+ const errorMessage = formatErrorMessage(
+ err,
+ t("fileManager.cannotReadFile"),
+ );
+ toast.error(errorMessage);
+ setTabs((tabs) =>
+ tabs.map((t) => (t.id === tabId ? { ...t, loading: false } : t)),
+ );
+ }
+ }
+ setActiveTab(tabId);
+ };
+
+ const handleRemoveRecent = async (file: any) => {
+ try {
+ await removeFileManagerRecent({
+ name: file.name,
+ path: file.path,
+ isSSH: true,
+ sshSessionId: file.sshSessionId,
+ hostId: currentHost?.id,
+ });
+ fetchHomeData();
+ } catch (err) {}
+ };
+
+ const handlePinFile = async (file: any) => {
+ try {
+ await addFileManagerPinned({
+ name: file.name,
+ path: file.path,
+ isSSH: true,
+ sshSessionId: file.sshSessionId,
+ hostId: currentHost?.id,
+ });
+ if (sidebarRef.current && sidebarRef.current.fetchFiles) {
+ sidebarRef.current.fetchFiles();
+ }
+ } catch (err) {}
+ };
+
+ const handleUnpinFile = async (file: any) => {
+ try {
+ await removeFileManagerPinned({
+ name: file.name,
+ path: file.path,
+ isSSH: true,
+ sshSessionId: file.sshSessionId,
+ hostId: currentHost?.id,
+ });
+ if (sidebarRef.current && sidebarRef.current.fetchFiles) {
+ sidebarRef.current.fetchFiles();
+ }
+ } catch (err) {}
+ };
+
+ const handleOpenShortcut = async (shortcut: any) => {
+ if (sidebarRef.current?.isOpeningShortcut) {
+ return;
+ }
+
+ if (sidebarRef.current && sidebarRef.current.openFolder) {
+ try {
+ sidebarRef.current.isOpeningShortcut = true;
+
+ const normalizedPath = shortcut.path.startsWith("/")
+ ? shortcut.path
+ : `/${shortcut.path}`;
+
+ await sidebarRef.current.openFolder(currentHost, normalizedPath);
+ } catch (err) {
+ } finally {
+ if (sidebarRef.current) {
+ sidebarRef.current.isOpeningShortcut = false;
+ }
+ }
+ } else {
+ }
+ };
+
+ const handleAddShortcut = async (folderPath: string) => {
+ try {
+ const name = folderPath.split("/").pop() || folderPath;
+ await addFileManagerShortcut({
+ name,
+ path: folderPath,
+ isSSH: true,
+ sshSessionId: currentHost?.id.toString(),
+ hostId: currentHost?.id,
+ });
+ } catch (err) {}
+ };
+
+ const handleRemoveShortcut = async (shortcut: any) => {
+ try {
+ await removeFileManagerShortcut({
+ name: shortcut.name,
+ path: shortcut.path,
+ isSSH: true,
+ sshSessionId: currentHost?.id.toString(),
+ hostId: currentHost?.id,
+ });
+ } catch (err) {}
+ };
+
+ const closeTab = (tabId: string | number) => {
+ const idx = tabs.findIndex((t) => t.id === tabId);
+ const newTabs = tabs.filter((t) => t.id !== tabId);
+ setTabs(newTabs);
+ if (activeTab === tabId) {
+ if (newTabs.length > 0) setActiveTab(newTabs[Math.max(0, idx - 1)].id);
+ else setActiveTab("home");
+ }
+ };
+
+ const setTabContent = (tabId: string | number, content: string) => {
+ setTabs((tabs) =>
+ tabs.map((t) =>
+ t.id === tabId
+ ? {
+ ...t,
+ content,
+ dirty: true,
+ error: undefined,
+ success: undefined,
+ }
+ : t,
+ ),
+ );
+ };
+
+ const handleSave = async (tab: Tab) => {
+ if (isSaving) {
+ return;
+ }
+
+ setIsSaving(true);
+
+ try {
+ if (!tab.sshSessionId) {
+ throw new Error(t("fileManager.noSshSessionId"));
+ }
+
+ if (!tab.filePath) {
+ throw new Error(t("fileManager.noFilePath"));
+ }
+
+ if (!currentHost?.id) {
+ throw new Error(t("fileManager.noCurrentHost"));
+ }
+
+ try {
+ const statusPromise = getSSHStatus(tab.sshSessionId);
+ const statusTimeoutPromise = new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error(t("fileManager.sshStatusCheckTimeout"))),
+ 10000,
+ ),
+ );
+
+ const status = (await Promise.race([
+ statusPromise,
+ statusTimeoutPromise,
+ ])) as { connected: boolean };
+
+ if (!status.connected) {
+ const connectPromise = connectSSH(tab.sshSessionId, {
+ hostId: currentHost.id,
+ ip: currentHost.ip,
+ port: currentHost.port,
+ username: currentHost.username,
+ password: currentHost.password,
+ sshKey: currentHost.key,
+ keyPassword: currentHost.keyPassword,
+ authType: currentHost.authType,
+ credentialId: currentHost.credentialId,
+ userId: currentHost.userId,
+ });
+ const connectTimeoutPromise = new Promise((_, reject) =>
+ setTimeout(
+ () => reject(new Error(t("fileManager.sshReconnectionTimeout"))),
+ 15000,
+ ),
+ );
+
+ await Promise.race([connectPromise, connectTimeoutPromise]);
+ }
+ } catch (statusErr) {}
+
+ const savePromise = writeSSHFile(
+ tab.sshSessionId,
+ tab.filePath,
+ tab.content,
+ );
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(() => {
+ reject(new Error(t("fileManager.saveOperationTimeout")));
+ }, 30000),
+ );
+
+ const result = await Promise.race([savePromise, timeoutPromise]);
+ setTabs((tabs) =>
+ tabs.map((t) =>
+ t.id === tab.id
+ ? {
+ ...t,
+ loading: false,
+ }
+ : t,
+ ),
+ );
+
+ if (result?.toast) {
+ toast[result.toast.type](result.toast.message);
+ } else {
+ toast.success(t("fileManager.fileSavedSuccessfully"));
+ }
+
+ Promise.allSettled([
+ (async () => {
+ try {
+ await addFileManagerRecent({
+ name: tab.fileName,
+ path: tab.filePath,
+ isSSH: true,
+ sshSessionId: tab.sshSessionId,
+ hostId: currentHost.id,
+ });
+ } catch (recentErr) {}
+ })(),
+ ]).then(() => {});
+ } catch (err) {
+ let errorMessage = formatErrorMessage(
+ err,
+ t("fileManager.cannotSaveFile"),
+ );
+
+ if (
+ errorMessage.includes("timed out") ||
+ errorMessage.includes("timeout")
+ ) {
+ errorMessage = t("fileManager.saveTimeout");
+ }
+
+ toast.error(`${t("fileManager.failedToSaveFile")}: ${errorMessage}`);
+ setTabs((tabs) =>
+ tabs.map((t) =>
+ t.id === tab.id
+ ? {
+ ...t,
+ loading: false,
+ }
+ : t,
+ ),
+ );
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleHostChange = (_host: SSHHost | null) => {};
+
+ const handleOperationComplete = () => {
+ if (sidebarRef.current && sidebarRef.current.fetchFiles) {
+ sidebarRef.current.fetchFiles();
+ }
+ };
+
+ const handleSuccess = (message: string) => {
+ toast.success(message);
+ };
+
+ const handleError = (error: string) => {
+ toast.error(error);
+ };
+
+ const updateCurrentPath = (newPath: string) => {
+ setCurrentPath(newPath);
+ };
+
+ const handleDeleteFromSidebar = (item: any) => {
+ setDeletingItem(item);
+ };
+
+ const performDelete = async (item: any) => {
+ if (!currentHost?.id) return;
+
+ try {
+ const { deleteSSHItem } = await import("@/ui/main-axios.ts");
+ const response = await deleteSSHItem(
+ currentHost.id.toString(),
+ item.path,
+ item.type === "directory",
+ );
+
+ if (response?.toast) {
+ toast[response.toast.type](response.toast.message);
+ } else {
+ toast.success(
+ `${item.type === "directory" ? t("fileManager.folder") : t("fileManager.file")} ${t("fileManager.deletedSuccessfully")}`,
+ );
+ }
+
+ setDeletingItem(null);
+ handleOperationComplete();
+ } catch (error: any) {
+ handleError(
+ error?.response?.data?.error || t("fileManager.failedToDeleteItem"),
+ );
+ }
+ };
+
+ if (!currentHost) {
+ return (
+
+
+ {})}
+ onOpenFile={handleOpenFile}
+ tabs={tabs}
+ ref={sidebarRef}
+ host={initialHost as SSHHost}
+ onOperationComplete={handleOperationComplete}
+ onError={handleError}
+ onSuccess={handleSuccess}
+ onPathChange={updateCurrentPath}
+ />
+
+
+
+
+ {t("fileManager.connectToServer")}
+
+
+ {t("fileManager.selectServerToEdit")}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {})}
+ onOpenFile={handleOpenFile}
+ tabs={tabs}
+ ref={sidebarRef}
+ host={currentHost as SSHHost}
+ onOperationComplete={handleOperationComplete}
+ onError={handleError}
+ onSuccess={handleSuccess}
+ onPathChange={updateCurrentPath}
+ onDeleteItem={handleDeleteFromSidebar}
+ />
+
+
+
+
+ ({ id: t.id, title: t.title }))}
+ activeTab={activeTab}
+ setActiveTab={setActiveTab}
+ closeTab={closeTab}
+ onHomeClick={() => {
+ setActiveTab("home");
+ }}
+ />
+
+
+
setShowOperations(!showOperations)}
+ className={cn(
+ "w-[30px] h-[30px]",
+ showOperations ? "bg-dark-hover border-dark-border-hover" : "",
+ )}
+ title={t("fileManager.fileOperations")}
+ >
+
+
+
+
{
+ const tab = tabs.find((t) => t.id === activeTab);
+ if (tab && !isSaving) handleSave(tab);
+ }}
+ disabled={
+ activeTab === "home" ||
+ !tabs.find((t) => t.id === activeTab)?.dirty ||
+ isSaving
+ }
+ className={cn(
+ "w-[30px] h-[30px]",
+ activeTab === "home" ||
+ !tabs.find((t) => t.id === activeTab)?.dirty ||
+ isSaving
+ ? "opacity-60 cursor-not-allowed"
+ : "",
+ )}
+ >
+ {isSaving ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {activeTab === "home" ? (
+
+ ) : (
+ (() => {
+ const tab = tabs.find((t) => t.id === activeTab);
+ if (!tab) return null;
+ return (
+
+
+
+ setTabContent(tab.id, content)
+ }
+ />
+
+
+ );
+ })()
+ )}
+
+ {showOperations && (
+
+
+
+ )}
+
+
+
+ {deletingItem && (
+
+
+
+
+
+
+
+ {t("fileManager.confirmDelete")}
+
+
+ {t("fileManager.confirmDeleteMessage", {
+ name: deletingItem.name,
+ })}
+ {deletingItem.type === "directory" &&
+ ` ${t("fileManager.deleteDirectoryWarning")}`}
+
+
+ {t("fileManager.actionCannotBeUndone")}
+
+
+ performDelete(deletingItem)}
+ className="flex-1"
+ >
+ {t("common.delete")}
+
+ setDeletingItem(null)}
+ className="flex-1"
+ >
+ {t("common.cancel")}
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx
new file mode 100644
index 00000000..3ad1f576
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx
@@ -0,0 +1,338 @@
+import React, { useEffect } from "react";
+import CodeMirror from "@uiw/react-codemirror";
+import { loadLanguage } from "@uiw/codemirror-extensions-langs";
+import { hyperLink } from "@uiw/codemirror-extensions-hyper-link";
+import { oneDark } from "@codemirror/theme-one-dark";
+import { EditorView } from "@codemirror/view";
+
+interface FileManagerCodeEditorProps {
+ content: string;
+ fileName: string;
+ onContentChange: (value: string) => void;
+}
+
+export function FileManagerFileEditor({
+ content,
+ fileName,
+ onContentChange,
+}: FileManagerCodeEditorProps) {
+ function getLanguageName(filename: string): string {
+ if (!filename || typeof filename !== "string") {
+ return "text";
+ }
+ const lastDotIndex = filename.lastIndexOf(".");
+ if (lastDotIndex === -1) {
+ return "text";
+ }
+ const ext = filename.slice(lastDotIndex + 1).toLowerCase();
+
+ switch (ext) {
+ case "ng":
+ return "angular";
+ case "apl":
+ return "apl";
+ case "asc":
+ return "asciiArmor";
+ case "ast":
+ return "asterisk";
+ case "bf":
+ return "brainfuck";
+ case "c":
+ return "c";
+ case "ceylon":
+ return "ceylon";
+ case "clj":
+ return "clojure";
+ case "cmake":
+ return "cmake";
+ case "cob":
+ case "cbl":
+ return "cobol";
+ case "coffee":
+ return "coffeescript";
+ case "lisp":
+ return "commonLisp";
+ case "cpp":
+ case "cc":
+ case "cxx":
+ return "cpp";
+ case "cr":
+ return "crystal";
+ case "cs":
+ return "csharp";
+ case "css":
+ return "css";
+ case "cypher":
+ return "cypher";
+ case "d":
+ return "d";
+ case "dart":
+ return "dart";
+ case "diff":
+ case "patch":
+ return "diff";
+ case "dockerfile":
+ return "dockerfile";
+ case "dtd":
+ return "dtd";
+ case "dylan":
+ return "dylan";
+ case "ebnf":
+ return "ebnf";
+ case "ecl":
+ return "ecl";
+ case "eiffel":
+ return "eiffel";
+ case "elm":
+ return "elm";
+ case "erl":
+ return "erlang";
+ case "factor":
+ return "factor";
+ case "fcl":
+ return "fcl";
+ case "fs":
+ return "forth";
+ case "f90":
+ case "for":
+ return "fortran";
+ case "s":
+ return "gas";
+ case "feature":
+ return "gherkin";
+ case "go":
+ return "go";
+ case "groovy":
+ return "groovy";
+ case "hs":
+ return "haskell";
+ case "hx":
+ return "haxe";
+ case "html":
+ case "htm":
+ return "html";
+ case "http":
+ return "http";
+ case "idl":
+ return "idl";
+ case "java":
+ return "java";
+ case "js":
+ case "mjs":
+ case "cjs":
+ return "javascript";
+ case "jinja2":
+ case "j2":
+ return "jinja2";
+ case "json":
+ return "json";
+ case "jsx":
+ return "jsx";
+ case "jl":
+ return "julia";
+ case "kt":
+ case "kts":
+ return "kotlin";
+ case "less":
+ return "less";
+ case "lezer":
+ return "lezer";
+ case "liquid":
+ return "liquid";
+ case "litcoffee":
+ return "livescript";
+ case "lua":
+ return "lua";
+ case "md":
+ return "markdown";
+ case "nb":
+ case "mat":
+ return "mathematica";
+ case "mbox":
+ return "mbox";
+ case "mmd":
+ return "mermaid";
+ case "mrc":
+ return "mirc";
+ case "moo":
+ return "modelica";
+ case "mscgen":
+ return "mscgen";
+ case "m":
+ return "mumps";
+ case "sql":
+ return "mysql";
+ case "nc":
+ return "nesC";
+ case "nginx":
+ return "nginx";
+ case "nix":
+ return "nix";
+ case "nsi":
+ return "nsis";
+ case "nt":
+ return "ntriples";
+ case "mm":
+ return "objectiveCpp";
+ case "octave":
+ return "octave";
+ case "oz":
+ return "oz";
+ case "pas":
+ return "pascal";
+ case "pl":
+ case "pm":
+ return "perl";
+ case "pgsql":
+ return "pgsql";
+ case "php":
+ return "php";
+ case "pig":
+ return "pig";
+ case "ps1":
+ return "powershell";
+ case "properties":
+ return "properties";
+ case "proto":
+ return "protobuf";
+ case "pp":
+ return "puppet";
+ case "py":
+ return "python";
+ case "q":
+ return "q";
+ case "r":
+ return "r";
+ case "rb":
+ return "ruby";
+ case "rs":
+ return "rust";
+ case "sas":
+ return "sas";
+ case "sass":
+ case "scss":
+ return "sass";
+ case "scala":
+ return "scala";
+ case "scm":
+ return "scheme";
+ case "shader":
+ return "shader";
+ case "sh":
+ case "bash":
+ return "shell";
+ case "siv":
+ return "sieve";
+ case "st":
+ return "smalltalk";
+ case "sol":
+ return "solidity";
+ case "solr":
+ return "solr";
+ case "rq":
+ return "sparql";
+ case "xlsx":
+ case "ods":
+ case "csv":
+ return "spreadsheet";
+ case "nut":
+ return "squirrel";
+ case "tex":
+ return "stex";
+ case "styl":
+ return "stylus";
+ case "svelte":
+ return "svelte";
+ case "swift":
+ return "swift";
+ case "tcl":
+ return "tcl";
+ case "textile":
+ return "textile";
+ case "tiddlywiki":
+ return "tiddlyWiki";
+ case "tiki":
+ return "tiki";
+ case "toml":
+ return "toml";
+ case "troff":
+ return "troff";
+ case "tsx":
+ return "tsx";
+ case "ttcn":
+ return "ttcn";
+ case "ttl":
+ case "turtle":
+ return "turtle";
+ case "ts":
+ return "typescript";
+ case "vb":
+ return "vb";
+ case "vbs":
+ return "vbscript";
+ case "vm":
+ return "velocity";
+ case "v":
+ return "verilog";
+ case "vhd":
+ case "vhdl":
+ return "vhdl";
+ case "vue":
+ return "vue";
+ case "wat":
+ return "wast";
+ case "webidl":
+ return "webIDL";
+ case "xq":
+ case "xquery":
+ return "xQuery";
+ case "xml":
+ return "xml";
+ case "yacas":
+ return "yacas";
+ case "yaml":
+ case "yml":
+ return "yaml";
+ case "z80":
+ return "z80";
+ default:
+ return "text";
+ }
+ }
+
+ useEffect(() => {
+ document.body.style.overflowX = "hidden";
+ return () => {
+ document.body.style.overflowX = "";
+ };
+ }, []);
+
+ return (
+
+
+ onContentChange(value)}
+ theme={undefined}
+ height="100%"
+ basicSetup={{ lineNumbers: true }}
+ className="min-h-full min-w-full flex-1"
+ />
+
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx
new file mode 100644
index 00000000..cc0feb26
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx
@@ -0,0 +1,234 @@
+import React from "react";
+import { Button } from "@/components/ui/button.tsx";
+import { Trash2, Folder, File, Plus, Pin } from "lucide-react";
+import {
+ Tabs,
+ TabsList,
+ TabsTrigger,
+ TabsContent,
+} from "@/components/ui/tabs.tsx";
+import { Input } from "@/components/ui/input.tsx";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import type { FileItem, ShortcutItem } from "../../../types/index";
+
+interface FileManagerHomeViewProps {
+ recent: FileItem[];
+ pinned: FileItem[];
+ shortcuts: ShortcutItem[];
+ onOpenFile: (file: FileItem) => void;
+ onRemoveRecent: (file: FileItem) => void;
+ onPinFile: (file: FileItem) => void;
+ onUnpinFile: (file: FileItem) => void;
+ onOpenShortcut: (shortcut: ShortcutItem) => void;
+ onRemoveShortcut: (shortcut: ShortcutItem) => void;
+ onAddShortcut: (path: string) => void;
+}
+
+export function FileManagerHomeView({
+ recent,
+ pinned,
+ shortcuts,
+ onOpenFile,
+ onRemoveRecent,
+ onPinFile,
+ onUnpinFile,
+ onOpenShortcut,
+ onRemoveShortcut,
+ onAddShortcut,
+}: FileManagerHomeViewProps) {
+ const { t } = useTranslation();
+ const [tab, setTab] = useState<"recent" | "pinned" | "shortcuts">("recent");
+ const [newShortcut, setNewShortcut] = useState("");
+
+ const renderFileCard = (
+ file: FileItem,
+ onRemove: () => void,
+ onPin?: () => void,
+ isPinned = false,
+ ) => (
+
+
onOpenFile(file)}
+ >
+ {file.type === "directory" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {onPin && (
+
+
+
+ )}
+ {onRemove && (
+
+
+
+ )}
+
+
+ );
+
+ const renderShortcutCard = (shortcut: ShortcutItem) => (
+
+
onOpenShortcut(shortcut)}
+ >
+
+
+
+
+ onRemoveShortcut(shortcut)}
+ >
+
+
+
+
+ );
+
+ return (
+
+
setTab(v as "recent" | "pinned" | "shortcuts")}
+ className="w-full"
+ >
+
+
+ {t("fileManager.recent")}
+
+
+ {t("fileManager.pinned")}
+
+
+ {t("fileManager.folderShortcuts")}
+
+
+
+
+
+ {recent.length === 0 ? (
+
+
+ {t("fileManager.noRecentFiles")}
+
+
+ ) : (
+ recent.map((file) =>
+ renderFileCard(
+ file,
+ () => onRemoveRecent(file),
+ () => (file.isPinned ? onUnpinFile(file) : onPinFile(file)),
+ file.isPinned,
+ ),
+ )
+ )}
+
+
+
+
+
+ {pinned.length === 0 ? (
+
+
+ {t("fileManager.noPinnedFiles")}
+
+
+ ) : (
+ pinned.map((file) =>
+ renderFileCard(file, undefined, () => onUnpinFile(file), true),
+ )
+ )}
+
+
+
+
+
+
setNewShortcut(e.target.value)}
+ className="flex-1 bg-dark-bg-button border-2 border-dark-border text-white placeholder:text-muted-foreground"
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && newShortcut.trim()) {
+ onAddShortcut(newShortcut.trim());
+ setNewShortcut("");
+ }
+ }}
+ />
+
{
+ if (newShortcut.trim()) {
+ onAddShortcut(newShortcut.trim());
+ setNewShortcut("");
+ }
+ }}
+ >
+
+ {t("common.add")}
+
+
+
+ {shortcuts.length === 0 ? (
+
+
+ {t("fileManager.noShortcuts")}
+
+
+ ) : (
+ shortcuts.map((shortcut) => renderShortcutCard(shortcut))
+ )}
+
+
+
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx
new file mode 100644
index 00000000..0cd317ea
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx
@@ -0,0 +1,630 @@
+import React, {
+ useEffect,
+ useState,
+ useRef,
+ forwardRef,
+ useImperativeHandle,
+} from "react";
+import {
+ Folder,
+ File,
+ ArrowUp,
+ Pin,
+ MoreVertical,
+ Trash2,
+ Edit3,
+} from "lucide-react";
+import { ScrollArea } from "@/components/ui/scroll-area.tsx";
+import { cn } from "@/lib/utils.ts";
+import { Input } from "@/components/ui/input.tsx";
+import { Button } from "@/components/ui/button.tsx";
+import { toast } from "sonner";
+import { useTranslation } from "react-i18next";
+import {
+ listSSHFiles,
+ renameSSHItem,
+ deleteSSHItem,
+ getFileManagerPinned,
+ addFileManagerPinned,
+ removeFileManagerPinned,
+ getSSHStatus,
+ connectSSH,
+} from "@/ui/main-axios.ts";
+import type { SSHHost } from "../../../types/index.js";
+
+const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
+ {
+ onOpenFile,
+ tabs,
+ host,
+ onOperationComplete,
+ onPathChange,
+ onDeleteItem,
+ }: {
+ onSelectView?: (view: string) => void;
+ onOpenFile: (file: any) => void;
+ tabs: any[];
+ host: SSHHost;
+ onOperationComplete?: () => void;
+ onError?: (error: string) => void;
+ onSuccess?: (message: string) => void;
+ onPathChange?: (path: string) => void;
+ onDeleteItem?: (item: any) => void;
+ },
+ ref,
+) {
+ const { t } = useTranslation();
+ const [currentPath, setCurrentPath] = useState("/");
+ const [files, setFiles] = useState([]);
+ const pathInputRef = useRef(null);
+
+ const [search, setSearch] = useState("");
+ const [debouncedSearch, setDebouncedSearch] = useState("");
+ const [fileSearch, setFileSearch] = useState("");
+ const [debouncedFileSearch, setDebouncedFileSearch] = useState("");
+ useEffect(() => {
+ const handler = setTimeout(() => setDebouncedSearch(search), 200);
+ return () => clearTimeout(handler);
+ }, [search]);
+ useEffect(() => {
+ const handler = setTimeout(() => setDebouncedSearch(fileSearch), 200);
+ return () => clearTimeout(handler);
+ }, [fileSearch]);
+
+ const [sshSessionId, setSshSessionId] = useState(null);
+ const [filesLoading, setFilesLoading] = useState(false);
+ const [connectingSSH, setConnectingSSH] = useState(false);
+ const [connectionCache, setConnectionCache] = useState<
+ Record<
+ string,
+ {
+ sessionId: string;
+ timestamp: number;
+ }
+ >
+ >({});
+ const [fetchingFiles, setFetchingFiles] = useState(false);
+
+ const [contextMenu, setContextMenu] = useState<{
+ visible: boolean;
+ x: number;
+ y: number;
+ item: any;
+ }>({
+ visible: false,
+ x: 0,
+ y: 0,
+ item: null,
+ });
+
+ const [renamingItem, setRenamingItem] = useState<{
+ item: any;
+ newName: string;
+ } | null>(null);
+
+ useEffect(() => {
+ const nextPath = host?.defaultPath || "/";
+ setCurrentPath(nextPath);
+ onPathChange?.(nextPath);
+ (async () => {
+ await connectToSSH(host);
+ })();
+ }, [host?.id]);
+
+ async function connectToSSH(server: SSHHost): Promise {
+ const sessionId = server.id.toString();
+
+ const cached = connectionCache[sessionId];
+ if (cached && Date.now() - cached.timestamp < 30000) {
+ setSshSessionId(cached.sessionId);
+ return cached.sessionId;
+ }
+
+ if (connectingSSH) {
+ return null;
+ }
+
+ setConnectingSSH(true);
+
+ try {
+ if (!server.password && !server.key) {
+ toast.error(t("common.noAuthCredentials"));
+ return null;
+ }
+
+ const connectionConfig = {
+ hostId: server.id,
+ ip: server.ip,
+ port: server.port,
+ username: server.username,
+ password: server.password,
+ sshKey: server.key,
+ keyPassword: server.keyPassword,
+ authType: server.authType,
+ credentialId: server.credentialId,
+ userId: server.userId,
+ };
+
+ await connectSSH(sessionId, connectionConfig);
+
+ setSshSessionId(sessionId);
+
+ setConnectionCache((prev) => ({
+ ...prev,
+ [sessionId]: { sessionId, timestamp: Date.now() },
+ }));
+
+ return sessionId;
+ } catch (err: any) {
+ toast.error(
+ err?.response?.data?.error || t("fileManager.failedToConnectSSH"),
+ );
+ setSshSessionId(null);
+ return null;
+ } finally {
+ setConnectingSSH(false);
+ }
+ }
+
+ async function fetchFiles() {
+ if (fetchingFiles) {
+ return;
+ }
+
+ setFetchingFiles(true);
+ setFiles([]);
+ setFilesLoading(true);
+
+ try {
+ let pinnedFiles: any[] = [];
+ try {
+ if (host) {
+ pinnedFiles = await getFileManagerPinned(host.id);
+ }
+ } catch (err) {}
+
+ if (host && sshSessionId) {
+ let res: any[] = [];
+
+ try {
+ const status = await getSSHStatus(sshSessionId);
+ if (!status.connected) {
+ const newSessionId = await connectToSSH(host);
+ if (newSessionId) {
+ setSshSessionId(newSessionId);
+ res = await listSSHFiles(newSessionId, currentPath);
+ } else {
+ throw new Error(t("fileManager.failedToReconnectSSH"));
+ }
+ } else {
+ res = await listSSHFiles(sshSessionId, currentPath);
+ }
+ } catch (sessionErr) {
+ const newSessionId = await connectToSSH(host);
+ if (newSessionId) {
+ setSshSessionId(newSessionId);
+ res = await listSSHFiles(newSessionId, currentPath);
+ } else {
+ throw sessionErr;
+ }
+ }
+
+ const processedFiles = (res || []).map((f: any) => {
+ const filePath =
+ currentPath + (currentPath.endsWith("/") ? "" : "/") + f.name;
+ const isPinned = pinnedFiles.some(
+ (pinned) => pinned.path === filePath,
+ );
+ return {
+ ...f,
+ path: filePath,
+ isPinned,
+ isSSH: true,
+ sshSessionId: sshSessionId,
+ };
+ });
+
+ setFiles(processedFiles);
+ }
+ } catch (err: any) {
+ setFiles([]);
+ toast.error(
+ err?.response?.data?.error ||
+ err?.message ||
+ t("fileManager.failedToListFiles"),
+ );
+ } finally {
+ setFilesLoading(false);
+ setFetchingFiles(false);
+ }
+ }
+
+ useEffect(() => {
+ if (host && sshSessionId && !connectingSSH && !fetchingFiles) {
+ const timeoutId = setTimeout(() => {
+ fetchFiles();
+ }, 100);
+ return () => clearTimeout(timeoutId);
+ }
+ }, [currentPath, host, sshSessionId]);
+
+ useImperativeHandle(ref, () => ({
+ openFolder: async (_server: SSHHost, path: string) => {
+ if (connectingSSH || fetchingFiles) {
+ return;
+ }
+
+ if (currentPath === path) {
+ setTimeout(() => fetchFiles(), 100);
+ return;
+ }
+
+ setFetchingFiles(false);
+ setFilesLoading(false);
+ setFiles([]);
+
+ setCurrentPath(path);
+ onPathChange?.(path);
+ if (!sshSessionId) {
+ const sessionId = await connectToSSH(host);
+ if (sessionId) setSshSessionId(sessionId);
+ }
+ },
+ fetchFiles: () => {
+ if (host && sshSessionId) {
+ fetchFiles();
+ }
+ },
+ getCurrentPath: () => currentPath,
+ }));
+
+ useEffect(() => {
+ if (pathInputRef.current) {
+ pathInputRef.current.scrollLeft = pathInputRef.current.scrollWidth;
+ }
+ }, [currentPath]);
+
+ const filteredFiles = files.filter((file) => {
+ const q = debouncedFileSearch.trim().toLowerCase();
+ if (!q) return true;
+ return file.name.toLowerCase().includes(q);
+ });
+
+ const handleContextMenu = (e: React.MouseEvent, item: any) => {
+ e.preventDefault();
+
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ const menuWidth = 160;
+ const menuHeight = 80;
+
+ let x = e.clientX;
+ let y = e.clientY;
+
+ if (x + menuWidth > viewportWidth) {
+ x = e.clientX - menuWidth;
+ }
+
+ if (y + menuHeight > viewportHeight) {
+ y = e.clientY - menuHeight;
+ }
+
+ if (x < 0) {
+ x = 0;
+ }
+
+ if (y < 0) {
+ y = 0;
+ }
+
+ setContextMenu({
+ visible: true,
+ x,
+ y,
+ item,
+ });
+ };
+
+ const closeContextMenu = () => {
+ setContextMenu({ visible: false, x: 0, y: 0, item: null });
+ };
+
+ const handleRename = async (item: any, newName: string) => {
+ if (!sshSessionId || !newName.trim() || newName === item.name) {
+ setRenamingItem(null);
+ return;
+ }
+
+ try {
+ await renameSSHItem(sshSessionId, item.path, newName.trim());
+ toast.success(
+ `${item.type === "directory" ? t("common.folder") : t("common.file")} ${t("common.renamedSuccessfully")}`,
+ );
+ setRenamingItem(null);
+ if (onOperationComplete) {
+ onOperationComplete();
+ } else {
+ fetchFiles();
+ }
+ } catch (error: any) {
+ toast.error(
+ error?.response?.data?.error || t("fileManager.failedToRenameItem"),
+ );
+ }
+ };
+
+ const startRename = (item: any) => {
+ setRenamingItem({ item, newName: item.name });
+ closeContextMenu();
+ };
+
+ const startDelete = (item: any) => {
+ onDeleteItem?.(item);
+ closeContextMenu();
+ };
+
+ useEffect(() => {
+ const handleClickOutside = () => closeContextMenu();
+ document.addEventListener("click", handleClickOutside);
+ return () => document.removeEventListener("click", handleClickOutside);
+ }, []);
+
+ const handlePathChange = (newPath: string) => {
+ setCurrentPath(newPath);
+ onPathChange?.(newPath);
+ };
+
+ return (
+
+
+
+ {host && (
+
+
+
{
+ let path = currentPath;
+ if (path && path !== "/" && path !== "") {
+ if (path.endsWith("/")) path = path.slice(0, -1);
+ const lastSlash = path.lastIndexOf("/");
+ if (lastSlash > 0) {
+ handlePathChange(path.slice(0, lastSlash));
+ } else {
+ handlePathChange("/");
+ }
+ } else {
+ handlePathChange("/");
+ }
+ }}
+ >
+
+
+
handlePathChange(e.target.value)}
+ className="flex-1 bg-dark-bg border-2 border-dark-border-hover text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-dark-border-light"
+ />
+
+
+ setFileSearch(e.target.value)}
+ />
+
+
+
+
+ {connectingSSH || filesLoading ? (
+
+ {t("common.loading")}
+
+ ) : filteredFiles.length === 0 ? (
+
+ {t("fileManager.noFilesOrFoldersFound")}
+
+ ) : (
+
+ {filteredFiles.map((item: any) => {
+ const isOpen = (tabs || []).some(
+ (t: any) => t.id === item.path,
+ );
+ const isRenaming =
+ renamingItem?.item?.path === item.path;
+ const isDeleting = false;
+
+ return (
+
+ !isOpen && handleContextMenu(e, item)
+ }
+ >
+ {isRenaming ? (
+
+ {item.type === "directory" ? (
+
+ ) : (
+
+ )}
+
+ setRenamingItem((prev) =>
+ prev
+ ? {
+ ...prev,
+ newName: e.target.value,
+ }
+ : null,
+ )
+ }
+ className="flex-1 h-6 text-sm bg-dark-bg-button border border-dark-border-hover text-white"
+ autoFocus
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleRename(
+ item,
+ renamingItem.newName,
+ );
+ } else if (e.key === "Escape") {
+ setRenamingItem(null);
+ }
+ }}
+ onBlur={() =>
+ handleRename(item, renamingItem.newName)
+ }
+ />
+
+ ) : (
+ <>
+
+ !isOpen &&
+ (item.type === "directory"
+ ? handlePathChange(item.path)
+ : onOpenFile({
+ name: item.name,
+ path: item.path,
+ isSSH: item.isSSH,
+ sshSessionId: item.sshSessionId,
+ }))
+ }
+ >
+ {item.type === "directory" ? (
+
+ ) : (
+
+ )}
+
+ {item.name}
+
+
+
+ {item.type === "file" && (
+
{
+ e.stopPropagation();
+ try {
+ if (item.isPinned) {
+ await removeFileManagerPinned({
+ name: item.name,
+ path: item.path,
+ hostId: host?.id,
+ isSSH: true,
+ sshSessionId:
+ host?.id.toString(),
+ });
+ setFiles(
+ files.map((f) =>
+ f.path === item.path
+ ? {
+ ...f,
+ isPinned: false,
+ }
+ : f,
+ ),
+ );
+ } else {
+ await addFileManagerPinned({
+ name: item.name,
+ path: item.path,
+ hostId: host?.id,
+ isSSH: true,
+ sshSessionId:
+ host?.id.toString(),
+ });
+ setFiles(
+ files.map((f) =>
+ f.path === item.path
+ ? {
+ ...f,
+ isPinned: true,
+ }
+ : f,
+ ),
+ );
+ }
+ } catch (err) {}
+ }}
+ >
+
+
+ )}
+ {!isOpen && (
+
{
+ e.stopPropagation();
+ handleContextMenu(e, item);
+ }}
+ >
+
+
+ )}
+
+ >
+ )}
+
+ );
+ })}
+
+ )}
+
+
+
+
+ )}
+
+
+
+ {contextMenu.visible && contextMenu.item && (
+
+ startRename(contextMenu.item)}
+ >
+
+ Rename
+
+ startDelete(contextMenu.item)}
+ >
+
+ Delete
+
+
+ )}
+
+ );
+});
+
+export { FileManagerLeftSidebar };
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx
new file mode 100644
index 00000000..cf88b044
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FileManagerLeftSidebarFileViewer.tsx
@@ -0,0 +1,128 @@
+import React from "react";
+import { Button } from "@/components/ui/button.tsx";
+import { Card } from "@/components/ui/card.tsx";
+import { Folder, File, Trash2, Pin } from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+interface SSHConnection {
+ id: string;
+ name: string;
+ ip: string;
+ port: number;
+ username: string;
+ isPinned?: boolean;
+}
+
+interface FileItem {
+ name: string;
+ type: "file" | "directory" | "link";
+ path: string;
+ isStarred?: boolean;
+}
+
+interface FileManagerLeftSidebarVileViewerProps {
+ sshConnections: SSHConnection[];
+ onAddSSH: () => void;
+ onConnectSSH: (conn: SSHConnection) => void;
+ onEditSSH: (conn: SSHConnection) => void;
+ onDeleteSSH: (conn: SSHConnection) => void;
+ onPinSSH: (conn: SSHConnection) => void;
+ currentPath: string;
+ files: FileItem[];
+ onOpenFile: (file: FileItem) => void;
+ onOpenFolder: (folder: FileItem) => void;
+ onStarFile: (file: FileItem) => void;
+ onDeleteFile: (file: FileItem) => void;
+ isLoading?: boolean;
+ error?: string;
+ isSSHMode: boolean;
+ onSwitchToLocal: () => void;
+ onSwitchToSSH: (conn: SSHConnection) => void;
+ currentSSH?: SSHConnection;
+}
+
+export function FileManagerLeftSidebarFileViewer({
+ currentPath,
+ files,
+ onOpenFile,
+ onOpenFolder,
+ onStarFile,
+ onDeleteFile,
+ isLoading,
+ error,
+ isSSHMode,
+}: FileManagerLeftSidebarVileViewerProps) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+ {isSSHMode ? t("common.sshPath") : t("common.localPath")}
+
+ {currentPath}
+
+ {isLoading ? (
+
+ {t("common.loading")}
+
+ ) : error ? (
+
{error}
+ ) : (
+
+ {files.map((item) => (
+
+
+ item.type === "directory"
+ ? onOpenFolder(item)
+ : onOpenFile(item)
+ }
+ >
+ {item.type === "directory" ? (
+
+ ) : (
+
+ )}
+
+ {item.name}
+
+
+
+
onStarFile(item)}
+ >
+
+
+
onDeleteFile(item)}
+ >
+
+
+
+
+ ))}
+ {files.length === 0 && (
+
+ No files or folders found.
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx
new file mode 100644
index 00000000..2c4bcfb4
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx
@@ -0,0 +1,805 @@
+import React, { useState, useRef, useEffect } from "react";
+import { Button } from "@/components/ui/button.tsx";
+import { Input } from "@/components/ui/input.tsx";
+import { Card } from "@/components/ui/card.tsx";
+import { Separator } from "@/components/ui/separator.tsx";
+import {
+ Upload,
+ FilePlus,
+ FolderPlus,
+ Trash2,
+ Edit3,
+ X,
+ AlertCircle,
+ FileText,
+ Folder,
+} from "lucide-react";
+import { cn } from "@/lib/utils.ts";
+import { useTranslation } from "react-i18next";
+import type { FileManagerOperationsProps } from "../../../types/index.js";
+
+export function FileManagerOperations({
+ currentPath,
+ sshSessionId,
+ onOperationComplete,
+ onError,
+ onSuccess,
+}: FileManagerOperationsProps) {
+ const { t } = useTranslation();
+ const [showUpload, setShowUpload] = useState(false);
+ const [showCreateFile, setShowCreateFile] = useState(false);
+ const [showCreateFolder, setShowCreateFolder] = useState(false);
+ const [showDelete, setShowDelete] = useState(false);
+ const [showRename, setShowRename] = useState(false);
+
+ const [uploadFile, setUploadFile] = useState(null);
+ const [newFileName, setNewFileName] = useState("");
+ const [newFolderName, setNewFolderName] = useState("");
+ const [deletePath, setDeletePath] = useState("");
+ const [deleteIsDirectory, setDeleteIsDirectory] = useState(false);
+ const [renamePath, setRenamePath] = useState("");
+ const [renameIsDirectory, setRenameIsDirectory] = useState(false);
+ const [newName, setNewName] = useState("");
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [showTextLabels, setShowTextLabels] = useState(true);
+ const fileInputRef = useRef(null);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ const checkContainerWidth = () => {
+ if (containerRef.current) {
+ const width = containerRef.current.offsetWidth;
+ setShowTextLabels(width > 240);
+ }
+ };
+
+ checkContainerWidth();
+
+ const resizeObserver = new ResizeObserver(checkContainerWidth);
+ if (containerRef.current) {
+ resizeObserver.observe(containerRef.current);
+ }
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, []);
+
+ const handleFileUpload = async () => {
+ if (!uploadFile || !sshSessionId) return;
+
+ setIsLoading(true);
+
+ const { toast } = await import("sonner");
+ const loadingToast = toast.loading(
+ t("fileManager.uploadingFile", { name: uploadFile.name }),
+ );
+
+ try {
+ const content = await uploadFile.text();
+ const { uploadSSHFile } = await import("@/ui/main-axios.ts");
+
+ const response = await uploadSSHFile(
+ sshSessionId,
+ currentPath,
+ uploadFile.name,
+ content,
+ );
+
+ toast.dismiss(loadingToast);
+
+ if (response?.toast) {
+ toast[response.toast.type](response.toast.message);
+ } else {
+ onSuccess(
+ t("fileManager.fileUploadedSuccessfully", { name: uploadFile.name }),
+ );
+ }
+
+ setShowUpload(false);
+ setUploadFile(null);
+ onOperationComplete();
+ } catch (error: any) {
+ toast.dismiss(loadingToast);
+ onError(
+ error?.response?.data?.error || t("fileManager.failedToUploadFile"),
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleCreateFile = async () => {
+ if (!newFileName.trim() || !sshSessionId) return;
+
+ setIsLoading(true);
+
+ const { toast } = await import("sonner");
+ const loadingToast = toast.loading(
+ t("fileManager.creatingFile", { name: newFileName.trim() }),
+ );
+
+ try {
+ const { createSSHFile } = await import("@/ui/main-axios.ts");
+
+ const response = await createSSHFile(
+ sshSessionId,
+ currentPath,
+ newFileName.trim(),
+ );
+
+ toast.dismiss(loadingToast);
+
+ if (response?.toast) {
+ toast[response.toast.type](response.toast.message);
+ } else {
+ onSuccess(
+ t("fileManager.fileCreatedSuccessfully", {
+ name: newFileName.trim(),
+ }),
+ );
+ }
+
+ setShowCreateFile(false);
+ setNewFileName("");
+ onOperationComplete();
+ } catch (error: any) {
+ toast.dismiss(loadingToast);
+ onError(
+ error?.response?.data?.error || t("fileManager.failedToCreateFile"),
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleCreateFolder = async () => {
+ if (!newFolderName.trim() || !sshSessionId) return;
+
+ setIsLoading(true);
+
+ const { toast } = await import("sonner");
+ const loadingToast = toast.loading(
+ t("fileManager.creatingFolder", { name: newFolderName.trim() }),
+ );
+
+ try {
+ const { createSSHFolder } = await import("@/ui/main-axios.ts");
+
+ const response = await createSSHFolder(
+ sshSessionId,
+ currentPath,
+ newFolderName.trim(),
+ );
+
+ toast.dismiss(loadingToast);
+
+ if (response?.toast) {
+ toast[response.toast.type](response.toast.message);
+ } else {
+ onSuccess(
+ t("fileManager.folderCreatedSuccessfully", {
+ name: newFolderName.trim(),
+ }),
+ );
+ }
+
+ setShowCreateFolder(false);
+ setNewFolderName("");
+ onOperationComplete();
+ } catch (error: any) {
+ toast.dismiss(loadingToast);
+ onError(
+ error?.response?.data?.error || t("fileManager.failedToCreateFolder"),
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleDelete = async () => {
+ if (!deletePath || !sshSessionId) return;
+
+ setIsLoading(true);
+
+ const { toast } = await import("sonner");
+ const loadingToast = toast.loading(
+ t("fileManager.deletingItem", {
+ type: deleteIsDirectory
+ ? t("fileManager.folder")
+ : t("fileManager.file"),
+ name: deletePath.split("/").pop(),
+ }),
+ );
+
+ try {
+ const { deleteSSHItem } = await import("@/ui/main-axios.ts");
+
+ const response = await deleteSSHItem(
+ sshSessionId,
+ deletePath,
+ deleteIsDirectory,
+ );
+
+ toast.dismiss(loadingToast);
+
+ if (response?.toast) {
+ toast[response.toast.type](response.toast.message);
+ } else {
+ onSuccess(
+ t("fileManager.itemDeletedSuccessfully", {
+ type: deleteIsDirectory
+ ? t("fileManager.folder")
+ : t("fileManager.file"),
+ }),
+ );
+ }
+
+ setShowDelete(false);
+ setDeletePath("");
+ setDeleteIsDirectory(false);
+ onOperationComplete();
+ } catch (error: any) {
+ toast.dismiss(loadingToast);
+ onError(
+ error?.response?.data?.error || t("fileManager.failedToDeleteItem"),
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleRename = async () => {
+ if (!renamePath || !newName.trim() || !sshSessionId) return;
+
+ setIsLoading(true);
+
+ const { toast } = await import("sonner");
+ const loadingToast = toast.loading(
+ t("fileManager.renamingItem", {
+ type: renameIsDirectory
+ ? t("fileManager.folder")
+ : t("fileManager.file"),
+ oldName: renamePath.split("/").pop(),
+ newName: newName.trim(),
+ }),
+ );
+
+ try {
+ const { renameSSHItem } = await import("@/ui/main-axios.ts");
+
+ const response = await renameSSHItem(
+ sshSessionId,
+ renamePath,
+ newName.trim(),
+ );
+
+ toast.dismiss(loadingToast);
+
+ if (response?.toast) {
+ toast[response.toast.type](response.toast.message);
+ } else {
+ onSuccess(
+ t("fileManager.itemRenamedSuccessfully", {
+ type: renameIsDirectory
+ ? t("fileManager.folder")
+ : t("fileManager.file"),
+ }),
+ );
+ }
+
+ setShowRename(false);
+ setRenamePath("");
+ setRenameIsDirectory(false);
+ setNewName("");
+ onOperationComplete();
+ } catch (error: any) {
+ toast.dismiss(loadingToast);
+ onError(
+ error?.response?.data?.error || t("fileManager.failedToRenameItem"),
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const openFileDialog = () => {
+ fileInputRef.current?.click();
+ };
+
+ const handleFileSelect = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ if (file) {
+ setUploadFile(file);
+ }
+ };
+
+ const resetStates = () => {
+ setShowUpload(false);
+ setShowCreateFile(false);
+ setShowCreateFolder(false);
+ setShowDelete(false);
+ setShowRename(false);
+ setUploadFile(null);
+ setNewFileName("");
+ setNewFolderName("");
+ setDeletePath("");
+ setDeleteIsDirectory(false);
+ setRenamePath("");
+ setRenameIsDirectory(false);
+ setNewName("");
+ };
+
+ if (!sshSessionId) {
+ return (
+
+
+
+ {t("fileManager.connectToSsh")}
+
+
+ );
+ }
+
+ return (
+
+
+ setShowUpload(true)}
+ className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
+ title={t("fileManager.uploadFile")}
+ >
+
+ {showTextLabels && (
+ {t("fileManager.uploadFile")}
+ )}
+
+ setShowCreateFile(true)}
+ className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
+ title={t("fileManager.newFile")}
+ >
+
+ {showTextLabels && (
+ {t("fileManager.newFile")}
+ )}
+
+ setShowCreateFolder(true)}
+ className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
+ title={t("fileManager.newFolder")}
+ >
+
+ {showTextLabels && (
+ {t("fileManager.newFolder")}
+ )}
+
+ setShowRename(true)}
+ className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
+ title={t("fileManager.rename")}
+ >
+
+ {showTextLabels && (
+ {t("fileManager.rename")}
+ )}
+
+ setShowDelete(true)}
+ className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover col-span-2"
+ title={t("fileManager.deleteItem")}
+ >
+
+ {showTextLabels && (
+ {t("fileManager.deleteItem")}
+ )}
+
+
+
+
+
+
+
+
+ {t("fileManager.currentPath")}:
+
+
+ {currentPath}
+
+
+
+
+
+
+
+ {showUpload && (
+
+
+
+
+
+
+ {t("fileManager.uploadFileTitle")}
+
+
+
+ {t("fileManager.maxFileSize")}
+
+
+
setShowUpload(false)}
+ className="h-8 w-8 p-0 flex-shrink-0 ml-2"
+ >
+
+
+
+
+
+
+ {uploadFile ? (
+
+
+
+ {uploadFile.name}
+
+
+ {(uploadFile.size / 1024).toFixed(2)} KB
+
+
setUploadFile(null)}
+ className="w-full text-sm h-8"
+ >
+ {t("fileManager.removeFile")}
+
+
+ ) : (
+
+
+
+ {t("fileManager.clickToSelectFile")}
+
+
+ {t("fileManager.chooseFile")}
+
+
+ )}
+
+
+
+
+
+
+ {isLoading
+ ? t("fileManager.uploading")
+ : t("fileManager.uploadFile")}
+
+ setShowUpload(false)}
+ disabled={isLoading}
+ className="w-full text-sm h-9"
+ >
+ {t("common.cancel")}
+
+
+
+
+ )}
+
+ {showCreateFile && (
+
+
+
+
+
+
+ {t("fileManager.createNewFile")}
+
+
+
+
setShowCreateFile(false)}
+ className="h-8 w-8 p-0 flex-shrink-0 ml-2"
+ >
+
+
+
+
+
+
+
+ {t("fileManager.fileName")}
+
+ setNewFileName(e.target.value)}
+ placeholder={t("placeholders.fileName")}
+ className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
+ onKeyDown={(e) => e.key === "Enter" && handleCreateFile()}
+ />
+
+
+
+
+ {isLoading
+ ? t("fileManager.creating")
+ : t("fileManager.createFile")}
+
+ setShowCreateFile(false)}
+ disabled={isLoading}
+ className="w-full text-sm h-9"
+ >
+ {t("common.cancel")}
+
+
+
+
+ )}
+
+ {showCreateFolder && (
+
+
+
+
+
+
+ {t("fileManager.createNewFolder")}
+
+
+
+
setShowCreateFolder(false)}
+ className="h-8 w-8 p-0 flex-shrink-0 ml-2"
+ >
+
+
+
+
+
+
+
+ {t("fileManager.folderName")}
+
+ setNewFolderName(e.target.value)}
+ placeholder={t("placeholders.folderName")}
+ className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
+ onKeyDown={(e) => e.key === "Enter" && handleCreateFolder()}
+ />
+
+
+
+
+ {isLoading
+ ? t("fileManager.creating")
+ : t("fileManager.createFolder")}
+
+ setShowCreateFolder(false)}
+ disabled={isLoading}
+ className="w-full text-sm h-9"
+ >
+ {t("common.cancel")}
+
+
+
+
+ )}
+
+ {showDelete && (
+
+
+
+
+
+
+ {t("fileManager.deleteItem")}
+
+
+
+
setShowDelete(false)}
+ className="h-8 w-8 p-0 flex-shrink-0 ml-2"
+ >
+
+
+
+
+
+
+
+
+
+ {t("fileManager.warningCannotUndo")}
+
+
+
+
+
+
+ {t("fileManager.itemPath")}
+
+ setDeletePath(e.target.value)}
+ placeholder={t("placeholders.fullPath")}
+ className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
+ />
+
+
+
+ setDeleteIsDirectory(e.target.checked)}
+ className="rounded border-dark-border-hover bg-dark-bg-button mt-0.5 flex-shrink-0"
+ />
+
+ {t("fileManager.thisIsDirectory")}
+
+
+
+
+
+ {isLoading
+ ? t("fileManager.deleting")
+ : t("fileManager.deleteItem")}
+
+ setShowDelete(false)}
+ disabled={isLoading}
+ className="w-full text-sm h-9"
+ >
+ {t("common.cancel")}
+
+
+
+
+ )}
+
+ {showRename && (
+
+
+
+
+
+
+ {t("fileManager.renameItem")}
+
+
+
+
setShowRename(false)}
+ className="h-8 w-8 p-0 flex-shrink-0 ml-2"
+ >
+
+
+
+
+
+
+
+ {t("fileManager.currentPathLabel")}
+
+ setRenamePath(e.target.value)}
+ placeholder={t("placeholders.currentPath")}
+ className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
+ />
+
+
+
+
+ {t("fileManager.newName")}
+
+ setNewName(e.target.value)}
+ placeholder={t("placeholders.newName")}
+ className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
+ onKeyDown={(e) => e.key === "Enter" && handleRename()}
+ />
+
+
+
+ setRenameIsDirectory(e.target.checked)}
+ className="rounded border-dark-border-hover bg-dark-bg-button mt-0.5 flex-shrink-0"
+ />
+
+ {t("fileManager.thisIsDirectoryRename")}
+
+
+
+
+
+ {isLoading
+ ? t("fileManager.renaming")
+ : t("fileManager.renameItem")}
+
+ setShowRename(false)}
+ disabled={isLoading}
+ className="w-full text-sm h-9"
+ >
+ {t("common.cancel")}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerTabList.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerTabList.tsx
new file mode 100644
index 00000000..439988ba
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/FileManagerTabList.tsx
@@ -0,0 +1,62 @@
+import React from "react";
+import { Button } from "@/components/ui/button.tsx";
+import { X, Home } from "lucide-react";
+
+interface FileManagerTab {
+ id: string | number;
+ title: string;
+}
+
+interface FileManagerTabList {
+ tabs: FileManagerTab[];
+ activeTab: string | number;
+ setActiveTab: (tab: string | number) => void;
+ closeTab: (tab: string | number) => void;
+ onHomeClick: () => void;
+}
+
+export function FileManagerTabList({
+ tabs,
+ activeTab,
+ setActiveTab,
+ closeTab,
+ onHomeClick,
+}: FileManagerTabList) {
+ return (
+
+
+
+
+ {tabs.map((tab) => {
+ const isActive = tab.id === activeTab;
+ return (
+
+ setActiveTab(tab.id)}
+ variant="outline"
+ className={`h-8 rounded-r-none !px-2 border-1 border-dark-border ${isActive ? "!bg-dark-bg-active !text-white !border-dark-border-active !hover:bg-dark-bg-active !active:bg-dark-bg-active !focus:bg-dark-bg-active !hover:text-white !active:text-white !focus:text-white" : ""}`}
+ >
+ {tab.title}
+
+
+ closeTab(tab.id)}
+ variant="outline"
+ className="h-8 rounded-l-none p-0 !w-9 border-1 border-dark-border"
+ >
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/Host Manager/HostManager.tsx b/src/ui/Desktop/Apps/Host Manager/HostManager.tsx
new file mode 100644
index 00000000..7d948d1d
--- /dev/null
+++ b/src/ui/Desktop/Apps/Host Manager/HostManager.tsx
@@ -0,0 +1,142 @@
+import React, { useState } from "react";
+import { HostManagerViewer } from "@/ui/Desktop/Apps/Host Manager/HostManagerViewer.tsx";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@/components/ui/tabs.tsx";
+import { Separator } from "@/components/ui/separator.tsx";
+import { HostManagerEditor } from "@/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx";
+import { CredentialsManager } from "@/ui/Desktop/Apps/Credentials/CredentialsManager.tsx";
+import { CredentialEditor } from "@/ui/Desktop/Apps/Credentials/CredentialEditor.tsx";
+import { useSidebar } from "@/components/ui/sidebar.tsx";
+import { useTranslation } from "react-i18next";
+import type { SSHHost, HostManagerProps } from "../../../types/index";
+
+export function HostManager({
+ onSelectView,
+ isTopbarOpen,
+}: HostManagerProps): React.ReactElement {
+ const { t } = useTranslation();
+ const [activeTab, setActiveTab] = useState("host_viewer");
+ const [editingHost, setEditingHost] = useState(null);
+
+ const [editingCredential, setEditingCredential] = useState(null);
+ const { state: sidebarState } = useSidebar();
+
+ const handleEditHost = (host: SSHHost) => {
+ setEditingHost(host);
+ setActiveTab("add_host");
+ };
+
+ const handleFormSubmit = (updatedHost?: SSHHost) => {
+ setEditingHost(null);
+ setActiveTab("host_viewer");
+ };
+
+ const handleEditCredential = (credential: any) => {
+ setEditingCredential(credential);
+ setActiveTab("add_credential");
+ };
+
+ const handleCredentialFormSubmit = () => {
+ setEditingCredential(null);
+ setActiveTab("credentials");
+ };
+
+ const handleTabChange = (value: string) => {
+ setActiveTab(value);
+ if (value !== "add_host") {
+ setEditingHost(null);
+ }
+ if (value !== "add_credential") {
+ setEditingCredential(null);
+ }
+ };
+
+ const topMarginPx = isTopbarOpen ? 74 : 26;
+ const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
+ const bottomMarginPx = 8;
+
+ return (
+
+
+
+
+
+
+ {t("hosts.hostViewer")}
+
+
+ {editingHost ? t("hosts.editHost") : t("hosts.addHost")}
+
+
+
+ {t("credentials.credentialsViewer")}
+
+
+ {editingCredential
+ ? t("credentials.editCredential")
+ : t("credentials.addCredential")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx
new file mode 100644
index 00000000..a799c716
--- /dev/null
+++ b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx
@@ -0,0 +1,1507 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Controller, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button.tsx";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+} from "@/components/ui/form.tsx";
+import { Input } from "@/components/ui/input.tsx";
+import { PasswordInput } from "@/components/ui/password-input.tsx";
+import { ScrollArea } from "@/components/ui/scroll-area.tsx";
+import { Separator } from "@/components/ui/separator.tsx";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@/components/ui/tabs.tsx";
+import React, { useEffect, useRef, useState } from "react";
+import { Switch } from "@/components/ui/switch.tsx";
+import { Alert, AlertDescription } from "@/components/ui/alert.tsx";
+import { toast } from "sonner";
+import {
+ createSSHHost,
+ getCredentials,
+ getSSHHosts,
+ updateSSHHost,
+} from "@/ui/main-axios.ts";
+import { useTranslation } from "react-i18next";
+import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
+
+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: any[];
+ createdAt: string;
+ updatedAt: string;
+ credentialId?: number;
+}
+
+interface SSHManagerHostEditorProps {
+ editingHost?: SSHHost | null;
+ onFormSubmit?: (updatedHost?: SSHHost) => void;
+}
+
+export function HostManagerEditor({
+ editingHost,
+ onFormSubmit,
+}: SSHManagerHostEditorProps) {
+ const { t } = useTranslation();
+ const [hosts, setHosts] = useState([]);
+ const [folders, setFolders] = useState([]);
+ const [sshConfigurations, setSshConfigurations] = useState([]);
+ const [credentials, setCredentials] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
+ "password",
+ );
+ const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">(
+ "upload",
+ );
+ const isSubmittingRef = useRef(false);
+
+ const ipInputRef = useRef(null);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const [hostsData, credentialsData] = await Promise.all([
+ getSSHHosts(),
+ getCredentials(),
+ ]);
+ setHosts(hostsData);
+ setCredentials(credentialsData);
+
+ const uniqueFolders = [
+ ...new Set(
+ hostsData
+ .filter((host) => host.folder && host.folder.trim() !== "")
+ .map((host) => host.folder),
+ ),
+ ].sort();
+
+ const uniqueConfigurations = [
+ ...new Set(
+ hostsData
+ .filter((host) => host.name && host.name.trim() !== "")
+ .map((host) => host.name),
+ ),
+ ].sort();
+
+ setFolders(uniqueFolders);
+ setSshConfigurations(uniqueConfigurations);
+ } catch (error) {
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ useEffect(() => {
+ const handleCredentialChange = async () => {
+ try {
+ setLoading(true);
+ const hostsData = await getSSHHosts();
+ setHosts(hostsData);
+
+ const uniqueFolders = [
+ ...new Set(
+ hostsData
+ .filter((host) => host.folder && host.folder.trim() !== "")
+ .map((host) => host.folder),
+ ),
+ ].sort();
+
+ const uniqueConfigurations = [
+ ...new Set(
+ hostsData
+ .filter((host) => host.name && host.name.trim() !== "")
+ .map((host) => host.name),
+ ),
+ ].sort();
+
+ setFolders(uniqueFolders);
+ setSshConfigurations(uniqueConfigurations);
+ } catch (error) {
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ window.addEventListener("credentials:changed", handleCredentialChange);
+
+ return () => {
+ window.removeEventListener("credentials:changed", handleCredentialChange);
+ };
+ }, []);
+
+ const formSchema = z
+ .object({
+ name: z.string().optional(),
+ ip: z.string().min(1),
+ port: z.coerce.number().min(1).max(65535),
+ username: z.string().min(1),
+ folder: z.string().optional(),
+ tags: z.array(z.string().min(1)).default([]),
+ pin: z.boolean().default(false),
+ authType: z.enum(["password", "key", "credential"]),
+ credentialId: z.number().optional().nullable(),
+ password: z.string().optional(),
+ key: z.any().optional().nullable(),
+ keyPassword: z.string().optional(),
+ keyType: z
+ .enum([
+ "auto",
+ "ssh-rsa",
+ "ssh-ed25519",
+ "ecdsa-sha2-nistp256",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp521",
+ "ssh-dss",
+ "ssh-rsa-sha2-256",
+ "ssh-rsa-sha2-512",
+ ])
+ .optional(),
+ enableTerminal: z.boolean().default(true),
+ enableTunnel: z.boolean().default(true),
+ tunnelConnections: z
+ .array(
+ z.object({
+ sourcePort: z.coerce.number().min(1).max(65535),
+ endpointPort: z.coerce.number().min(1).max(65535),
+ endpointHost: z.string().min(1),
+ maxRetries: z.coerce.number().min(0).max(100).default(3),
+ retryInterval: z.coerce.number().min(1).max(3600).default(10),
+ autoStart: z.boolean().default(false),
+ }),
+ )
+ .default([]),
+ enableFileManager: z.boolean().default(true),
+ defaultPath: z.string().optional(),
+ })
+ .superRefine((data, ctx) => {
+ if (data.authType === "password") {
+ if (!data.password || data.password.trim() === "") {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("hosts.passwordRequired"),
+ path: ["password"],
+ });
+ }
+ } else if (data.authType === "key") {
+ if (
+ !data.key ||
+ (typeof data.key === "string" && data.key.trim() === "")
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("hosts.sshKeyRequired"),
+ path: ["key"],
+ });
+ }
+ if (!data.keyType) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("hosts.keyTypeRequired"),
+ path: ["keyType"],
+ });
+ }
+ } else if (data.authType === "credential") {
+ if (
+ !data.credentialId ||
+ (typeof data.credentialId === "string" &&
+ data.credentialId.trim() === "")
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("hosts.credentialRequired"),
+ path: ["credentialId"],
+ });
+ }
+ }
+
+ data.tunnelConnections.forEach((connection, index) => {
+ if (
+ connection.endpointHost &&
+ !sshConfigurations.includes(connection.endpointHost)
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("hosts.mustSelectValidSshConfig"),
+ path: ["tunnelConnections", index, "endpointHost"],
+ });
+ }
+ });
+ });
+
+ type FormData = z.infer;
+
+ const form = useForm({
+ resolver: zodResolver(formSchema) as any,
+ defaultValues: {
+ name: "",
+ ip: "",
+ port: 22,
+ username: "",
+ folder: "",
+ tags: [],
+ pin: false,
+ authType: "password" as const,
+ credentialId: null,
+ password: "",
+ key: null,
+ keyPassword: "",
+ keyType: "auto" as const,
+ enableTerminal: true,
+ enableTunnel: true,
+ enableFileManager: true,
+ defaultPath: "/",
+ tunnelConnections: [],
+ },
+ });
+
+ useEffect(() => {
+ if (authTab === "credential") {
+ const currentCredentialId = form.getValues("credentialId");
+ if (currentCredentialId) {
+ const selectedCredential = credentials.find(
+ (c) => c.id === currentCredentialId,
+ );
+ if (selectedCredential) {
+ form.setValue("username", selectedCredential.username);
+ }
+ }
+ }
+ }, [authTab, credentials, form]);
+
+ useEffect(() => {
+ if (editingHost) {
+ const cleanedHost = { ...editingHost };
+ if (cleanedHost.credentialId && cleanedHost.key) {
+ cleanedHost.key = undefined;
+ cleanedHost.keyPassword = undefined;
+ cleanedHost.keyType = undefined;
+ } else if (cleanedHost.credentialId && cleanedHost.password) {
+ cleanedHost.password = undefined;
+ } else if (cleanedHost.key && cleanedHost.password) {
+ cleanedHost.password = undefined;
+ }
+
+ const defaultAuthType = cleanedHost.credentialId
+ ? "credential"
+ : cleanedHost.key
+ ? "key"
+ : "password";
+ setAuthTab(defaultAuthType);
+
+ const formData = {
+ name: cleanedHost.name || "",
+ ip: cleanedHost.ip || "",
+ port: cleanedHost.port || 22,
+ username: cleanedHost.username || "",
+ folder: cleanedHost.folder || "",
+ tags: cleanedHost.tags || [],
+ pin: Boolean(cleanedHost.pin),
+ authType: defaultAuthType as "password" | "key" | "credential",
+ credentialId: null,
+ password: "",
+ key: null,
+ keyPassword: "",
+ keyType: "auto" as const,
+ enableTerminal: Boolean(cleanedHost.enableTerminal),
+ enableTunnel: Boolean(cleanedHost.enableTunnel),
+ enableFileManager: Boolean(cleanedHost.enableFileManager),
+ defaultPath: cleanedHost.defaultPath || "/",
+ tunnelConnections: cleanedHost.tunnelConnections || [],
+ };
+
+ if (defaultAuthType === "password") {
+ formData.password = cleanedHost.password || "";
+ } else if (defaultAuthType === "key") {
+ formData.key = "existing_key";
+ formData.keyPassword = cleanedHost.keyPassword || "";
+ formData.keyType = (cleanedHost.keyType as any) || "auto";
+ } else if (defaultAuthType === "credential") {
+ formData.credentialId =
+ cleanedHost.credentialId || "existing_credential";
+ }
+
+ form.reset(formData);
+ } else {
+ setAuthTab("password");
+ const defaultFormData = {
+ name: "",
+ ip: "",
+ port: 22,
+ username: "",
+ folder: "",
+ tags: [],
+ pin: false,
+ authType: "password" as const,
+ credentialId: null,
+ password: "",
+ key: null,
+ keyPassword: "",
+ keyType: "auto" as const,
+ enableTerminal: true,
+ enableTunnel: true,
+ enableFileManager: true,
+ defaultPath: "/",
+ tunnelConnections: [],
+ };
+
+ form.reset(defaultFormData);
+ }
+ }, [editingHost?.id]);
+
+ useEffect(() => {
+ const focusTimer = setTimeout(() => {
+ if (ipInputRef.current) {
+ ipInputRef.current.focus();
+ }
+ }, 300);
+
+ return () => clearTimeout(focusTimer);
+ }, [editingHost]);
+
+ const onSubmit = async (data: FormData) => {
+ try {
+ isSubmittingRef.current = true;
+
+ if (!data.name || data.name.trim() === "") {
+ data.name = `${data.username}@${data.ip}`;
+ }
+
+ const submitData: any = {
+ name: data.name,
+ ip: data.ip,
+ port: data.port,
+ username: data.username,
+ folder: data.folder || "",
+ tags: data.tags || [],
+ pin: Boolean(data.pin),
+ authType: data.authType,
+ enableTerminal: Boolean(data.enableTerminal),
+ enableTunnel: Boolean(data.enableTunnel),
+ enableFileManager: Boolean(data.enableFileManager),
+ defaultPath: data.defaultPath || "/",
+ tunnelConnections: data.tunnelConnections || [],
+ };
+
+ submitData.credentialId = null;
+ submitData.password = null;
+ submitData.key = null;
+ submitData.keyPassword = null;
+ submitData.keyType = null;
+
+ if (data.authType === "credential") {
+ if (data.credentialId === "existing_credential") {
+ delete submitData.credentialId;
+ } else {
+ submitData.credentialId = data.credentialId;
+ }
+ } else if (data.authType === "password") {
+ submitData.password = data.password;
+ } else if (data.authType === "key") {
+ if (data.key instanceof File) {
+ const keyContent = await data.key.text();
+ submitData.key = keyContent;
+ } else if (data.key === "existing_key") {
+ delete submitData.key;
+ } else {
+ submitData.key = data.key;
+ }
+ submitData.keyPassword = data.keyPassword;
+ submitData.keyType = data.keyType;
+ }
+
+ if (editingHost) {
+ const updatedHost = await updateSSHHost(editingHost.id, submitData);
+ toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name }));
+
+ if (onFormSubmit) {
+ onFormSubmit(updatedHost);
+ }
+ } else {
+ const newHost = await createSSHHost(submitData);
+ toast.success(t("hosts.hostAddedSuccessfully", { name: data.name }));
+
+ if (onFormSubmit) {
+ onFormSubmit(newHost);
+ }
+ }
+
+ window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
+
+ form.reset();
+ } catch (error) {
+ toast.error(t("hosts.failedToSaveHost"));
+ } finally {
+ isSubmittingRef.current = false;
+ }
+ };
+
+ const [tagInput, setTagInput] = useState("");
+
+ const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
+ const folderInputRef = useRef(null);
+ const folderDropdownRef = useRef(null);
+
+ const folderValue = form.watch("folder");
+ const filteredFolders = React.useMemo(() => {
+ if (!folderValue) return folders;
+ return folders.filter((f) =>
+ f.toLowerCase().includes(folderValue.toLowerCase()),
+ );
+ }, [folderValue, folders]);
+
+ const handleFolderClick = (folder: string) => {
+ form.setValue("folder", folder);
+ setFolderDropdownOpen(false);
+ };
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ folderDropdownRef.current &&
+ !folderDropdownRef.current.contains(event.target as Node) &&
+ folderInputRef.current &&
+ !folderInputRef.current.contains(event.target as Node)
+ ) {
+ setFolderDropdownOpen(false);
+ }
+ }
+
+ if (folderDropdownOpen) {
+ document.addEventListener("mousedown", handleClickOutside);
+ } else {
+ document.removeEventListener("mousedown", handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [folderDropdownOpen]);
+
+ const keyTypeOptions = [
+ { value: "auto", label: t("hosts.autoDetect") },
+ { value: "ssh-rsa", label: t("hosts.rsa") },
+ { value: "ssh-ed25519", label: t("hosts.ed25519") },
+ { value: "ecdsa-sha2-nistp256", label: t("hosts.ecdsaNistP256") },
+ { value: "ecdsa-sha2-nistp384", label: t("hosts.ecdsaNistP384") },
+ { value: "ecdsa-sha2-nistp521", label: t("hosts.ecdsaNistP521") },
+ { value: "ssh-dss", label: t("hosts.dsa") },
+ { value: "ssh-rsa-sha2-256", label: t("hosts.rsaSha2256") },
+ { value: "ssh-rsa-sha2-512", label: t("hosts.rsaSha2512") },
+ ];
+
+ const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false);
+ const keyTypeButtonRef = useRef(null);
+ const keyTypeDropdownRef = useRef(null);
+
+ useEffect(() => {
+ function onClickOutside(event: MouseEvent) {
+ if (
+ keyTypeDropdownOpen &&
+ keyTypeDropdownRef.current &&
+ !keyTypeDropdownRef.current.contains(event.target as Node) &&
+ keyTypeButtonRef.current &&
+ !keyTypeButtonRef.current.contains(event.target as Node)
+ ) {
+ setKeyTypeDropdownOpen(false);
+ }
+ }
+
+ document.addEventListener("mousedown", onClickOutside);
+ return () => document.removeEventListener("mousedown", onClickOutside);
+ }, [keyTypeDropdownOpen]);
+
+ const [sshConfigDropdownOpen, setSshConfigDropdownOpen] = useState<{
+ [key: number]: boolean;
+ }>({});
+ const sshConfigInputRefs = useRef<{ [key: number]: HTMLInputElement | null }>(
+ {},
+ );
+ const sshConfigDropdownRefs = useRef<{
+ [key: number]: HTMLDivElement | null;
+ }>({});
+
+ const getFilteredSshConfigs = (index: number) => {
+ const value = form.watch(`tunnelConnections.${index}.endpointHost`);
+
+ const currentHostName =
+ form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`;
+
+ let filtered = sshConfigurations.filter(
+ (config) => config !== currentHostName,
+ );
+
+ if (value) {
+ filtered = filtered.filter((config) =>
+ config.toLowerCase().includes(value.toLowerCase()),
+ );
+ }
+
+ return filtered;
+ };
+
+ const handleSshConfigClick = (config: string, index: number) => {
+ form.setValue(`tunnelConnections.${index}.endpointHost`, config);
+ setSshConfigDropdownOpen((prev) => ({ ...prev, [index]: false }));
+ };
+
+ useEffect(() => {
+ function handleSshConfigClickOutside(event: MouseEvent) {
+ const openDropdowns = Object.keys(sshConfigDropdownOpen).filter(
+ (key) => sshConfigDropdownOpen[parseInt(key)],
+ );
+
+ openDropdowns.forEach((indexStr: string) => {
+ const index = parseInt(indexStr);
+ if (
+ sshConfigDropdownRefs.current[index] &&
+ !sshConfigDropdownRefs.current[index]?.contains(
+ event.target as Node,
+ ) &&
+ sshConfigInputRefs.current[index] &&
+ !sshConfigInputRefs.current[index]?.contains(event.target as Node)
+ ) {
+ setSshConfigDropdownOpen((prev) => ({ ...prev, [index]: false }));
+ }
+ });
+ }
+
+ const hasOpenDropdowns = Object.values(sshConfigDropdownOpen).some(
+ (open) => open,
+ );
+
+ if (hasOpenDropdowns) {
+ document.addEventListener("mousedown", handleSshConfigClickOutside);
+ } else {
+ document.removeEventListener("mousedown", handleSshConfigClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener("mousedown", handleSshConfigClickOutside);
+ };
+ }, [sshConfigDropdownOpen]);
+
+ return (
+
+
+
+
+
+
+ {t("hosts.general")}
+
+ {t("hosts.terminal")}
+
+ {t("hosts.tunnel")}
+
+ {t("hosts.fileManager")}
+
+
+
+
+ {t("hosts.connectionDetails")}
+
+
+ (
+
+ {t("hosts.ipAddress")}
+
+ {
+ field.ref(e);
+ ipInputRef.current = e;
+ }}
+ />
+
+
+ )}
+ />
+
+ (
+
+ {t("hosts.port")}
+
+
+
+
+ )}
+ />
+
+ (
+
+ {t("hosts.username")}
+
+
+
+
+ )}
+ />
+
+
+ {t("hosts.organization")}
+
+
+
+ {t("hosts.authentication")}
+
+ {
+ const newAuthType = value as
+ | "password"
+ | "key"
+ | "credential";
+ setAuthTab(newAuthType);
+ form.setValue("authType", newAuthType);
+
+ if (newAuthType === "password") {
+ form.setValue("key", null);
+ form.setValue("keyPassword", "");
+ form.setValue("keyType", "auto");
+ form.setValue("credentialId", null);
+ } else if (newAuthType === "key") {
+ form.setValue("password", "");
+ form.setValue("credentialId", null);
+ } else if (newAuthType === "credential") {
+ form.setValue("password", "");
+ form.setValue("key", null);
+ form.setValue("keyPassword", "");
+ form.setValue("keyType", "auto");
+ }
+ }}
+ className="flex-1 flex flex-col h-full min-h-0"
+ >
+
+
+ {t("hosts.password")}
+
+ {t("hosts.key")}
+
+ {t("hosts.credential")}
+
+
+
+ (
+
+ {t("hosts.password")}
+
+
+
+
+ )}
+ />
+
+
+ {
+ setKeyInputMethod(value as "upload" | "paste");
+ if (value === "upload") {
+ form.setValue("key", null);
+ } else {
+ form.setValue("key", "");
+ }
+ }}
+ className="w-full"
+ >
+
+
+ {t("hosts.uploadFile")}
+
+
+ {t("hosts.pasteKey")}
+
+
+
+ (
+
+ {t("hosts.sshPrivateKey")}
+
+
+ {
+ const file = e.target.files?.[0];
+ field.onChange(file || null);
+ }}
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
+ />
+
+
+ {field.value === "existing_key"
+ ? t("hosts.existingKey")
+ : field.value
+ ? editingHost
+ ? t("hosts.updateKey")
+ : field.value.name
+ : t("hosts.upload")}
+
+
+
+
+
+ )}
+ />
+
+
+ (
+
+ {t("hosts.sshPrivateKey")}
+
+
+ field.onChange(e.target.value)
+ }
+ />
+
+
+ )}
+ />
+
+
+
+
(
+
+ {t("hosts.keyPassword")}
+
+
+
+
+ )}
+ />
+ (
+
+ {t("hosts.keyType")}
+
+
+
+ setKeyTypeDropdownOpen((open) => !open)
+ }
+ >
+ {keyTypeOptions.find(
+ (opt) => opt.value === field.value,
+ )?.label || t("hosts.autoDetect")}
+
+ {keyTypeDropdownOpen && (
+
+
+ {keyTypeOptions.map((opt) => (
+