diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..e2e5500d --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,578 @@ +language: "en" +early_access: false +reviews: + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + path_filters: + - "!**/.xml" + - "!**/__generated__/**" + - "!**/generated/**" + - "!**/*.json" + - "!**/*.svg" + - "!**/*.png" + - "!**/*.jpg" + - "!**/*.gif" + - "!**/*.lock" + - "!**/node_modules/**" + - "!**/dist/**" + - "!**/public/locales/**" + - "!**/repo-images/**" + path_instructions: + - path: "**/*.{ts,tsx}" + instructions: | + Review TypeScript and React code for Termix server management platform. Key considerations: + + **Architecture & Patterns:** + - Follow the established multi-port backend architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Use proper separation between Desktop and Mobile UI components + - Maintain consistent state management patterns with React hooks and context + - Follow the established tab-based navigation system + + **Database & Backend:** + - Use Drizzle ORM with SQLite for database operations + - Implement proper JWT authentication middleware patterns + - Follow the established API error handling patterns in main-axios.ts + - Use proper logging with the structured logger system (apiLogger, authLogger, sshLogger, etc.) + - Maintain proper input validation and sanitization + + **UI/UX Guidelines:** + - Use Shadcn/UI components with Tailwind CSS for consistent styling + - Follow the established theme system with dark/light mode support + - Use proper responsive design patterns for Desktop/Mobile views + - Implement proper loading states and error handling + - Use the established confirmation patterns with useConfirmation hook + - Use CSS variables and classes from index.css instead of hardcoding colors + - Follow the established color token system (--primary, --secondary, --background, etc.) + - Use proper Tailwind CSS classes instead of inline styles + - Implement proper focus states and accessibility indicators + + **SSH & Security:** + - Implement proper SSH connection management with session handling + - Use secure credential storage and management patterns + - Follow the established authentication flow (password, key, credential-based) + - Implement proper file operation security and validation + + **Code Quality:** + - Use proper TypeScript types from the centralized types/index.ts + - Follow the established API patterns in main-axios.ts + - Implement proper error boundaries and fallback UI + - Use proper React patterns (hooks, context, refs) + - Maintain consistent naming conventions and file organization + - All API interactions should go through main-axios.ts functions, not direct axios calls + - Use proper component interaction patterns through props and callbacks + - Follow the established state management patterns with useState and useEffect + - Use proper event handling and form submission patterns + + **Bug Detection & Fixes:** + - Identify and fix memory leaks in useEffect cleanup functions + - Fix missing dependency arrays in useEffect hooks + - Resolve infinite re-render loops caused by object/array dependencies + - Fix race conditions in async operations and API calls + - Identify and fix potential null/undefined access errors + - Fix improper state updates that cause stale closures + - Resolve event handler memory leaks and proper cleanup + - Fix improper error handling that could crash the application + - Identify and fix accessibility issues and keyboard navigation problems + - Fix responsive design issues and mobile compatibility problems + - Resolve TypeScript type errors and missing type definitions + - Fix improper form validation and submission handling + - Identify and fix performance issues and unnecessary re-renders + - Fix improper API error handling and user feedback + - Resolve authentication state inconsistencies and token management issues + + **Internationalization:** + - Use the i18next translation system with proper t() function calls + - Support both English and Chinese locales + - Use proper translation keys and fallbacks + + **Performance:** + - Implement proper cleanup in useEffect hooks + - Use proper memoization where appropriate + - Follow the established polling and refresh patterns + - Implement proper connection pooling and resource management + + **Specific to Termix:** + - Maintain compatibility with Electron and web versions + - Follow the established terminal integration patterns with xterm.js + - Use proper file manager operations and SSH session management + - Implement proper tunnel management and status tracking + - Follow the established alert and notification system patterns + + Highlight any deviations from these patterns and suggest improvements for maintainability, security, and user experience. + + **General Bug Detection & Fixes:** + - Identify and fix common React bugs (missing keys, improper state updates, memory leaks) + - Fix TypeScript errors and type safety issues + - Resolve accessibility violations and keyboard navigation problems + - Fix responsive design issues and mobile compatibility problems + - Identify and fix performance bottlenecks and unnecessary re-renders + - Fix improper error handling that could crash the application + - Resolve security vulnerabilities and improper data handling + - Fix improper form validation and user input handling + - Identify and fix race conditions and async operation issues + - Fix improper cleanup and resource management + - Resolve improper authentication and authorization issues + - Fix improper API error handling and user feedback + - Identify and fix potential null/undefined access errors + - Fix improper event handling and memory leaks + - Resolve improper state management and data flow issues + + - path: "**/backend/**/*.{ts,js}" + instructions: | + Review backend code for Termix server management platform. Key considerations: + + **Backend Architecture:** + - Follow the multi-port microservice architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Use Express.js with proper middleware patterns + - Implement proper CORS and security headers + - Use proper request/response logging with structured logging + + **Database Operations:** + - Use Drizzle ORM with proper schema definitions + - Implement proper database migrations and schema updates + - Use proper transaction handling for critical operations + - Follow the established database connection patterns + + **Authentication & Security:** + - Implement proper JWT token validation and refresh + - Use bcryptjs for password hashing with proper salt rounds + - Implement proper input validation and sanitization + - Use proper CORS configuration for security + - Implement proper rate limiting and security headers + + **SSH Operations:** + - Use ssh2 library with proper connection management + - Implement proper SSH key handling and validation + - Use proper session management and cleanup + - Implement proper error handling for SSH operations + - Use proper file operation security and validation + + **API Design:** + - Follow RESTful API patterns with proper HTTP status codes + - Implement proper error response formatting + - Use proper request/response validation + - Implement proper API versioning and backward compatibility + - All API routes should be defined in main-axios.ts, not scattered across components + - Use the established multi-port API architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Follow the established error handling patterns with handleApiError function + - Use proper structured logging with service-specific loggers (apiLogger, authLogger, sshLogger, etc.) + + **Logging & Monitoring:** + - Use the structured logging system with proper context + - Implement proper error tracking and reporting + - Use proper performance monitoring and metrics + - Implement proper health checks and status endpoints + + Highlight any security vulnerabilities, performance issues, or architectural deviations. + + - path: "**/components/**/*.{ts,tsx}" + instructions: | + Review UI components for Termix server management platform. Key considerations: + + **Component Design:** + - Use Shadcn/UI components as the foundation + - Implement proper component composition and reusability + - Use proper TypeScript interfaces and prop types + - Follow the established component naming conventions + + **Styling & Theming:** + - Use Tailwind CSS with proper responsive design + - Implement proper dark/light theme support + - Use proper color tokens and design system consistency + - Implement proper accessibility features (ARIA labels, keyboard navigation) + - Use CSS variables from index.css instead of hardcoding colors (--primary, --secondary, --background, etc.) + - Follow the established color scheme and design tokens + - Use proper Tailwind CSS utility classes instead of custom CSS + - Implement proper focus states and hover effects + + **State Management:** + - Use proper React hooks and context patterns + - Implement proper state lifting and prop drilling avoidance + - Use proper memoization with useMemo and useCallback + - Implement proper cleanup in useEffect hooks + + **Form Handling:** + - Use react-hook-form with proper validation + - Implement proper form state management + - Use proper error handling and user feedback + - Implement proper accessibility for form elements + + **SSH Integration:** + - Implement proper SSH connection status indicators + - Use proper terminal integration with xterm.js + - Implement proper file manager operations + - Use proper tunnel status and management UI + + Highlight any UI/UX issues, accessibility problems, or performance concerns. + + - path: "**/types/**/*.{ts,js}" + instructions: | + Review type definitions for Termix server management platform. Key considerations: + + **Type Design:** + - Use proper TypeScript interfaces and type definitions + - Implement proper type safety and validation + - Use proper generic types and utility types + - Follow the established type naming conventions + + **API Types:** + - Define proper request/response types for all API endpoints + - Use proper error types and status codes + - Implement proper validation types and schemas + - Use proper pagination and filtering types + + **SSH Types:** + - Define proper SSH connection and configuration types + - Use proper tunnel and credential types + - Implement proper file operation types + - Use proper authentication and security types + + **Type Safety:** + - Ensure proper type coverage and completeness + - Use proper strict type checking + - Implement proper type narrowing and guards + - Use proper type assertions and casting + + Highlight any type safety issues, missing types, or type inconsistencies. + + - path: "**/hooks/**/*.{ts,tsx}" + instructions: | + Review custom hooks for Termix server management platform. Key considerations: + + **Hook Design:** + - Use proper React hooks patterns and conventions + - Implement proper hook composition and reusability + - Use proper TypeScript types for hook parameters and return values + - Follow the established hook naming conventions + + **State Management:** + - Implement proper state management with useState and useReducer + - Use proper context and provider patterns + - Implement proper state persistence and synchronization + - Use proper state cleanup and memory management + + **Side Effects:** + - Use proper useEffect patterns with proper dependencies + - Implement proper cleanup functions and resource management + - Use proper async operations and error handling + - Implement proper polling and refresh patterns + + **Performance:** + - Use proper memoization with useMemo and useCallback + - Implement proper debouncing and throttling + - Use proper lazy loading and code splitting + - Implement proper optimization patterns + + **SSH Integration:** + - Implement proper SSH connection management hooks + - Use proper terminal integration hooks + - Implement proper file manager operation hooks + - Use proper tunnel management hooks + + **Hook-Specific Bug Detection:** + - Fix missing cleanup functions in useEffect hooks that cause memory leaks + - Resolve infinite loops caused by incorrect dependency arrays + - Fix stale closure issues in event handlers and async operations + - Identify and fix improper state updates that cause unnecessary re-renders + - Fix race conditions in async hooks and API calls + - Resolve improper ref usage and null reference errors + - Fix improper context usage and provider nesting issues + - Identify and fix custom hook dependency issues + - Resolve improper memoization that causes stale data + - Fix improper error handling in custom hooks + + Highlight any hook design issues, performance problems, or reusability concerns. + + - path: "**/lib/**/*.{ts,js}" + instructions: | + Review utility libraries and helper functions for Termix server management platform. Key considerations: + + **Utility Functions:** + - Implement proper utility functions with clear purposes + - Use proper TypeScript types and JSDoc documentation + - Implement proper error handling and validation + - Follow the established utility naming conventions + + **Logging System:** + - Use proper structured logging with context and metadata + - Implement proper log levels and filtering + - Use proper log formatting and output + - Implement proper log rotation and cleanup + + **API Utilities:** + - Implement proper API client configuration and management + - Use proper request/response interceptors + - Implement proper error handling and retry logic + - Use proper authentication and authorization handling + - All API functions should be centralized in main-axios.ts + - Use proper service-specific API instances (sshHostApi, tunnelApi, fileManagerApi, statsApi, authApi) + - Follow the established error handling patterns with handleApiError function + - Use proper structured logging with service-specific loggers + + **Security Utilities:** + - Implement proper input validation and sanitization + - Use proper encryption and decryption functions + - Implement proper secure random generation + - Use proper security headers and CORS handling + + **SSH Utilities:** + - Implement proper SSH connection utilities + - Use proper SSH key handling and validation + - Implement proper SSH command execution + - Use proper SSH file operation utilities + + **Utility Bug Detection:** + - Fix improper error handling in utility functions that could crash the application + - Resolve null/undefined access errors in utility functions + - Fix improper input validation that could cause security vulnerabilities + - Identify and fix memory leaks in utility functions + - Fix improper async/await usage and promise handling + - Resolve improper type checking and validation errors + - Fix improper logging that could expose sensitive information + - Identify and fix performance bottlenecks in utility functions + - Fix improper data transformation and serialization issues + - Resolve improper configuration and environment variable handling + + Highlight any utility design issues, performance problems, or security concerns. + + - path: "**/main-axios.ts" + instructions: | + Review main-axios.ts API client configuration for Termix server management platform. Key considerations: + + **API Client Architecture:** + - Maintain the multi-port API architecture (SSH: 8081, Tunnel: 8083, File Manager: 8084, Stats: 8085) + - Use proper service-specific API instances (sshHostApi, tunnelApi, fileManagerApi, statsApi, authApi) + - Implement proper API instance creation with createApiInstance function + - Use proper base URL configuration for different environments (dev, production, Electron) + + **Error Handling:** + - Use the centralized handleApiError function for consistent error handling + - Implement proper error classification (auth, network, validation, server errors) + - Use proper error logging with service-specific loggers + - Implement proper error response formatting and user-friendly messages + + **Request/Response Interceptors:** + - Implement proper JWT token handling in request interceptors + - Use proper request timing and performance logging + - Implement proper response logging and error tracking + - Use proper authentication token refresh and cleanup + + **API Function Organization:** + - Group API functions by service (SSH Host Management, Tunnel Management, File Manager, etc.) + - Use proper TypeScript types for all API functions + - Implement proper parameter validation and sanitization + - Use proper return type definitions and error handling + + **Authentication:** + - Implement proper JWT token management and refresh + - Use proper cookie handling for web and Electron environments + - Implement proper authentication state management + - Use proper token expiration and cleanup + + **Logging:** + - Use proper structured logging with context and metadata + - Implement proper request/response logging with performance metrics + - Use proper error logging with appropriate log levels + - Implement proper service-specific logger selection + + **Performance:** + - Implement proper request timeout and retry logic + - Use proper connection pooling and resource management + - Implement proper request deduplication and caching + - Use proper performance monitoring and metrics + + **Security:** + - Implement proper input validation and sanitization + - Use proper CORS and security header handling + - Implement proper authentication and authorization + - Use proper secure communication and data handling + + **API Bug Detection:** + - Fix improper error handling that could expose sensitive information + - Resolve race conditions in concurrent API calls + - Fix improper token management and authentication state issues + - Identify and fix memory leaks in API interceptors + - Fix improper request/response validation that could cause crashes + - Resolve improper timeout handling and retry logic + - Fix improper error response formatting and user feedback + - Identify and fix performance issues in API calls + - Fix improper request deduplication and caching issues + - Resolve improper authentication token refresh and cleanup + - Fix improper CORS and security header configuration + - Identify and fix potential security vulnerabilities in API handling + + Highlight any API design issues, error handling problems, or security concerns. + + - path: "**/electron/**/*.{ts,js,cjs}" + instructions: | + Review Electron application code for Termix server management platform. Key considerations: + + **Electron Architecture:** + - Use proper Electron main and renderer process separation + - Implement proper IPC (Inter-Process Communication) patterns + - Use proper security and sandboxing configurations + - Follow the established Electron best practices + + **Security:** + - Implement proper security policies and configurations + - Use proper context isolation and node integration + - Implement proper CSP and security headers + - Use proper authentication and authorization handling + + **Performance:** + - Implement proper memory management and cleanup + - Use proper resource optimization and caching + - Implement proper background processing and threading + - Use proper performance monitoring and profiling + + **Electron Bug Detection:** + - Fix improper IPC communication that could cause crashes + - Resolve memory leaks in Electron main and renderer processes + - Fix improper window management and lifecycle issues + - Identify and fix security vulnerabilities in Electron configuration + - Fix improper context isolation and node integration issues + - Resolve improper event handling and cleanup in Electron + - Fix improper file system access and permission issues + - Identify and fix performance issues in Electron processes + - Fix improper auto-updater and version management + - Resolve improper tray and menu functionality issues + - Fix improper security policies and CSP configuration + - Identify and fix potential security vulnerabilities in Electron setup + + Highlight any Electron-specific issues, security vulnerabilities, or performance problems. + + - path: "**/docker/**/*" + instructions: | + Review Docker configuration files for Termix server management platform. Key considerations: + + **Dockerfile Design:** + - Use proper multi-stage builds for optimization + - Implement proper layer caching and optimization + - Use proper security and minimal base images + - Follow the established Docker best practices + + **Security:** + - Implement proper user and permission management + - Use proper security scanning and vulnerability assessment + - Implement proper secrets and credential management + - Use proper network security and isolation + + **Performance:** + - Implement proper resource optimization and allocation + - Use proper caching and build optimization + - Implement proper monitoring and logging + - Use proper health checks and status monitoring + + **Docker Bug Detection:** + - Fix improper multi-stage build optimization that causes large images + - Resolve security vulnerabilities in base images and dependencies + - Fix improper volume and data persistence configuration + - Identify and fix resource limit and constraint issues + - Fix improper networking and port configuration + - Resolve improper environment variable and secret management + - Fix improper health check and status monitoring configuration + - Identify and fix performance issues in container startup + - Fix improper logging and monitoring configuration + - Resolve improper backup and recovery procedures + - Fix improper scaling and load balancing configuration + - Identify and fix potential security vulnerabilities in Docker setup + + Highlight any Docker configuration issues, security vulnerabilities, or performance problems. + + - path: "**/*.md" + instructions: | + Review documentation files for Termix server management platform. Key considerations: + + **Documentation Quality:** + - Ensure proper grammar, spelling, and clarity + - Use proper formatting and structure + - Implement proper code examples and snippets + - Follow the established documentation standards + + **Content Accuracy:** + - Ensure proper technical accuracy and completeness + - Use proper up-to-date information and examples + - Implement proper cross-references and links + - Use proper version and compatibility information + + **User Experience:** + - Ensure proper user-friendly language and explanations + - Use proper step-by-step instructions and guides + - Implement proper troubleshooting and FAQ sections + - Use proper visual aids and diagrams where appropriate + + Highlight any documentation issues, inaccuracies, or missing information. + + - path: "**/index.css" + instructions: | + Review index.css styling configuration for Termix server management platform. Key considerations: + + **CSS Variable System:** + - Define proper CSS custom properties for colors, spacing, and typography + - Use consistent naming conventions for CSS variables (--primary, --secondary, --background, etc.) + - Implement proper dark/light theme variable definitions + - Use proper semantic color naming (--destructive, --muted, --accent, etc.) + + **Design System:** + - Follow the established design token system + - Use proper color palette definitions with proper contrast ratios + - Implement proper typography scale and font family definitions + - Use proper spacing and sizing scale definitions + + **Theme Support:** + - Implement proper dark and light theme variable definitions + - Use proper CSS custom property fallbacks + - Implement proper theme switching support + - Use proper color scheme media queries + + **Component Styling:** + - Define proper base styles for common components + - Use proper utility classes and helper styles + - Implement proper responsive design utilities + - Use proper accessibility-focused styling + + **Color Management:** + - Avoid hardcoded color values, use CSS variables instead + - Implement proper color contrast and accessibility + - Use proper semantic color definitions + - Implement proper color state variations (hover, focus, active) + + **Typography:** + - Define proper font family and weight definitions + - Use proper line height and letter spacing + - Implement proper text size and hierarchy + - Use proper font loading and fallback strategies + + **Layout Utilities:** + - Define proper spacing and margin utilities + - Use proper flexbox and grid utilities + - Implement proper responsive breakpoint utilities + - Use proper container and layout helpers + + **Accessibility:** + - Implement proper focus indicators and states + - Use proper color contrast ratios + - Implement proper reduced motion support + - Use proper screen reader friendly styling + + **Performance:** + - Use efficient CSS selectors and properties + - Implement proper CSS organization and structure + - Use proper CSS custom property optimization + - Implement proper critical CSS and loading strategies + + Highlight any styling issues, accessibility problems, or design system inconsistencies. + auto_review: + enabled: true + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + - "DRAFT" + - "EXPERIMENTAL" + - "TEST" + drafts: false +chat: + auto_reply: true diff --git a/.env b/.env index c1e19f61..6f985423 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VERSION=1.5.0 \ No newline at end of file +VERSION=1.6.0 +VITE_API_HOST=localhost \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 14c1d8b7..908aaba5 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: [LukeGus] \ No newline at end of file +github: [LukeGus] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..55f187ee --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help Termix improve +title: "[BUG]" +labels: bug +assignees: "" +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots/Logs** +If applicable, add screenshots or console/Docker logs to help explain your problem. + +**Environment (please complete the following information):** + +- Browser [e.g. chrome, safari] +- Version [e.g. 1.6.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..8f421adb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for Termix +title: "[FEATURE]" +labels: enhancement +assignees: "" +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 89762628..8bb2f443 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,7 +21,7 @@ updates: dependency-type: "production" update-types: - "minor" - + - package-ecosystem: "docker" directory: "/docker" schedule: @@ -37,4 +37,4 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" \ No newline at end of file + interval: "weekly" diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b2bf6f80..0cb53035 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -5,8 +5,8 @@ on: branches: - development paths-ignore: - - '**.md' - - '.gitignore' + - "**.md" + - ".gitignore" workflow_dispatch: inputs: tag_name: diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index fb9e6004..6f1bbbff 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -88,11 +88,13 @@ jobs: - name: Create Linux Portable zip run: | - zip -r Termix-Linux-Portable.zip release/linux-unpacked/* + cd release/linux-unpacked + zip -r ../../Termix-Linux-Portable.zip * + cd ../.. - name: Upload Linux Portable Artifact uses: actions/upload-artifact@v4 with: name: Termix-Linux-Portable path: Termix-Linux-Portable.zip - retention-days: 3 + retention-days: 30 diff --git a/.gitignore b/.gitignore index d0adddb5..9066858c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ dist-ssr *.sln *.sw? /db/ +/release/ +/.claude/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..1b8ac889 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +# Ignore artifacts: +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a2a2e8a..d3e7f12f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing +\_# Contributing ## Prerequisites @@ -9,13 +9,13 @@ ## Installation 1. Clone the repository: - ```sh - git clone https://github.com/LukeGus/Termix - ``` + ```sh + git clone https://github.com/LukeGus/Termix + ``` 2. Install the dependencies: - ```sh - npm install - ``` + ```sh + npm install + ``` ## Running the development server @@ -23,10 +23,10 @@ Run the following commands: ```sh npm run dev -npx tsc -p tsconfig.node.json -node ./dist/backend/starter.js +npm run dev:backend ``` +a This will start the backend and the frontend Vite server. You can access Termix by going to `http://localhost:5174/`. ## Contributing @@ -34,23 +34,74 @@ This will start the backend and the frontend Vite server. You can access Termix 1. **Fork the repository**: Click the "Fork" button at the top right of the [repository page](https://github.com/LukeGus/Termix). 2. **Create a new branch**: - ```sh - git checkout -b feature/my-new-feature - ``` + ```sh + git checkout -b feature/my-new-feature + ``` 3. **Make your changes**: Implement your feature, fix, or improvement. 4. **Commit your changes**: - ```sh - git commit -m "Add feature: my new feature" - ``` + ```sh + git commit -m "Feature request my new feature" + ``` 5. **Push to your fork**: - ```sh - git push origin feature/my-new-feature - ``` + ```sh + git push origin feature/my-feature-request + ``` 6. **Open a pull request**: Go to the original repository and create a PR with a clear description. ## 📝 Guidelines - Follow the existing code style. Use Tailwind CSS with shadcn components. +- Use the below color scheme with the respective CSS variable placed in the `className` of a div/component. - Place all API routes in the `main-axios.ts` file. Updating the `openapi.json` is unneeded. - Include meaningful commit messages. -- Link related issues when applicable. \ No newline at end of file +- Link related issues when applicable. +- `MobileApp.tsx` renders when the users screen width is less than 768px, otherwise it loads the usual `DesktopApp.tsx`. + +## Color Scheme + +### Background Colors + +| CSS Variable | Color Value | Usage | Description | +| ----------------------------- | ----------- | --------------------------- | ---------------------------------------- | +| `--color-dark-bg` | `#18181b` | Main dark background | Primary dark background color | +| `--color-dark-bg-darker` | `#0e0e10` | Darker backgrounds | Darker variant for panels and containers | +| `--color-dark-bg-darkest` | `#09090b` | Darkest backgrounds | Darkest background (terminal) | +| `--color-dark-bg-light` | `#141416` | Light dark backgrounds | Lighter variant of dark background | +| `--color-dark-bg-very-light` | `#101014` | Very light dark backgrounds | Very light variant of dark background | +| `--color-dark-bg-panel` | `#1b1b1e` | Panel backgrounds | Background for panels and cards | +| `--color-dark-bg-panel-hover` | `#232327` | Panel hover states | Background for panels on hover | + +### Element-Specific Backgrounds + +| CSS Variable | Color Value | Usage | Description | +| ------------------------ | ----------- | ------------------ | --------------------------------------------- | +| `--color-dark-bg-input` | `#222225` | Input fields | Background for input fields and form elements | +| `--color-dark-bg-button` | `#23232a` | Button backgrounds | Background for buttons and clickable elements | +| `--color-dark-bg-active` | `#1d1d1f` | Active states | Background for active/selected elements | +| `--color-dark-bg-header` | `#131316` | Header backgrounds | Background for headers and navigation bars | + +### Border Colors + +| CSS Variable | Color Value | Usage | Description | +| ---------------------------- | ----------- | --------------- | ---------------------------------------- | +| `--color-dark-border` | `#303032` | Default borders | Standard border color | +| `--color-dark-border-active` | `#2d2d30` | Active borders | Border color for active elements | +| `--color-dark-border-hover` | `#434345` | Hover borders | Border color on hover states | +| `--color-dark-border-light` | `#5a5a5d` | Light borders | Lighter border color for subtle elements | +| `--color-dark-border-medium` | `#373739` | Medium borders | Medium weight border color | +| `--color-dark-border-panel` | `#222224` | Panel borders | Border color for panels and cards | + +### Interactive States + +| CSS Variable | Color Value | Usage | Description | +| ------------------------ | ----------- | ----------------- | --------------------------------------------- | +| `--color-dark-hover` | `#2d2d30` | Hover states | Background color for hover effects | +| `--color-dark-active` | `#2a2a2c` | Active states | Background color for active elements | +| `--color-dark-pressed` | `#1a1a1c` | Pressed states | Background color for pressed/clicked elements | +| `--color-dark-hover-alt` | `#2a2a2d` | Alternative hover | Alternative hover state color | + +## Support + +If you need help with Termix, you can join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support +channel. You can also open an issue or open a pull request on the [GitHub](https://github.com/LukeGus/Termix/issues) +repo. diff --git a/README-CN.md b/README-CN.md new file mode 100644 index 00000000..5cee2e1f --- /dev/null +++ b/README-CN.md @@ -0,0 +1,109 @@ +# 仓库统计 + +

+ English 英文 | + 中文 中文 +

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

+ + Termix Banner +

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

+ + Termix Banner +

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

+ Termix Demo 1 + Termix Demo 2 +

+ +

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

+ +

+ +

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

+ English English | + 中文 中文 +

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

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

# License + Distributed under the Apache License Version 2.0. See LICENSE for more information. diff --git a/components.json b/components.json index 2082f482..8bfc737f 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 7a95cabb..92d774a6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,7 +6,11 @@ RUN apk add --no-cache python3 make g++ COPY package*.json ./ -RUN npm ci --force && \ +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + +RUN npm ci --force --ignore-scripts && \ npm cache clean --force # Stage 2: Build frontend @@ -23,6 +27,12 @@ WORKDIR /app COPY . . +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + +RUN npm rebuild better-sqlite3 --force + RUN npm run build:backend # Stage 4: Production dependencies @@ -31,6 +41,10 @@ WORKDIR /app COPY package*.json ./ +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + RUN npm ci --only=production --ignore-scripts --force && \ npm cache clean --force @@ -42,7 +56,13 @@ RUN apk add --no-cache python3 make g++ COPY package*.json ./ -RUN npm ci --only=production bcryptjs better-sqlite3 --force && \ +ENV npm_config_target_platform=linux +ENV npm_config_target_arch=x64 +ENV npm_config_target_libc=glibc + +# Install native modules and compile them properly +RUN npm ci --only=production --force && \ + npm rebuild better-sqlite3 bcryptjs --force && \ npm cache clean --force # Stage 6: Final image @@ -57,14 +77,12 @@ RUN apk add --no-cache nginx gettext su-exec && \ COPY docker/nginx.conf /etc/nginx/nginx.conf COPY --from=frontend-builder /app/dist /usr/share/nginx/html -COPY --from=frontend-builder /app/public/locales /usr/share/nginx/html/locales +COPY --from=frontend-builder /app/src/locales /usr/share/nginx/html/locales RUN chown -R nginx:nginx /usr/share/nginx/html WORKDIR /app -COPY --from=production-deps /app/node_modules /app/node_modules -COPY --from=native-builder /app/node_modules/bcryptjs /app/node_modules/bcryptjs -COPY --from=native-builder /app/node_modules/better-sqlite3 /app/node_modules/better-sqlite3 +COPY --from=native-builder /app/node_modules /app/node_modules COPY --from=backend-builder /app/dist/backend ./dist/backend COPY package.json ./ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a4c55fad..5e7ec7e9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -12,4 +12,4 @@ services: volumes: termix-data: - driver: local \ No newline at end of file + driver: local diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index d6f5033a..a45affd0 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -19,9 +19,9 @@ cd /app export NODE_ENV=production if command -v su-exec > /dev/null 2>&1; then - su-exec node node dist/backend/starter.js + su-exec node node dist/backend/backend/starter.js else - su -s /bin/sh node -c "node dist/backend/starter.js" + su -s /bin/sh node -c "node dist/backend/backend/starter.js" fi echo "All services started" diff --git a/docker/nginx.conf b/docker/nginx.conf index 728aad3b..2a943a46 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -18,7 +18,7 @@ http { index index.html index.htm; } - location /users/ { + location ~ ^/users(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -27,7 +27,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /version/ { + location ~ ^/version(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -36,7 +36,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /releases/ { + location ~ ^/releases(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -45,7 +45,16 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /alerts/ { + location ~ ^/alerts(/.*)?$ { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ ^/credentials(/.*)?$ { proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Host $host; @@ -129,7 +138,16 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /status/ { + location /health { + proxy_pass http://127.0.0.1:8081; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location ~ ^/status(/.*)?$ { proxy_pass http://127.0.0.1:8085; proxy_http_version 1.1; proxy_set_header Host $host; @@ -138,7 +156,7 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - location /metrics/ { + location ~ ^/metrics(/.*)?$ { proxy_pass http://127.0.0.1:8085; proxy_http_version 1.1; proxy_set_header Host $host; diff --git a/electron-builder.json b/electron-builder.json new file mode 100644 index 00000000..21bdb711 --- /dev/null +++ b/electron-builder.json @@ -0,0 +1,44 @@ +{ + "appId": "com.termix.app", + "productName": "Termix", + "directories": { + "output": "release" + }, + "files": [ + "dist/**/*", + "electron/**/*", + "public/**/*", + "!**/node_modules/**/*", + "!src/**/*", + "!*.md", + "!tsconfig*.json", + "!vite.config.ts", + "!eslint.config.js" + ], + "asarUnpack": ["node_modules/node-fetch/**/*"], + "extraMetadata": { + "main": "electron/main.cjs" + }, + "buildDependenciesFromSource": false, + "nodeGypRebuild": false, + "npmRebuild": false, + "win": { + "target": "nsis", + "icon": "public/icon.ico", + "executableName": "Termix" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "artifactName": "${productName}-Setup-${version}.${ext}", + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "Termix", + "uninstallDisplayName": "Termix" + }, + "linux": { + "target": "AppImage", + "icon": "public/icon.png", + "category": "Development" + } +} diff --git a/electron/main.cjs b/electron/main.cjs new file mode 100644 index 00000000..7c42cdf5 --- /dev/null +++ b/electron/main.cjs @@ -0,0 +1,334 @@ +const { app, BrowserWindow, shell, ipcMain } = require("electron"); +const path = require("path"); +const fs = require("fs"); + +let mainWindow = null; + +const isDev = process.env.NODE_ENV === "development" || !app.isPackaged; + +const gotTheLock = app.requestSingleInstanceLock(); +if (!gotTheLock) { + console.log("Another instance is already running, quitting..."); + app.quit(); + process.exit(0); +} else { + app.on("second-instance", (event, commandLine, workingDirectory) => { + console.log("Second instance detected, focusing existing window..."); + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + mainWindow.show(); + } + }); +} + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 800, + minHeight: 600, + title: "Termix", + icon: isDev + ? path.join(__dirname, "..", "public", "icon.png") + : path.join(process.resourcesPath, "public", "icon.png"), + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + webSecurity: !isDev, + preload: path.join(__dirname, "preload.js"), + }, + show: false, + }); + + if (process.platform !== "darwin") { + mainWindow.setMenuBarVisibility(false); + } + + if (isDev) { + mainWindow.loadURL("http://localhost:5173"); + mainWindow.webContents.openDevTools(); + } else { + const indexPath = path.join(__dirname, "..", "dist", "index.html"); + console.log("Loading frontend from:", indexPath); + mainWindow.loadFile(indexPath); + } + + mainWindow.once("ready-to-show", () => { + console.log("Window ready to show"); + mainWindow.show(); + }); + + mainWindow.webContents.on( + "did-fail-load", + (event, errorCode, errorDescription, validatedURL) => { + console.error( + "Failed to load:", + errorCode, + errorDescription, + validatedURL, + ); + }, + ); + + mainWindow.webContents.on("did-finish-load", () => { + console.log("Frontend loaded successfully"); + }); + + mainWindow.on("close", (event) => { + if (process.platform === "darwin") { + event.preventDefault(); + mainWindow.hide(); + } + }); + + mainWindow.on("closed", () => { + mainWindow = null; + }); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: "deny" }; + }); +} + +ipcMain.handle("get-app-version", () => { + return app.getVersion(); +}); + +ipcMain.handle("get-platform", () => { + return process.platform; +}); + +ipcMain.handle("get-server-config", () => { + try { + const userDataPath = app.getPath("userData"); + const configPath = path.join(userDataPath, "server-config.json"); + + if (fs.existsSync(configPath)) { + const configData = fs.readFileSync(configPath, "utf8"); + return JSON.parse(configData); + } + return null; + } catch (error) { + console.error("Error reading server config:", error); + return null; + } +}); + +ipcMain.handle("save-server-config", (event, config) => { + try { + const userDataPath = app.getPath("userData"); + const configPath = path.join(userDataPath, "server-config.json"); + + if (!fs.existsSync(userDataPath)) { + fs.mkdirSync(userDataPath, { recursive: true }); + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + return { success: true }; + } catch (error) { + console.error("Error saving server config:", error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle("test-server-connection", async (event, serverUrl) => { + try { + let fetch; + try { + fetch = globalThis.fetch || require("node:fetch"); + } catch (e) { + const https = require("https"); + const http = require("http"); + const { URL } = require("url"); + + fetch = (url, options = {}) => { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === "https:"; + const client = isHttps ? https : http; + + const req = client.request( + url, + { + method: options.method || "GET", + headers: options.headers || {}, + timeout: options.timeout || 5000, + }, + (res) => { + let data = ""; + res.on("data", (chunk) => (data += chunk)); + res.on("end", () => { + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + text: () => Promise.resolve(data), + json: () => Promise.resolve(JSON.parse(data)), + }); + }); + }, + ); + + req.on("error", reject); + req.on("timeout", () => { + req.destroy(); + reject(new Error("Request timeout")); + }); + + if (options.body) { + req.write(options.body); + } + req.end(); + }); + }; + } + + const normalizedServerUrl = serverUrl.replace(/\/$/, ""); + + const healthUrl = `${normalizedServerUrl}/health`; + + try { + const response = await fetch(healthUrl, { + method: "GET", + timeout: 5000, + }); + + if (response.ok) { + const data = await response.text(); + + if ( + data.includes("") || + data.includes("") + ) { + console.log( + "Health endpoint returned HTML instead of JSON - not a Termix server", + ); + return { + success: false, + error: + "Server returned HTML instead of JSON. This does not appear to be a Termix server.", + }; + } + + try { + const healthData = JSON.parse(data); + if ( + healthData && + (healthData.status === "ok" || + healthData.status === "healthy" || + healthData.healthy === true || + healthData.database === "connected") + ) { + return { + success: true, + status: response.status, + testedUrl: healthUrl, + }; + } + } catch (parseError) { + console.log("Health endpoint did not return valid JSON"); + } + } + } catch (urlError) { + console.error("Health check failed:", urlError); + } + + try { + const versionUrl = `${normalizedServerUrl}/version`; + const response = await fetch(versionUrl, { + method: "GET", + timeout: 5000, + }); + + if (response.ok) { + const data = await response.text(); + + if ( + data.includes("") || + data.includes("") + ) { + console.log( + "Version endpoint returned HTML instead of JSON - not a Termix server", + ); + return { + success: false, + error: + "Server returned HTML instead of JSON. This does not appear to be a Termix server.", + }; + } + + try { + const versionData = JSON.parse(data); + if ( + versionData && + (versionData.status === "up_to_date" || + versionData.status === "requires_update" || + (versionData.localVersion && + versionData.version && + versionData.latest_release)) + ) { + return { + success: true, + status: response.status, + testedUrl: versionUrl, + warning: + "Health endpoint not available, but server appears to be running", + }; + } + } catch (parseError) { + console.log("Version endpoint did not return valid JSON"); + } + } + } catch (versionError) { + console.error("Version check failed:", versionError); + } + + return { + success: false, + error: + "Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.", + }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +app.whenReady().then(() => { + createWindow(); + console.log("Termix started successfully"); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") { + app.quit(); + } +}); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } else if (mainWindow) { + mainWindow.show(); + } +}); + +app.on("before-quit", () => { + console.log("App is quitting..."); +}); + +app.on("will-quit", () => { + console.log("App will quit..."); +}); + +process.on("uncaughtException", (error) => { + console.error("Uncaught Exception:", error); +}); + +process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection at:", promise, "reason:", reason); +}); diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 00000000..e1e436d8 --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,29 @@ +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("electronAPI", { + getAppVersion: () => ipcRenderer.invoke("get-app-version"), + getPlatform: () => ipcRenderer.invoke("get-platform"), + + getServerConfig: () => ipcRenderer.invoke("get-server-config"), + saveServerConfig: (config) => + ipcRenderer.invoke("save-server-config", config), + testServerConnection: (serverUrl) => + ipcRenderer.invoke("test-server-connection", serverUrl), + + showSaveDialog: (options) => ipcRenderer.invoke("show-save-dialog", options), + showOpenDialog: (options) => ipcRenderer.invoke("show-open-dialog", options), + + onUpdateAvailable: (callback) => ipcRenderer.on("update-available", callback), + onUpdateDownloaded: (callback) => + ipcRenderer.on("update-downloaded", callback), + + removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel), + isElectron: true, + isDev: process.env.NODE_ENV === "development", + + invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), +}); + +window.IS_ELECTRON = true; + +console.log("electronAPI exposed to window"); diff --git a/eslint.config.js b/eslint.config.js index d94e7deb..f4616740 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,18 +1,18 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { globalIgnores } from "eslint/config"; export default tseslint.config([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ js.configs.recommended, tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], + reactHooks.configs["recommended-latest"], reactRefresh.configs.vite, ], languageOptions: { @@ -20,4 +20,4 @@ export default tseslint.config([ globals: globals.browser, }, }, -]) +]); diff --git a/openapi.json b/openapi.json index b8d6ce05..8c8c0a50 100644 --- a/openapi.json +++ b/openapi.json @@ -1,2214 +1,2257 @@ { - "openapi": "3.0.3", - "info": { - "title": "Termix API", - "version": "1.0.0", - "description": "Comprehensive API for Termix SSH management, file operations, tunneling, and server monitoring. This API provides endpoints for managing SSH hosts, file operations, tunnel connections, server monitoring, user management, and system alerts.", - "contact": { - "name": "Termix Development Team" + "openapi": "3.0.3", + "info": { + "title": "Termix API", + "version": "1.0.0", + "description": "Comprehensive API for Termix SSH management, file operations, tunneling, and server monitoring. This API provides endpoints for managing SSH hosts, file operations, tunnel connections, server monitoring, user management, and system alerts.", + "contact": { + "name": "Termix Development Team" + } + }, + "servers": [ + { + "url": "http://localhost:8081", + "description": "Main database and authentication server" + }, + { + "url": "http://localhost:8083", + "description": "SSH tunnel management server" + }, + { + "url": "http://localhost:8084", + "description": "SSH file manager server" + }, + { + "url": "http://localhost:8085", + "description": "Server statistics and monitoring server" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + { + "name": "System", + "description": "System health, version, and release information endpoints" + }, + { + "name": "SSH Hosts", + "description": "SSH host management, creation, updates, and deletion" + }, + { + "name": "File Manager", + "description": "File manager operations including recent, pinned, and shortcuts" + }, + { + "name": "SSH File Operations", + "description": "SSH file operations like reading, writing, creating, and deleting files" + }, + { + "name": "Tunnel Management", + "description": "SSH tunnel connection, disconnection, and status management" + }, + { + "name": "Server Statistics", + "description": "Server status monitoring and metrics collection" + }, + { + "name": "User Management", + "description": "User account management and administration" + }, + { + "name": "Authentication", + "description": "User authentication, login, and password management" + }, + { + "name": "TOTP", + "description": "Two-factor authentication using TOTP (Time-based One-Time Password)" + }, + { + "name": "Alerts", + "description": "System alerts and notifications management" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" } }, - "servers": [ - { - "url": "http://localhost:8081", - "description": "Main database and authentication server" + "schemas": { + "SSHHost": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "ip": { "type": "string" }, + "port": { "type": "integer" }, + "username": { "type": "string" }, + "folder": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "pin": { "type": "boolean" }, + "authType": { "type": "string", "enum": ["password", "key"] }, + "password": { "type": "string" }, + "key": { "type": "string" }, + "keyPassword": { "type": "string" }, + "keyType": { "type": "string" }, + "enableTerminal": { "type": "boolean" }, + "enableTunnel": { "type": "boolean" }, + "enableFileManager": { "type": "boolean" }, + "defaultPath": { "type": "string" }, + "tunnelConnections": { + "type": "array", + "items": { "type": "object" } + }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["id", "ip", "port", "username", "authType"] }, - { - "url": "http://localhost:8083", - "description": "SSH tunnel management server" + "SSHHostData": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "ip": { "type": "string" }, + "port": { "type": "integer" }, + "username": { "type": "string" }, + "folder": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "pin": { "type": "boolean" }, + "authType": { "type": "string", "enum": ["password", "key"] }, + "password": { "type": "string" }, + "key": { "type": "string" }, + "keyPassword": { "type": "string" }, + "keyType": { "type": "string" }, + "enableTerminal": { "type": "boolean" }, + "enableTunnel": { "type": "boolean" }, + "enableFileManager": { "type": "boolean" }, + "defaultPath": { "type": "string" }, + "tunnelConnections": { + "type": "array", + "items": { "type": "object" } + } + }, + "required": ["ip", "port", "username", "authType"] }, - { - "url": "http://localhost:8084", - "description": "SSH file manager server" + "TunnelConfig": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "hostName": { "type": "string" }, + "sourceIP": { "type": "string" }, + "sourceSSHPort": { "type": "integer" }, + "sourceUsername": { "type": "string" }, + "sourcePassword": { "type": "string" }, + "sourceAuthMethod": { "type": "string" }, + "sourceSSHKey": { "type": "string" }, + "sourceKeyPassword": { "type": "string" }, + "sourceKeyType": { "type": "string" }, + "endpointIP": { "type": "string" }, + "endpointSSHPort": { "type": "integer" }, + "endpointUsername": { "type": "string" }, + "endpointPassword": { "type": "string" }, + "endpointAuthMethod": { "type": "string" }, + "endpointSSHKey": { "type": "string" }, + "endpointKeyPassword": { "type": "string" }, + "endpointKeyType": { "type": "string" }, + "sourcePort": { "type": "integer" }, + "endpointPort": { "type": "integer" }, + "maxRetries": { "type": "integer" }, + "retryInterval": { "type": "integer" }, + "autoStart": { "type": "boolean" }, + "isPinned": { "type": "boolean" } + }, + "required": [ + "name", + "hostName", + "sourceIP", + "sourceSSHPort", + "sourceUsername", + "endpointIP", + "endpointSSHPort", + "endpointUsername", + "sourcePort", + "endpointPort" + ] }, - { - "url": "http://localhost:8085", - "description": "Server statistics and monitoring server" - } - ], - "security": [ - { - "bearerAuth": [] - } - ], - "tags": [ - { - "name": "System", - "description": "System health, version, and release information endpoints" - }, - { - "name": "SSH Hosts", - "description": "SSH host management, creation, updates, and deletion" - }, - { - "name": "File Manager", - "description": "File manager operations including recent, pinned, and shortcuts" - }, - { - "name": "SSH File Operations", - "description": "SSH file operations like reading, writing, creating, and deleting files" - }, - { - "name": "Tunnel Management", - "description": "SSH tunnel connection, disconnection, and status management" - }, - { - "name": "Server Statistics", - "description": "Server status monitoring and metrics collection" - }, - { - "name": "User Management", - "description": "User account management and administration" - }, - { - "name": "Authentication", - "description": "User authentication, login, and password management" - }, - { - "name": "TOTP", - "description": "Two-factor authentication using TOTP (Time-based One-Time Password)" - }, - { - "name": "Alerts", - "description": "System alerts and notifications management" - } - ], - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" + "TunnelStatus": { + "type": "object", + "properties": { + "status": { "type": "string" }, + "reason": { "type": "string" }, + "errorType": { "type": "string" }, + "retryCount": { "type": "integer" }, + "maxRetries": { "type": "integer" }, + "nextRetryIn": { "type": "integer" }, + "retryExhausted": { "type": "boolean" } } }, - "schemas": { - "SSHHost": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "name": { "type": "string" }, - "ip": { "type": "string" }, - "port": { "type": "integer" }, - "username": { "type": "string" }, - "folder": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } }, - "pin": { "type": "boolean" }, - "authType": { "type": "string", "enum": ["password", "key"] }, - "password": { "type": "string" }, - "key": { "type": "string" }, - "keyPassword": { "type": "string" }, - "keyType": { "type": "string" }, - "enableTerminal": { "type": "boolean" }, - "enableTunnel": { "type": "boolean" }, - "enableFileManager": { "type": "boolean" }, - "defaultPath": { "type": "string" }, - "tunnelConnections": { "type": "array", "items": { "type": "object" } }, - "createdAt": { "type": "string", "format": "date-time" }, - "updatedAt": { "type": "string", "format": "date-time" } - }, - "required": ["id", "ip", "port", "username", "authType"] - }, - "SSHHostData": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "ip": { "type": "string" }, - "port": { "type": "integer" }, - "username": { "type": "string" }, - "folder": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } }, - "pin": { "type": "boolean" }, - "authType": { "type": "string", "enum": ["password", "key"] }, - "password": { "type": "string" }, - "key": { "type": "string" }, - "keyPassword": { "type": "string" }, - "keyType": { "type": "string" }, - "enableTerminal": { "type": "boolean" }, - "enableTunnel": { "type": "boolean" }, - "enableFileManager": { "type": "boolean" }, - "defaultPath": { "type": "string" }, - "tunnelConnections": { "type": "array", "items": { "type": "object" } } - }, - "required": ["ip", "port", "username", "authType"] - }, - "TunnelConfig": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "hostName": { "type": "string" }, - "sourceIP": { "type": "string" }, - "sourceSSHPort": { "type": "integer" }, - "sourceUsername": { "type": "string" }, - "sourcePassword": { "type": "string" }, - "sourceAuthMethod": { "type": "string" }, - "sourceSSHKey": { "type": "string" }, - "sourceKeyPassword": { "type": "string" }, - "sourceKeyType": { "type": "string" }, - "endpointIP": { "type": "string" }, - "endpointSSHPort": { "type": "integer" }, - "endpointUsername": { "type": "string" }, - "endpointPassword": { "type": "string" }, - "endpointAuthMethod": { "type": "string" }, - "endpointSSHKey": { "type": "string" }, - "endpointKeyPassword": { "type": "string" }, - "endpointKeyType": { "type": "string" }, - "sourcePort": { "type": "integer" }, - "endpointPort": { "type": "integer" }, - "maxRetries": { "type": "integer" }, - "retryInterval": { "type": "integer" }, - "autoStart": { "type": "boolean" }, - "isPinned": { "type": "boolean" } - }, - "required": ["name", "hostName", "sourceIP", "sourceSSHPort", "sourceUsername", "endpointIP", "endpointSSHPort", "endpointUsername", "sourcePort", "endpointPort"] - }, - "TunnelStatus": { - "type": "object", - "properties": { - "status": { "type": "string" }, - "reason": { "type": "string" }, - "errorType": { "type": "string" }, - "retryCount": { "type": "integer" }, - "maxRetries": { "type": "integer" }, - "nextRetryIn": { "type": "integer" }, - "retryExhausted": { "type": "boolean" } - } - }, - "ServerStatus": { - "type": "object", - "properties": { - "status": { "type": "string", "enum": ["online", "offline"] }, - "lastChecked": { "type": "string", "format": "date-time" } - } - }, - "ServerMetrics": { - "type": "object", - "properties": { - "cpu": { - "type": "object", - "properties": { - "percent": { "type": "number" }, - "cores": { "type": "number" }, - "load": { "type": "array", "items": { "type": "number" }, "minItems": 3, "maxItems": 3 } - } - }, - "memory": { - "type": "object", - "properties": { - "percent": { "type": "number" }, - "usedGiB": { "type": "number" }, - "totalGiB": { "type": "number" } - } - }, - "disk": { - "type": "object", - "properties": { - "percent": { "type": "number" }, - "usedHuman": { "type": "string" }, - "totalHuman": { "type": "string" } - } - }, - "lastChecked": { "type": "string", "format": "date-time" } - } - }, - "FileManagerFile": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["file", "directory"] }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" } - }, - "required": ["name", "path"] - }, - "UserInfo": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "username": { "type": "string" }, - "is_admin": { "type": "boolean" } - }, - "required": ["id", "username", "is_admin"] - }, - "AuthResponse": { - "type": "object", - "properties": { - "token": { "type": "string" } - }, - "required": ["token"] - }, - "Error": { - "type": "object", - "properties": { - "error": { "type": "string" }, - "details": { "type": "string" } - } + "ServerStatus": { + "type": "object", + "properties": { + "status": { "type": "string", "enum": ["online", "offline"] }, + "lastChecked": { "type": "string", "format": "date-time" } } }, - "parameters": { - "hostId": { - "name": "hostId", - "in": "query", - "description": "The ID of the SSH host", - "required": true, - "schema": { - "type": "integer" - } - }, - "sessionId": { - "name": "sessionId", - "in": "query", - "description": "The SSH session identifier", - "required": true, - "schema": { - "type": "string" - } - }, - "path": { - "name": "path", - "in": "query", - "description": "The file or directory path", - "required": true, - "schema": { - "type": "string" - } - }, - "tunnelName": { - "name": "tunnelName", - "in": "path", - "description": "The name of the tunnel", - "required": true, - "schema": { - "type": "string" - } - }, - "userId": { - "name": "userId", - "in": "path", - "description": "The user identifier", - "required": true, - "schema": { - "type": "string" - } - }, - "hostIdPath": { - "name": "id", - "in": "path", - "description": "The SSH host identifier", - "required": true, - "schema": { - "type": "integer" - } - }, - "serverIdPath": { - "name": "id", - "in": "path", - "description": "The server identifier", - "required": true, - "schema": { - "type": "integer" - } + "ServerMetrics": { + "type": "object", + "properties": { + "cpu": { + "type": "object", + "properties": { + "percent": { "type": "number" }, + "cores": { "type": "number" }, + "load": { + "type": "array", + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + } + } + }, + "memory": { + "type": "object", + "properties": { + "percent": { "type": "number" }, + "usedGiB": { "type": "number" }, + "totalGiB": { "type": "number" } + } + }, + "disk": { + "type": "object", + "properties": { + "percent": { "type": "number" }, + "usedHuman": { "type": "string" }, + "totalHuman": { "type": "string" } + } + }, + "lastChecked": { "type": "string", "format": "date-time" } } }, - "responses": { - "BadRequest": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "FileManagerFile": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "type": { "type": "string", "enum": ["file", "directory"] }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" } }, - "Unauthorized": { - "description": "Unauthorized", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "required": ["name", "path"] + }, + "UserInfo": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "username": { "type": "string" }, + "is_admin": { "type": "boolean" } }, - "NotFound": { - "description": "Not Found", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "required": ["id", "username", "is_admin"] + }, + "AuthResponse": { + "type": "object", + "properties": { + "token": { "type": "string" } }, - "InternalServerError": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/Error" } - } - } + "required": ["token"] + }, + "Error": { + "type": "object", + "properties": { + "error": { "type": "string" }, + "details": { "type": "string" } } } }, - "paths": { - "/health": { - "get": { - "summary": "Health check endpoint", - "description": "Simple health check to verify the API server is running and responsive. **Server: localhost:8081**", - "operationId": "getHealth", - "tags": ["System"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { "type": "string", "example": "ok" } - } + "parameters": { + "hostId": { + "name": "hostId", + "in": "query", + "description": "The ID of the SSH host", + "required": true, + "schema": { + "type": "integer" + } + }, + "sessionId": { + "name": "sessionId", + "in": "query", + "description": "The SSH session identifier", + "required": true, + "schema": { + "type": "string" + } + }, + "path": { + "name": "path", + "in": "query", + "description": "The file or directory path", + "required": true, + "schema": { + "type": "string" + } + }, + "tunnelName": { + "name": "tunnelName", + "in": "path", + "description": "The name of the tunnel", + "required": true, + "schema": { + "type": "string" + } + }, + "userId": { + "name": "userId", + "in": "path", + "description": "The user identifier", + "required": true, + "schema": { + "type": "string" + } + }, + "hostIdPath": { + "name": "id", + "in": "path", + "description": "The SSH host identifier", + "required": true, + "schema": { + "type": "integer" + } + }, + "serverIdPath": { + "name": "id", + "in": "path", + "description": "The server identifier", + "required": true, + "schema": { + "type": "integer" + } + } + }, + "responses": { + "BadRequest": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "Unauthorized": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "NotFound": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + }, + "InternalServerError": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Error" } + } + } + } + } + }, + "paths": { + "/health": { + "get": { + "summary": "Health check endpoint", + "description": "Simple health check to verify the API server is running and responsive. **Server: localhost:8081**", + "operationId": "getHealth", + "tags": ["System"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", "example": "ok" } } } } } } } - }, - "/version": { - "get": { - "summary": "Get version information and check for updates", - "description": "Get version information and check for updates. **Server: localhost:8081**", - "operationId": "getVersion", - "tags": ["System"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { "type": "string", "enum": ["up_to_date", "requires_update"] }, - "version": { "type": "string" }, - "latest_release": { - "type": "object", - "properties": { - "tag_name": { "type": "string" }, - "name": { "type": "string" }, - "published_at": { "type": "string" }, - "html_url": { "type": "string" } - } - }, - "cached": { "type": "boolean" }, - "cache_age": { "type": "number" } - } + } + }, + "/version": { + "get": { + "summary": "Get version information and check for updates", + "description": "Get version information and check for updates. **Server: localhost:8081**", + "operationId": "getVersion", + "tags": ["System"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["up_to_date", "requires_update"] + }, + "version": { "type": "string" }, + "latest_release": { + "type": "object", + "properties": { + "tag_name": { "type": "string" }, + "name": { "type": "string" }, + "published_at": { "type": "string" }, + "html_url": { "type": "string" } + } + }, + "cached": { "type": "boolean" }, + "cache_age": { "type": "number" } } } } - }, - "401": { - "description": "Version information not available", - "content": { - "text/plain": { - "schema": { "type": "string" } - } + } + }, + "401": { + "description": "Version information not available", + "content": { + "text/plain": { + "schema": { "type": "string" } } } } } - }, - "/releases/rss": { - "get": { - "summary": "Get releases in RSS format", - "description": "Get releases in RSS format. **Server: localhost:8081**", - "operationId": "getReleasesRSS", - "tags": ["System"], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { "type": "integer", "default": 1 } - }, - { - "name": "per_page", - "in": "query", - "schema": { "type": "integer", "default": 20, "maximum": 100 } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "feed": { + } + }, + "/releases/rss": { + "get": { + "summary": "Get releases in RSS format", + "description": "Get releases in RSS format. **Server: localhost:8081**", + "operationId": "getReleasesRSS", + "tags": ["System"], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { "type": "integer", "default": 1 } + }, + { + "name": "per_page", + "in": "query", + "schema": { "type": "integer", "default": 20, "maximum": 100 } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feed": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" }, + "link": { "type": "string" }, + "updated": { "type": "string" } + } + }, + "items": { + "type": "array", + "items": { "type": "object", "properties": { + "id": { "type": "integer" }, "title": { "type": "string" }, "description": { "type": "string" }, "link": { "type": "string" }, - "updated": { "type": "string" } - } - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "integer" }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "link": { "type": "string" }, - "pubDate": { "type": "string" }, - "version": { "type": "string" }, - "isPrerelease": { "type": "boolean" }, - "isDraft": { "type": "boolean" }, - "assets": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "size": { "type": "number" }, - "download_count": { "type": "number" }, - "download_url": { "type": "string" } - } + "pubDate": { "type": "string" }, + "version": { "type": "string" }, + "isPrerelease": { "type": "boolean" }, + "isDraft": { "type": "boolean" }, + "assets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "size": { "type": "number" }, + "download_count": { "type": "number" }, + "download_url": { "type": "string" } } } } } - }, - "total_count": { "type": "integer" }, - "cached": { "type": "boolean" }, - "cache_age": { "type": "number" } - } + } + }, + "total_count": { "type": "integer" }, + "cached": { "type": "boolean" }, + "cache_age": { "type": "number" } } } } } } } - }, - "/ssh/db/host": { - "get": { - "summary": "Get all SSH hosts", - "description": "Retrieve a list of all configured SSH hosts in the system. This endpoint requires authentication and returns host information including connection details, authentication methods, and enabled features. **Server: localhost:8081**", - "operationId": "getSSHHosts", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "Successfully retrieved SSH hosts", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/SSHHost" } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "post": { - "summary": "Create a new SSH host", - "description": "Create a new SSH host configuration. **Server: localhost:8081**", - "operationId": "createSSHHost", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + } + }, + "/ssh/db/host": { + "get": { + "summary": "Get all SSH hosts", + "description": "Retrieve a list of all configured SSH hosts in the system. This endpoint requires authentication and returns host information including connection details, authentication methods, and enabled features. **Server: localhost:8081**", + "operationId": "getSSHHosts", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "Successfully retrieved SSH hosts", "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "key": { - "type": "string", - "format": "binary", - "description": "SSH private key file (optional)" - }, - "data": { - "type": "string", - "description": "JSON string containing host data" - } - } - } - }, "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHostData" } + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/SSHHost" } + } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHost" } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/db/host/{id}": { - "get": { - "summary": "Get SSH host by ID", - "description": "Get SSH host by ID. **Server: localhost:8081**", - "operationId": "getSSHHostById", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostIdPath" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHost" } + "post": { + "summary": "Create a new SSH host", + "description": "Create a new SSH host configuration. **Server: localhost:8081**", + "operationId": "createSSHHost", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "binary", + "description": "SSH private key file (optional)" + }, + "data": { + "type": "string", + "description": "JSON string containing host data" + } } } }, - "404": { "$ref": "#/components/responses/NotFound" }, - "401": { "$ref": "#/components/responses/Unauthorized" } + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHostData" } + } } }, - "put": { - "summary": "Update SSH host", - "description": "Update SSH host configuration. **Server: localhost:8081**", - "operationId": "updateSSHHost", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostIdPath" - } - ], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "key": { - "type": "string", - "format": "binary", - "description": "SSH private key file (optional)" - }, - "data": { - "type": "string", - "description": "JSON string containing host data" - } - } - } - }, "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHostData" } + "schema": { "$ref": "#/components/schemas/SSHHost" } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/SSHHost" } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/db/host/{id}": { + "get": { + "summary": "Get SSH host by ID", + "description": "Get SSH host by ID. **Server: localhost:8081**", + "operationId": "getSSHHostById", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostIdPath" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHost" } + } + } + }, + "404": { "$ref": "#/components/responses/NotFound" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + }, + "put": { + "summary": "Update SSH host", + "description": "Update SSH host configuration. **Server: localhost:8081**", + "operationId": "updateSSHHost", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostIdPath" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "binary", + "description": "SSH private key file (optional)" + }, + "data": { + "type": "string", + "description": "JSON string containing host data" + } } } }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { "$ref": "#/components/responses/NotFound" } + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHostData" } + } } }, - "delete": { - "summary": "Delete SSH host", - "description": "Delete SSH host configuration. **Server: localhost:8081**", - "operationId": "deleteSSHHost", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostIdPath" + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SSHHost" } + } } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" }, - "404": { "$ref": "#/components/responses/NotFound" } - } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { "$ref": "#/components/responses/NotFound" } } }, - "/ssh/db/folders": { - "get": { - "summary": "Get all SSH host folders", - "description": "Get all SSH host folders. **Server: localhost:8081**", - "operationId": "getSSHFolders", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "type": "string" } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + "delete": { + "summary": "Delete SSH host", + "description": "Delete SSH host configuration. **Server: localhost:8081**", + "operationId": "deleteSSHHost", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostIdPath" } - } - }, - "/ssh/bulk-import": { - "post": { - "summary": "Bulk import SSH hosts", - "description": "Bulk import SSH hosts. **Server: localhost:8081**", - "operationId": "bulkImportSSHHosts", - "tags": ["SSH Hosts"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "hosts": { + "message": { "type": "string" } + } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, + "404": { "$ref": "#/components/responses/NotFound" } + } + } + }, + "/ssh/db/folders": { + "get": { + "summary": "Get all SSH host folders", + "description": "Get all SSH host folders. **Server: localhost:8081**", + "operationId": "getSSHFolders", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/bulk-import": { + "post": { + "summary": "Bulk import SSH hosts", + "description": "Bulk import SSH hosts. **Server: localhost:8081**", + "operationId": "bulkImportSSHHosts", + "tags": ["SSH Hosts"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "hosts": { + "type": "array", + "items": { "$ref": "#/components/schemas/SSHHostData" } + } + }, + "required": ["hosts"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "success": { "type": "integer" }, + "failed": { "type": "integer" }, + "errors": { "type": "array", - "items": { "$ref": "#/components/schemas/SSHHostData" } + "items": { "type": "string" } } - }, - "required": ["hosts"] + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" }, - "success": { "type": "integer" }, - "failed": { "type": "integer" }, - "errors": { - "type": "array", - "items": { "type": "string" } - } - } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/file_manager/recent": { + "get": { + "summary": "Get recent files for a host", + "description": "Get recent files for a host. **Server: localhost:8081**", + "operationId": "getFileManagerRecent", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostId" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/FileManagerFile" } } } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/recent": { - "get": { - "summary": "Get recent files for a host", - "description": "Get recent files for a host. **Server: localhost:8081**", - "operationId": "getFileManagerRecent", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/FileManagerFile" } - } - } + "post": { + "summary": "Add file to recent list", + "description": "Add file to recent list. **Server: localhost:8081**", + "operationId": "addFileManagerRecent", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" }, + "hostId": { "type": "integer" } + }, + "required": ["name", "path", "isSSH", "hostId"] } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } }, - "post": { - "summary": "Add file to recent list", - "description": "Add file to recent list. **Server: localhost:8081**", - "operationId": "addFileManagerRecent", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" }, - "hostId": { "type": "integer" } - }, - "required": ["name", "path", "isSSH", "hostId"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "delete": { - "summary": "Remove file from recent list", - "description": "Remove file from recent list. **Server: localhost:8081**", - "operationId": "removeFileManagerRecent", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "name": "name", - "in": "query", - "description": "File name", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isSSH", - "in": "query", - "description": "Whether this is an SSH file", - "required": true, - "schema": { "type": "boolean" } - }, - { - "name": "sshSessionId", - "in": "query", - "description": "SSH session ID", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "hostId", - "in": "query", - "description": "Host ID", - "required": true, - "schema": { "type": "integer" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/pinned": { - "get": { - "summary": "Get pinned files for a host", - "description": "Get pinned files for a host. **Server: localhost:8081**", - "operationId": "getFileManagerPinned", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { "$ref": "#/components/schemas/FileManagerFile" } - } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "post": { - "summary": "Add file to pinned list", - "description": "Add file to pinned list. **Server: localhost:8081**", - "operationId": "addFileManagerPinned", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "requestBody": { + "delete": { + "summary": "Remove file from recent list", + "description": "Remove file from recent list. **Server: localhost:8081**", + "operationId": "removeFileManagerRecent", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "File name", "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isSSH", + "in": "query", + "description": "Whether this is an SSH file", + "required": true, + "schema": { "type": "boolean" } + }, + { + "name": "sshSessionId", + "in": "query", + "description": "SSH session ID", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "hostId", + "in": "query", + "description": "Host ID", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" }, - "hostId": { "type": "integer" } - }, - "required": ["name", "path", "isSSH", "hostId"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/file_manager/pinned": { + "get": { + "summary": "Get pinned files for a host", + "description": "Get pinned files for a host. **Server: localhost:8081**", + "operationId": "getFileManagerPinned", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostId" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/FileManagerFile" } } } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "delete": { - "summary": "Remove file from pinned list", - "description": "Remove file from pinned list. **Server: localhost:8081**", - "operationId": "removeFileManagerPinned", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "name": "name", - "in": "query", - "description": "File name", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isSSH", - "in": "query", - "description": "Whether this is an SSH file", - "required": true, - "schema": { "type": "boolean" } - }, - { - "name": "sshSessionId", - "in": "query", - "description": "SSH session ID", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "hostId", - "in": "query", - "description": "Host ID", - "required": true, - "schema": { "type": "integer" } } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/shortcuts": { - "get": { - "summary": "Get file shortcuts for a host", - "description": "Get file shortcuts for a host. **Server: localhost:8081**", - "operationId": "getFileManagerShortcuts", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "$ref": "#/components/parameters/hostId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "path": { "type": "string" } - }, - "required": ["name", "path"] - } - } - } + "post": { + "summary": "Add file to pinned list", + "description": "Add file to pinned list. **Server: localhost:8081**", + "operationId": "addFileManagerPinned", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" }, + "hostId": { "type": "integer" } + }, + "required": ["name", "path", "isSSH", "hostId"] } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } }, - "post": { - "summary": "Add file shortcut", - "description": "Add file shortcut. **Server: localhost:8081**", - "operationId": "addFileManagerShortcut", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "isSSH": { "type": "boolean" }, - "sshSessionId": { "type": "string" }, - "hostId": { "type": "integer" } - }, - "required": ["name", "path", "isSSH", "hostId"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - }, - "delete": { - "summary": "Remove file shortcut", - "description": "Remove file shortcut. **Server: localhost:8081**", - "operationId": "removeFileManagerShortcut", - "tags": ["File Manager"], - "security": [{ "bearerAuth": [] }], - "parameters": [ - { - "name": "name", - "in": "query", - "description": "File name", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isSSH", - "in": "query", - "description": "Whether this is an SSH file", - "required": true, - "schema": { "type": "boolean" } - }, - { - "name": "sshSessionId", - "in": "query", - "description": "SSH session ID", - "required": false, - "schema": { "type": "string" } - }, - { - "name": "hostId", - "in": "query", - "description": "Host ID", - "required": true, - "schema": { "type": "integer" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/ssh/connect": { - "post": { - "summary": "Connect to SSH server", - "description": "Connect to SSH server. **Server: localhost:8084**", - "operationId": "connectSSH", - "tags": ["SSH File Operations"], - "requestBody": { + "delete": { + "summary": "Remove file from pinned list", + "description": "Remove file from pinned list. **Server: localhost:8081**", + "operationId": "removeFileManagerPinned", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "File name", "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isSSH", + "in": "query", + "description": "Whether this is an SSH file", + "required": true, + "schema": { "type": "boolean" } + }, + { + "name": "sshSessionId", + "in": "query", + "description": "SSH session ID", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "hostId", + "in": "query", + "description": "Host ID", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "sessionId": { "type": "string" }, - "ip": { "type": "string" }, - "port": { "type": "integer" }, - "username": { "type": "string" }, - "password": { "type": "string" }, - "sshKey": { "type": "string" }, - "keyPassword": { "type": "string" } - }, - "required": ["sessionId", "ip", "username", "port"] + "message": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" } - } + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } } - }, - "/ssh/file_manager/ssh/disconnect": { - "post": { - "summary": "Disconnect from SSH server", - "description": "Disconnect from SSH server. **Server: localhost:8084**", - "operationId": "disconnectSSH", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, + } + }, + "/ssh/file_manager/shortcuts": { + "get": { + "summary": "Get file shortcuts for a host", + "description": "Get file shortcuts for a host. **Server: localhost:8081**", + "operationId": "getFileManagerShortcuts", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "$ref": "#/components/parameters/hostId" + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" } - }, - "required": ["sessionId"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { + "type": "array", + "items": { "type": "object", "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/status": { - "get": { - "summary": "Get SSH connection status", - "description": "Get SSH connection status. **Server: localhost:8084**", - "operationId": "getSSHStatus", - "tags": ["SSH File Operations"], - "parameters": [ - { - "$ref": "#/components/parameters/sessionId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "connected": { "type": "boolean" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/listFiles": { - "get": { - "summary": "List files in SSH directory", - "description": "List files in SSH directory. **Server: localhost:8084**", - "operationId": "listSSHFiles", - "tags": ["SSH File Operations"], - "parameters": [ - { - "$ref": "#/components/parameters/sessionId" - }, - { - "$ref": "#/components/parameters/path" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "path": { "type": "string" }, - "type": { "type": "string", "enum": ["file", "directory"] }, - "size": { "type": "number" }, - "modified": { "type": "string" }, - "permissions": { "type": "string" } - } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/readFile": { - "get": { - "summary": "Read SSH file content", - "description": "Read SSH file content. **Server: localhost:8084**", - "operationId": "readSSHFile", - "tags": ["SSH File Operations"], - "parameters": [ - { - "$ref": "#/components/parameters/sessionId" - }, - { - "$ref": "#/components/parameters/path" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "content": { "type": "string" }, + "name": { "type": "string" }, "path": { "type": "string" } - } + }, + "required": ["name", "path"] } } } } - } - } - }, - "/ssh/file_manager/ssh/writeFile": { - "post": { - "summary": "Write content to SSH file", - "description": "Write content to SSH file. **Server: localhost:8084**", - "operationId": "writeSSHFile", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "path": { "type": "string" }, - "content": { "type": "string" } - }, - "required": ["sessionId", "path", "content"] - } - } - } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } + "401": { "$ref": "#/components/responses/Unauthorized" } } }, - "/ssh/file_manager/ssh/createFile": { - "post": { - "summary": "Create new SSH file", - "description": "Create new SSH file. **Server: localhost:8084**", - "operationId": "createSSHFile", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "path": { "type": "string" }, - "fileName": { "type": "string" }, - "content": { "type": "string" } - }, - "required": ["sessionId", "path", "fileName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/createFolder": { - "post": { - "summary": "Create new SSH folder", - "description": "Create new SSH folder. **Server: localhost:8084**", - "operationId": "createSSHFolder", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "path": { "type": "string" }, - "folderName": { "type": "string" } - }, - "required": ["sessionId", "path", "folderName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/deleteItem": { - "delete": { - "summary": "Delete SSH file or folder", - "description": "Delete SSH file or folder. **Server: localhost:8084**", - "operationId": "deleteSSHItem", - "tags": ["SSH File Operations"], - "parameters": [ - { - "name": "sessionId", - "in": "query", - "description": "SSH session ID", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "path", - "in": "query", - "description": "File or directory path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "isDirectory", - "in": "query", - "description": "Whether the item is a directory", - "required": true, - "schema": { "type": "boolean" } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/file_manager/ssh/renameItem": { - "put": { - "summary": "Rename SSH file or folder", - "description": "Rename SSH file or folder. **Server: localhost:8084**", - "operationId": "renameSSHItem", - "tags": ["SSH File Operations"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionId": { "type": "string" }, - "oldPath": { "type": "string" }, - "newName": { "type": "string" } - }, - "required": ["sessionId", "oldPath", "newName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/tunnel/status": { - "get": { - "summary": "Get all tunnel statuses", - "description": "Get all tunnel statuses. **Server: localhost:8083**", - "operationId": "getTunnelStatuses", - "tags": ["Tunnel Management"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/TunnelStatus" } - } - } - } - } - } - } - }, - "/ssh/tunnel/status/{tunnelName}": { - "get": { - "summary": "Get tunnel status by name", - "description": "Get tunnel status by name. **Server: localhost:8083**", - "operationId": "getTunnelStatusByName", - "tags": ["Tunnel Management"], - "parameters": [ - { - "$ref": "#/components/parameters/tunnelName" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TunnelStatus" } - } - } - } - } - } - }, - "/ssh/tunnel/connect": { - "post": { - "summary": "Connect to tunnel", - "description": "Connect to tunnel. **Server: localhost:8083**", - "operationId": "connectTunnel", - "tags": ["Tunnel Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/TunnelConfig" } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/tunnel/disconnect": { - "post": { - "summary": "Disconnect tunnel", - "description": "Disconnect tunnel. **Server: localhost:8083**", - "operationId": "disconnectTunnel", - "tags": ["Tunnel Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tunnelName": { "type": "string" } - }, - "required": ["tunnelName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/ssh/tunnel/cancel": { - "post": { - "summary": "Cancel tunnel connection", - "description": "Cancel tunnel connection. **Server: localhost:8083**", - "operationId": "cancelTunnel", - "tags": ["Tunnel Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tunnelName": { "type": "string" } - }, - "required": ["tunnelName"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/status": { - "get": { - "summary": "Get all server statuses", - "description": "Get all server statuses. **Server: localhost:8085**", - "operationId": "getAllServerStatuses", - "tags": ["Server Statistics"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { "$ref": "#/components/schemas/ServerStatus" } - } - } - } - } - } - } - }, - "/status/{id}": { - "get": { - "summary": "Get server status by ID", - "description": "Get server status by ID. **Server: localhost:8085**", - "operationId": "getServerStatusById", - "tags": ["Server Statistics"], - "parameters": [ - { - "$ref": "#/components/parameters/serverIdPath" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ServerStatus" } - } - } - } - } - } - }, - "/metrics/{id}": { - "get": { - "summary": "Get server metrics by ID", - "description": "Get server metrics by ID. **Server: localhost:8085**", - "operationId": "getServerMetricsById", - "tags": ["Server Statistics"], - "parameters": [ - { - "$ref": "#/components/parameters/serverIdPath" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ServerMetrics" } - } - } - } - } - } - }, - "/refresh": { - "post": { - "summary": "Refresh server statistics", - "description": "Refresh server statistics. **Server: localhost:8085**", - "operationId": "refreshServerStats", - "tags": ["Server Statistics"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/users/create": { - "post": { - "summary": "Create new user account", - "description": "Create new user account. **Server: localhost:8081**", - "operationId": "createUser", - "tags": ["User Management"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "username": { "type": "string" }, - "password": { "type": "string" } - }, - "required": ["username", "password"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" } - } - } - }, - "/users/login": { - "post": { - "summary": "User login", - "description": "User login. **Server: localhost:8081**", - "operationId": "loginUser", - "tags": ["Authentication"], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "username": { "type": "string" }, - "password": { "type": "string" } - }, - "required": ["username", "password"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AuthResponse" } - } - } - }, - "400": { "$ref": "#/components/responses/BadRequest" }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - } - }, - "/users/me": { - "get": { - "summary": "Get current user info", - "description": "Get current user info. **Server: localhost:8081**", - "operationId": "getUserInfo", - "tags": ["User Management"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/UserInfo" } - } - } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } - } - } - }, - "/users/count": { - "get": { - "summary": "Get total user count", - "description": "Get total user count. **Server: localhost:8081**", - "operationId": "getUserCount", - "tags": ["User Management"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { "type": "integer" } - } - } - } - } - } - } - } - }, - "/users/registration-allowed": { - "get": { - "summary": "Check if user registration is allowed", - "description": "Check if user registration is allowed. **Server: localhost:8081**", - "operationId": "getRegistrationAllowed", - "tags": ["User Management"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "allowed": { "type": "boolean" } - } - } - } + "post": { + "summary": "Add file shortcut", + "description": "Add file shortcut. **Server: localhost:8081**", + "operationId": "addFileManagerShortcut", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "isSSH": { "type": "boolean" }, + "sshSessionId": { "type": "string" }, + "hostId": { "type": "integer" } + }, + "required": ["name", "path", "isSSH", "hostId"] } } } }, - "patch": { - "summary": "Update registration allowed status", - "description": "Update registration allowed status. **Server: localhost:8081**", - "operationId": "updateRegistrationAllowed", - "tags": ["User Management"], - "security": [{ "bearerAuth": [] }], - "requestBody": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + }, + "delete": { + "summary": "Remove file shortcut", + "description": "Remove file shortcut. **Server: localhost:8081**", + "operationId": "removeFileManagerShortcut", + "tags": ["File Manager"], + "security": [{ "bearerAuth": [] }], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "File name", "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isSSH", + "in": "query", + "description": "Whether this is an SSH file", + "required": true, + "schema": { "type": "boolean" } + }, + { + "name": "sshSessionId", + "in": "query", + "description": "SSH session ID", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "hostId", + "in": "query", + "description": "Host ID", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/ssh/file_manager/ssh/connect": { + "post": { + "summary": "Connect to SSH server", + "description": "Connect to SSH server. **Server: localhost:8084**", + "operationId": "connectSSH", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "ip": { "type": "string" }, + "port": { "type": "integer" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "sshKey": { "type": "string" }, + "keyPassword": { "type": "string" } + }, + "required": ["sessionId", "ip", "username", "port"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" } + } + } + }, + "/ssh/file_manager/ssh/disconnect": { + "post": { + "summary": "Disconnect from SSH server", + "description": "Disconnect from SSH server. **Server: localhost:8084**", + "operationId": "disconnectSSH", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" } + }, + "required": ["sessionId"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/status": { + "get": { + "summary": "Get SSH connection status", + "description": "Get SSH connection status. **Server: localhost:8084**", + "operationId": "getSSHStatus", + "tags": ["SSH File Operations"], + "parameters": [ + { + "$ref": "#/components/parameters/sessionId" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connected": { "type": "boolean" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/listFiles": { + "get": { + "summary": "List files in SSH directory", + "description": "List files in SSH directory. **Server: localhost:8084**", + "operationId": "listSSHFiles", + "tags": ["SSH File Operations"], + "parameters": [ + { + "$ref": "#/components/parameters/sessionId" + }, + { + "$ref": "#/components/parameters/path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "type": { + "type": "string", + "enum": ["file", "directory"] + }, + "size": { "type": "number" }, + "modified": { "type": "string" }, + "permissions": { "type": "string" } + } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/readFile": { + "get": { + "summary": "Read SSH file content", + "description": "Read SSH file content. **Server: localhost:8084**", + "operationId": "readSSHFile", + "tags": ["SSH File Operations"], + "parameters": [ + { + "$ref": "#/components/parameters/sessionId" + }, + { + "$ref": "#/components/parameters/path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { "type": "string" }, + "path": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/writeFile": { + "post": { + "summary": "Write content to SSH file", + "description": "Write content to SSH file. **Server: localhost:8084**", + "operationId": "writeSSHFile", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "path": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["sessionId", "path", "content"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/createFile": { + "post": { + "summary": "Create new SSH file", + "description": "Create new SSH file. **Server: localhost:8084**", + "operationId": "createSSHFile", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "path": { "type": "string" }, + "fileName": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["sessionId", "path", "fileName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/createFolder": { + "post": { + "summary": "Create new SSH folder", + "description": "Create new SSH folder. **Server: localhost:8084**", + "operationId": "createSSHFolder", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "path": { "type": "string" }, + "folderName": { "type": "string" } + }, + "required": ["sessionId", "path", "folderName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/deleteItem": { + "delete": { + "summary": "Delete SSH file or folder", + "description": "Delete SSH file or folder. **Server: localhost:8084**", + "operationId": "deleteSSHItem", + "tags": ["SSH File Operations"], + "parameters": [ + { + "name": "sessionId", + "in": "query", + "description": "SSH session ID", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "path", + "in": "query", + "description": "File or directory path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "isDirectory", + "in": "query", + "description": "Whether the item is a directory", + "required": true, + "schema": { "type": "boolean" } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/file_manager/ssh/renameItem": { + "put": { + "summary": "Rename SSH file or folder", + "description": "Rename SSH file or folder. **Server: localhost:8084**", + "operationId": "renameSSHItem", + "tags": ["SSH File Operations"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { "type": "string" }, + "oldPath": { "type": "string" }, + "newName": { "type": "string" } + }, + "required": ["sessionId", "oldPath", "newName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/tunnel/status": { + "get": { + "summary": "Get all tunnel statuses", + "description": "Get all tunnel statuses. **Server: localhost:8083**", + "operationId": "getTunnelStatuses", + "tags": ["Tunnel Management"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TunnelStatus" + } + } + } + } + } + } + } + }, + "/ssh/tunnel/status/{tunnelName}": { + "get": { + "summary": "Get tunnel status by name", + "description": "Get tunnel status by name. **Server: localhost:8083**", + "operationId": "getTunnelStatusByName", + "tags": ["Tunnel Management"], + "parameters": [ + { + "$ref": "#/components/parameters/tunnelName" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TunnelStatus" } + } + } + } + } + } + }, + "/ssh/tunnel/connect": { + "post": { + "summary": "Connect to tunnel", + "description": "Connect to tunnel. **Server: localhost:8083**", + "operationId": "connectTunnel", + "tags": ["Tunnel Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/TunnelConfig" } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/tunnel/disconnect": { + "post": { + "summary": "Disconnect tunnel", + "description": "Disconnect tunnel. **Server: localhost:8083**", + "operationId": "disconnectTunnel", + "tags": ["Tunnel Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tunnelName": { "type": "string" } + }, + "required": ["tunnelName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/ssh/tunnel/cancel": { + "post": { + "summary": "Cancel tunnel connection", + "description": "Cancel tunnel connection. **Server: localhost:8083**", + "operationId": "cancelTunnel", + "tags": ["Tunnel Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tunnelName": { "type": "string" } + }, + "required": ["tunnelName"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/status": { + "get": { + "summary": "Get all server statuses", + "description": "Get all server statuses. **Server: localhost:8085**", + "operationId": "getAllServerStatuses", + "tags": ["Server Statistics"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ServerStatus" + } + } + } + } + } + } + } + }, + "/status/{id}": { + "get": { + "summary": "Get server status by ID", + "description": "Get server status by ID. **Server: localhost:8085**", + "operationId": "getServerStatusById", + "tags": ["Server Statistics"], + "parameters": [ + { + "$ref": "#/components/parameters/serverIdPath" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ServerStatus" } + } + } + } + } + } + }, + "/metrics/{id}": { + "get": { + "summary": "Get server metrics by ID", + "description": "Get server metrics by ID. **Server: localhost:8085**", + "operationId": "getServerMetricsById", + "tags": ["Server Statistics"], + "parameters": [ + { + "$ref": "#/components/parameters/serverIdPath" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ServerMetrics" } + } + } + } + } + } + }, + "/refresh": { + "post": { + "summary": "Refresh server statistics", + "description": "Refresh server statistics. **Server: localhost:8085**", + "operationId": "refreshServerStats", + "tags": ["Server Statistics"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/users/create": { + "post": { + "summary": "Create new user account", + "description": "Create new user account. **Server: localhost:8081**", + "operationId": "createUser", + "tags": ["User Management"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "password": { "type": "string" } + }, + "required": ["username", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" } + } + } + }, + "/users/login": { + "post": { + "summary": "User login", + "description": "User login. **Server: localhost:8081**", + "operationId": "loginUser", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "password": { "type": "string" } + }, + "required": ["username", "password"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AuthResponse" } + } + } + }, + "400": { "$ref": "#/components/responses/BadRequest" }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/me": { + "get": { + "summary": "Get current user info", + "description": "Get current user info. **Server: localhost:8081**", + "operationId": "getUserInfo", + "tags": ["User Management"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UserInfo" } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/count": { + "get": { + "summary": "Get total user count", + "description": "Get total user count. **Server: localhost:8081**", + "operationId": "getUserCount", + "tags": ["User Management"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "count": { "type": "integer" } + } + } + } + } + } + } + } + }, + "/users/registration-allowed": { + "get": { + "summary": "Check if user registration is allowed", + "description": "Check if user registration is allowed. **Server: localhost:8081**", + "operationId": "getRegistrationAllowed", + "tags": ["User Management"], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "allowed": { "type": "boolean" } - }, - "required": ["allowed"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } } } } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } } }, - "/users/initiate-reset": { - "post": { - "summary": "Initiate password reset", - "description": "Initiate password reset. **Server: localhost:8081**", - "operationId": "initiatePasswordReset", - "tags": ["Authentication"], - "requestBody": { - "required": true, + "patch": { + "summary": "Update registration allowed status", + "description": "Update registration allowed status. **Server: localhost:8081**", + "operationId": "updateRegistrationAllowed", + "tags": ["User Management"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "allowed": { "type": "boolean" } + }, + "required": ["allowed"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "username": { "type": "string" } - }, - "required": ["username"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } + "message": { "type": "string" } } } } } - } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } } - }, - "/users/verify-reset-code": { - "post": { - "summary": "Verify password reset code", - "description": "Verify password reset code. **Server: localhost:8081**", - "operationId": "verifyPasswordResetCode", - "tags": ["Authentication"], - "requestBody": { - "required": true, + } + }, + "/users/initiate-reset": { + "post": { + "summary": "Initiate password reset", + "description": "Initiate password reset. **Server: localhost:8081**", + "operationId": "initiatePasswordReset", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" } + }, + "required": ["username"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "username": { "type": "string" }, - "resetCode": { "type": "string" } - }, - "required": ["username", "resetCode"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tempToken": { "type": "string" } - } + "message": { "type": "string" } } } } } } } - }, - "/users/complete-reset": { - "post": { - "summary": "Complete password reset", - "description": "Complete password reset. **Server: localhost:8081**", - "operationId": "completePasswordReset", - "tags": ["Authentication"], - "requestBody": { - "required": true, + } + }, + "/users/verify-reset-code": { + "post": { + "summary": "Verify password reset code", + "description": "Verify password reset code. **Server: localhost:8081**", + "operationId": "verifyPasswordResetCode", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "resetCode": { "type": "string" } + }, + "required": ["username", "resetCode"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "username": { "type": "string" }, - "tempToken": { "type": "string" }, - "newPassword": { "type": "string" } - }, - "required": ["username", "tempToken", "newPassword"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } + "tempToken": { "type": "string" } } } } } } } - }, - "/users/totp/setup": { - "post": { - "summary": "Setup TOTP authentication", - "description": "Setup TOTP authentication. **Server: localhost:8081**", - "operationId": "setupTOTP", - "tags": ["TOTP"], - "security": [{ "bearerAuth": [] }], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "secret": { "type": "string" }, - "qr_code": { "type": "string" } - } - } - } + } + }, + "/users/complete-reset": { + "post": { + "summary": "Complete password reset", + "description": "Complete password reset. **Server: localhost:8081**", + "operationId": "completePasswordReset", + "tags": ["Authentication"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "tempToken": { "type": "string" }, + "newPassword": { "type": "string" } + }, + "required": ["username", "tempToken", "newPassword"] } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } - } - }, - "/users/totp/enable": { - "post": { - "summary": "Enable TOTP authentication", - "description": "Enable TOTP authentication. **Server: localhost:8081**", - "operationId": "enableTOTP", - "tags": ["TOTP"], - "security": [{ "bearerAuth": [] }], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "totp_code": { "type": "string" } - }, - "required": ["totp_code"] + "message": { "type": "string" } + } + } + } + } + } + } + } + }, + "/users/totp/setup": { + "post": { + "summary": "Setup TOTP authentication", + "description": "Setup TOTP authentication. **Server: localhost:8081**", + "operationId": "setupTOTP", + "tags": ["TOTP"], + "security": [{ "bearerAuth": [] }], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "secret": { "type": "string" }, + "qr_code": { "type": "string" } + } } } } }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/totp/enable": { + "post": { + "summary": "Enable TOTP authentication", + "description": "Enable TOTP authentication. **Server: localhost:8081**", + "operationId": "enableTOTP", + "tags": ["TOTP"], + "security": [{ "bearerAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "totp_code": { "type": "string" } + }, + "required": ["totp_code"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "backup_codes": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" } + } + } + }, + "/users/totp/verify-login": { + "post": { + "summary": "Verify TOTP during login", + "description": "Verify TOTP during login. **Server: localhost:8081**", + "operationId": "verifyTOTPLogin", + "tags": ["TOTP"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "temp_token": { "type": "string" }, + "totp_code": { "type": "string" } + }, + "required": ["temp_token", "totp_code"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AuthResponse" } + } + } + } + } + } + }, + "/alerts": { + "get": { + "summary": "Get all system alerts", + "description": "Get all system alerts. **Server: localhost:8081**", + "operationId": "getAllAlerts", + "tags": ["Alerts"], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "type": "object", "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, "message": { "type": "string" }, - "backup_codes": { - "type": "array", - "items": { "type": "string" } - } + "expiresAt": { "type": "string" }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "critical"] + }, + "type": { + "type": "string", + "enum": ["info", "warning", "error", "success"] + }, + "actionUrl": { "type": "string" }, + "actionText": { "type": "string" } } } } } - }, - "401": { "$ref": "#/components/responses/Unauthorized" } + } } } - }, - "/users/totp/verify-login": { - "post": { - "summary": "Verify TOTP during login", - "description": "Verify TOTP during login. **Server: localhost:8081**", - "operationId": "verifyTOTPLogin", - "tags": ["TOTP"], - "requestBody": { - "required": true, + } + }, + "/alerts/user/{userId}": { + "get": { + "summary": "Get alerts for specific user", + "description": "Get alerts for specific user. **Server: localhost:8081**", + "operationId": "getUserAlerts", + "tags": ["Alerts"], + "parameters": [ + { + "$ref": "#/components/parameters/userId" + } + ], + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "temp_token": { "type": "string" }, - "totp_code": { "type": "string" } - }, - "required": ["temp_token", "totp_code"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/AuthResponse" } - } - } - } - } - } - }, - "/alerts": { - "get": { - "summary": "Get all system alerts", - "description": "Get all system alerts. **Server: localhost:8081**", - "operationId": "getAllAlerts", - "tags": ["Alerts"], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "title": { "type": "string" }, - "message": { "type": "string" }, - "expiresAt": { "type": "string" }, - "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, - "type": { "type": "string", "enum": ["info", "warning", "error", "success"] }, - "actionUrl": { "type": "string" }, - "actionText": { "type": "string" } - } - } - } - } - } - } - } - } - }, - "/alerts/user/{userId}": { - "get": { - "summary": "Get alerts for specific user", - "description": "Get alerts for specific user. **Server: localhost:8081**", - "operationId": "getUserAlerts", - "tags": ["Alerts"], - "parameters": [ - { - "$ref": "#/components/parameters/userId" - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "alerts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "title": { "type": "string" }, - "message": { "type": "string" }, - "expiresAt": { "type": "string" }, - "priority": { "type": "string", "enum": ["low", "medium", "high", "critical"] }, - "type": { "type": "string", "enum": ["info", "warning", "error", "success"] }, - "actionUrl": { "type": "string" }, - "actionText": { "type": "string" } - } + "alerts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "message": { "type": "string" }, + "expiresAt": { "type": "string" }, + "priority": { + "type": "string", + "enum": ["low", "medium", "high", "critical"] + }, + "type": { + "type": "string", + "enum": ["info", "warning", "error", "success"] + }, + "actionUrl": { "type": "string" }, + "actionText": { "type": "string" } } } } @@ -2218,38 +2261,38 @@ } } } - }, - "/alerts/dismiss": { - "post": { - "summary": "Dismiss an alert", - "description": "Dismiss an alert. **Server: localhost:8081**", - "operationId": "dismissAlert", - "tags": ["Alerts"], - "requestBody": { - "required": true, + } + }, + "/alerts/dismiss": { + "post": { + "summary": "Dismiss an alert", + "description": "Dismiss an alert. **Server: localhost:8081**", + "operationId": "dismissAlert", + "tags": ["Alerts"], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "alertId": { "type": "string" } + }, + "required": ["userId", "alertId"] + } + } + } + }, + "responses": { + "200": { + "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { - "userId": { "type": "string" }, - "alertId": { "type": "string" } - }, - "required": ["userId", "alertId"] - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } + "message": { "type": "string" } } } } @@ -2259,4 +2302,4 @@ } } } - \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index a3195012..d34f84e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "termix", - "version": "0.0.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "termix", - "version": "0.0.0", + "version": "1.6.0", "dependencies": { "@hookform/resolvers": "^5.1.1", "@radix-ui/react-accordion": "^1.2.11", @@ -69,6 +69,7 @@ "react-hook-form": "^7.60.0", "react-i18next": "^15.7.3", "react-resizable-panels": "^3.0.3", + "react-simple-keyboard": "^3.8.120", "react-xtermjs": "^1.0.10", "sonner": "^2.0.7", "speakeasy": "^2.0.0", @@ -92,43 +93,37 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", + "concurrently": "^9.2.1", + "electron": "^38.0.0", + "electron-builder": "^26.0.12", + "electron-icon-builder": "^2.0.1", + "electron-packager": "^17.1.2", "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "prettier": "3.6.2", "ts-node": "^10.9.2", "tw-animate-css": "^1.3.5", "typescript": "~5.9.2", "typescript-eslint": "^8.40.0", - "vite": "^7.1.3" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "vite": "^7.1.5", + "wait-on": "^8.0.4" } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@codemirror/autocomplete": { - "version": "6.18.6", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", - "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "version": "6.18.7", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.7.tgz", + "integrity": "sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -264,22 +259,10 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@codemirror/lang-lezer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-lezer/-/lang-lezer-6.0.2.tgz", - "integrity": "sha512-mcVAf8lw+sCfSlr2ivMqV8JtNmOQjSXdA1vHKRtoW0OZsz1k6qhF+DX0K2TbWlAThqiGgRkRSZyYzIoEtKB2uQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/lezer": "^1.0.0" - } - }, "node_modules/@codemirror/lang-liquid": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.2.3.tgz", - "integrity": "sha512-yeN+nMSrf/lNii3FJxVVEGQwFG0/2eDyH6gNOj+TGCa0hlNO4bhQnoO5ISnd7JOG+7zTEcI/GOoyraisFVY7jQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz", + "integrity": "sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -293,9 +276,9 @@ } }, "node_modules/@codemirror/lang-markdown": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.3.tgz", - "integrity": "sha512-1fn1hQAPWlSSMCvnF810AkhWpNLkJpl66CRfIy3vVl20Sl4NwChkorCHqpMtNbXr1EuMJsrDnhEpjZxKZ2UX3A==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.4.tgz", + "integrity": "sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.7.1", @@ -357,9 +340,9 @@ } }, "node_modules/@codemirror/lang-sql": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.9.0.tgz", - "integrity": "sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.9.1.tgz", + "integrity": "sha512-ecSk3gm/mlINcURMcvkCZmXgdzPSq8r/yfCtTB4vgqGGIbBC2IJIAy7GqYTy5pgBEooTVmHP2GZK6Z7h63CDGg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -426,9 +409,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz", - "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -522,9 +505,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", - "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "version": "6.38.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.2.tgz", + "integrity": "sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -557,10 +540,786 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.2.18", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz", + "integrity": "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/node-gyp/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/node-gyp/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@electron/node-gyp/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/node-gyp/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/node-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.0.tgz", + "integrity": "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@electron/rebuild/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/@electron/rebuild/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -574,9 +1333,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -590,9 +1349,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -606,9 +1365,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -622,9 +1381,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -638,9 +1397,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -654,9 +1413,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -670,9 +1429,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -686,9 +1445,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -702,9 +1461,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -718,9 +1477,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -734,9 +1493,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -750,9 +1509,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -766,9 +1525,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -782,9 +1541,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -798,9 +1557,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -814,9 +1573,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -830,9 +1589,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -846,9 +1605,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -862,9 +1621,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -878,9 +1637,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -894,9 +1653,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -910,9 +1669,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -926,9 +1685,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -942,9 +1701,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -958,9 +1717,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -974,9 +1733,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1091,9 +1850,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -1128,31 +1887,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.2", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", - "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.2" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -1165,10 +1924,34 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@hookform/resolvers": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz", - "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz", + "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==", "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" @@ -1188,33 +1971,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1243,6 +2012,132 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1255,16 +2150,519 @@ "node": ">=18.0.0" } }, + "node_modules/@jimp/bmp": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.13.tgz", + "integrity": "sha512-9edAxu7N2FX7vzkdl5Jo1BbACfycUtBQX+XBMcHA2bk62P8R0otgkHg798frgAk/WxQIzwxqOH6wMiCwrlAzdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "bmp-js": "^0.1.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/core": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.16.13.tgz", + "integrity": "sha512-qXpA1tzTnlkTku9yqtuRtS/wVntvE6f3m3GNxdTdtmc+O+Wcg9Xo2ABPMh7Nc0AHbMKzwvwgB2JnjZmlmJEObg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "any-base": "^1.1.0", + "buffer": "^5.2.0", + "exif-parser": "^0.1.12", + "file-type": "^16.5.4", + "load-bmfont": "^1.3.1", + "mkdirp": "^0.5.1", + "phin": "^2.9.1", + "pixelmatch": "^4.0.2", + "tinycolor2": "^1.4.1" + } + }, + "node_modules/@jimp/custom": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.16.13.tgz", + "integrity": "sha512-LTATglVUPGkPf15zX1wTMlZ0+AU7cGEGF6ekVF1crA8eHUWsGjrYTB+Ht4E3HTrCok8weQG+K01rJndCp/l4XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/core": "^0.16.13" + } + }, + "node_modules/@jimp/gif": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.16.13.tgz", + "integrity": "sha512-yFAMZGv3o+YcjXilMWWwS/bv1iSqykFahFMSO169uVMtfQVfa90kt4/kDwrXNR6Q9i6VHpFiGZMlF2UnHClBvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "gifwrap": "^0.9.2", + "omggif": "^1.0.9" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/jpeg": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.16.13.tgz", + "integrity": "sha512-BJHlDxzTlCqP2ThqP8J0eDrbBfod7npWCbJAcfkKqdQuFk0zBPaZ6KKaQKyKxmWJ87Z6ohANZoMKEbtvrwz1AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "jpeg-js": "^0.4.2" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blit": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.16.13.tgz", + "integrity": "sha512-8Z1k96ZFxlhK2bgrY1JNWNwvaBeI/bciLM0yDOni2+aZwfIIiC7Y6PeWHTAvjHNjphz+XCt01WQmOYWCn0ML6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-blur": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.16.13.tgz", + "integrity": "sha512-PvLrfa8vkej3qinlebyhLpksJgCF5aiysDMSVhOZqwH5nQLLtDE9WYbnsofGw4r0VVpyw3H/ANCIzYTyCtP9Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-circle": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.16.13.tgz", + "integrity": "sha512-RNave7EFgZrb5V5EpdvJGAEHMnDAJuwv05hKscNfIYxf0kR3KhViBTDy+MoTnMlIvaKFULfwIgaZWzyhuINMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-color": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.16.13.tgz", + "integrity": "sha512-xW+9BtEvoIkkH/Wde9ql4nAFbYLkVINhpgAE7VcBUsuuB34WUbcBl/taOuUYQrPEFQJ4jfXiAJZ2H/rvKjCVnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "tinycolor2": "^1.4.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-contain": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.16.13.tgz", + "integrity": "sha512-QayTXw4tXMwU6q6acNTQrTTFTXpNRBe+MgTGMDU0lk+23PjlFCO/9sacflelG8lsp7vNHhAxFeHptDMAksEYzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-cover": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.16.13.tgz", + "integrity": "sha512-BSsP71GTNaqWRcvkbWuIVH+zK7b3TSNebbhDkFK0fVaUTzHuKMS/mgY4hDZIEVt7Rf5FjadAYtsujHN9w0iSYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5", + "@jimp/plugin-scale": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-crop": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.16.13.tgz", + "integrity": "sha512-WEl2tPVYwzYL8OKme6Go2xqiWgKsgxlMwyHabdAU4tXaRwOCnOI7v4021gCcBb9zn/oWwguHuKHmK30Fw2Z/PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-displace": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.16.13.tgz", + "integrity": "sha512-qt9WKq8vWrcjySa9DyQ0x/RBMHQeiVjdVSY1SJsMjssPUf0pS74qorcuAkGi89biN3YoGUgPkpqECnAWnYwgGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-dither": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.16.13.tgz", + "integrity": "sha512-5/N3yJggbWQTlGZHQYJPmQXEwR52qaXjEzkp1yRBbtdaekXE3BG/suo0fqeoV/csf8ooI78sJzYmIrxNoWVtgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-fisheye": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.16.13.tgz", + "integrity": "sha512-2rZmTdFbT/cF9lEZIkXCYO0TsT114Q27AX5IAo0Sju6jVQbvIk1dFUTnwLDadTo8wkJlFzGqMQ24Cs8cHWOliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-flip": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.16.13.tgz", + "integrity": "sha512-EmcgAA74FTc5u7Z+hUO/sRjWwfPPLuOQP5O64x5g4j0T12Bd29IgsYZxoutZo/rb3579+JNa/3wsSEmyVv1EpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-rotate": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-gaussian": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.16.13.tgz", + "integrity": "sha512-A1XKfGQD0iDdIiKqFYi8nZMv4dDVYdxbrmgR7y/CzUHhSYdcmoljLIIsZZM3Iks/Wa353W3vtvkWLuDbQbch1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-invert": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.16.13.tgz", + "integrity": "sha512-xFMrIn7czEZbdbMzZWuaZFnlLGJDVJ82y5vlsKsXRTG2kcxRsMPXvZRWHV57nSs1YFsNqXSbrC8B98n0E32njQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-mask": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.16.13.tgz", + "integrity": "sha512-wLRYKVBXql2GAYgt6FkTnCfE+q5NomM7Dlh0oIPGAoMBWDyTx0eYutRK6PlUrRK2yMHuroAJCglICTbxqGzowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-normalize": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.16.13.tgz", + "integrity": "sha512-3tfad0n9soRna4IfW9NzQdQ2Z3ijkmo21DREHbE6CGcMIxOSvfRdSvf1qQPApxjTSo8LTU4MCi/fidx/NZ0GqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-print": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.16.13.tgz", + "integrity": "sha512-0m6i3p01PGRkGAK9r53hDYrkyMq+tlhLOIbsSTmZyh6HLshUKlTB7eXskF5OpVd5ZUHoltlNc6R+ggvKIzxRFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "load-bmfont": "^1.4.0" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-resize": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.16.13.tgz", + "integrity": "sha512-qoqtN8LDknm3fJm9nuPygJv30O3vGhSBD2TxrsCnhtOsxKAqVPJtFVdGd/qVuZ8nqQANQmTlfqTiK9mVWQ7MiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-rotate": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.16.13.tgz", + "integrity": "sha512-Ev+Jjmj1nHYw897z9C3R9dYsPv7S2/nxdgfFb/h8hOwK0Ovd1k/+yYS46A0uj/JCKK0pQk8wOslYBkPwdnLorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blit": ">=0.3.5", + "@jimp/plugin-crop": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-scale": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.16.13.tgz", + "integrity": "sha512-05POQaEJVucjTiSGMoH68ZiELc7QqpIpuQlZ2JBbhCV+WCbPFUBcGSmE7w4Jd0E2GvCho/NoMODLwgcVGQA97A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-shadow": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.16.13.tgz", + "integrity": "sha512-nmu5VSZ9hsB1JchTKhnnCY+paRBnwzSyK5fhkhtQHHoFD5ArBQ/5wU8y6tCr7k/GQhhGq1OrixsECeMjPoc8Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-blur": ">=0.3.5", + "@jimp/plugin-resize": ">=0.3.5" + } + }, + "node_modules/@jimp/plugin-threshold": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.16.13.tgz", + "integrity": "sha512-+3zArBH0OE3Rhjm4HyAokMsZlIq5gpQec33CncyoSwxtRBM2WAhUVmCUKuBo+Lr/2/4ISoY4BWpHKhMLDix6cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5", + "@jimp/plugin-color": ">=0.8.0", + "@jimp/plugin-resize": ">=0.8.0" + } + }, + "node_modules/@jimp/plugins": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.16.13.tgz", + "integrity": "sha512-CJLdqODEhEVs4MgWCxpWL5l95sCBlkuSLz65cxEm56X5akIsn4LOlwnKoSEZioYcZUBvHhCheH67AyPTudfnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/plugin-blit": "^0.16.13", + "@jimp/plugin-blur": "^0.16.13", + "@jimp/plugin-circle": "^0.16.13", + "@jimp/plugin-color": "^0.16.13", + "@jimp/plugin-contain": "^0.16.13", + "@jimp/plugin-cover": "^0.16.13", + "@jimp/plugin-crop": "^0.16.13", + "@jimp/plugin-displace": "^0.16.13", + "@jimp/plugin-dither": "^0.16.13", + "@jimp/plugin-fisheye": "^0.16.13", + "@jimp/plugin-flip": "^0.16.13", + "@jimp/plugin-gaussian": "^0.16.13", + "@jimp/plugin-invert": "^0.16.13", + "@jimp/plugin-mask": "^0.16.13", + "@jimp/plugin-normalize": "^0.16.13", + "@jimp/plugin-print": "^0.16.13", + "@jimp/plugin-resize": "^0.16.13", + "@jimp/plugin-rotate": "^0.16.13", + "@jimp/plugin-scale": "^0.16.13", + "@jimp/plugin-shadow": "^0.16.13", + "@jimp/plugin-threshold": "^0.16.13", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.16.13.tgz", + "integrity": "sha512-8cGqINvbWJf1G0Her9zbq9I80roEX0A+U45xFby3tDWfzn+Zz8XKDF1Nv9VUwVx0N3zpcG1RPs9hfheG4Cq2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/utils": "^0.16.13", + "pngjs": "^3.3.3" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/png/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@jimp/tiff": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.16.13.tgz", + "integrity": "sha512-oJY8d9u95SwW00VPHuCNxPap6Q1+E/xM5QThb9Hu+P6EGuu6lIeLaNBMmFZyblwFbwrH+WBOZlvIzDhi4Dm/6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "utif": "^2.0.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/types": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.16.13.tgz", + "integrity": "sha512-mC0yVNUobFDjoYLg4hoUwzMKgNlxynzwt3cDXzumGvRJ7Kb8qQGOWJQjQFo5OxmGExqzPphkirdbBF88RVLBCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/bmp": "^0.16.13", + "@jimp/gif": "^0.16.13", + "@jimp/jpeg": "^0.16.13", + "@jimp/png": "^0.16.13", + "@jimp/tiff": "^0.16.13", + "timm": "^1.6.1" + }, + "peerDependencies": { + "@jimp/custom": ">=0.3.5" + } + }, + "node_modules/@jimp/utils": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.16.13.tgz", + "integrity": "sha512-VyCpkZzFTHXtKgVO35iKN0sYR10psGpV6SkcSeV4oF7eSYlR8Bl6aQLCzVeFjvESF7mxTmIiI3/XrMobVrtxDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "regenerator-runtime": "^0.13.3" + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1275,15 +2673,15 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1361,9 +2759,9 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz", - "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.3.tgz", + "integrity": "sha512-jexmlKq5NpGiB7t+0QkyhSXRgaiab5YisHIQW9C7EcU19KSUsDguZe9WY+rmRDg34nXoNH2LQ4SxpC+aJUchSQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1382,16 +2780,6 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@lezer/lezer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/lezer/-/lezer-1.1.2.tgz", - "integrity": "sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==", - "license": "MIT", - "dependencies": { - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@lezer/lr": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", @@ -1412,9 +2800,9 @@ } }, "node_modules/@lezer/php": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.3.tgz", - "integrity": "sha512-NDwgktd5qh/EfEn7Dogf2N6eNnC5WPJ5NslB8nKhgXSuH2uSJamCEom1g4VGo+ibfoADK8D69NMCMhuuYbVskg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.4.tgz", + "integrity": "sha512-D2dJ0t8Z28/G1guztRczMFvPDUqzeMLSQbdWQmaiHV7urc8NlEOnjYk9UrZ531OcLiRxD4Ihcbv7AsDpNKDRaQ==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -1477,31 +2865,90 @@ "@lezer/lr": "^1.4.0" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, - "node_modules/@nextjournal/lang-clojure": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nextjournal/lang-clojure/-/lang-clojure-1.0.0.tgz", - "integrity": "sha512-gOCV71XrYD0DhwGoPMWZmZ0r92/lIHsqQu9QWdpZYYBwiChNwMO4sbVMP7eTuAqffFB2BTtCSC+1skSH9d3bNg==", - "license": "ISC", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@nextjournal/lezer-clojure": "1.0.0" - } - }, - "node_modules/@nextjournal/lezer-clojure": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nextjournal/lezer-clojure/-/lezer-clojure-1.0.0.tgz", - "integrity": "sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==", - "license": "ISC", - "dependencies": { - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1540,6 +2987,59 @@ "node": ">= 8" } }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -1547,19 +3047,19 @@ "license": "MIT" }, "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, "node_modules/@radix-ui/react-accordion": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", - "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -1634,15 +3134,15 @@ } }, "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", - "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", @@ -1664,16 +3164,16 @@ } }, "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", - "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -1785,13 +3285,22 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", @@ -1818,98 +3327,17 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", - "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, @@ -1929,9 +3357,9 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -2010,25 +3438,25 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", @@ -2050,21 +3478,21 @@ } }, "node_modules/@radix-ui/react-popover": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", - "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", @@ -2087,9 +3515,9 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", @@ -2143,9 +3571,9 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", @@ -2214,12 +3642,12 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -2245,17 +3673,17 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.9.tgz", - "integrity": "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" @@ -2276,22 +3704,22 @@ } }, "node_modules/@radix-ui/react-select": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", - "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", @@ -2342,13 +3770,13 @@ } }, "node_modules/@radix-ui/react-slider": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz", - "integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", @@ -2393,12 +3821,12 @@ } }, "node_modules/@radix-ui/react-switch": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", - "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", @@ -2422,18 +3850,18 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", - "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", + "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { @@ -2485,95 +3913,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2757,21 +4096,6 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, - "node_modules/@replit/codemirror-lang-csharp": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz", - "integrity": "sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==", - "license": "MIT", - "peerDependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, "node_modules/@replit/codemirror-lang-nix": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz", @@ -2819,16 +4143,16 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.11.tgz", - "integrity": "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==", + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz", - "integrity": "sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", "cpu": [ "arm" ], @@ -2839,9 +4163,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.0.tgz", - "integrity": "sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", "cpu": [ "arm64" ], @@ -2852,9 +4176,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.0.tgz", - "integrity": "sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", "cpu": [ "arm64" ], @@ -2865,9 +4189,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.0.tgz", - "integrity": "sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", "cpu": [ "x64" ], @@ -2878,9 +4202,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.0.tgz", - "integrity": "sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", "cpu": [ "arm64" ], @@ -2891,9 +4215,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.0.tgz", - "integrity": "sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", "cpu": [ "x64" ], @@ -2904,9 +4228,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.0.tgz", - "integrity": "sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", "cpu": [ "arm" ], @@ -2917,9 +4241,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.0.tgz", - "integrity": "sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", "cpu": [ "arm" ], @@ -2930,9 +4254,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.0.tgz", - "integrity": "sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", "cpu": [ "arm64" ], @@ -2943,9 +4267,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.0.tgz", - "integrity": "sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", "cpu": [ "arm64" ], @@ -2956,9 +4280,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.0.tgz", - "integrity": "sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", "cpu": [ "loong64" ], @@ -2968,10 +4292,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.0.tgz", - "integrity": "sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", "cpu": [ "ppc64" ], @@ -2982,9 +4306,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.0.tgz", - "integrity": "sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", "cpu": [ "riscv64" ], @@ -2995,9 +4319,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.0.tgz", - "integrity": "sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", "cpu": [ "riscv64" ], @@ -3008,9 +4332,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.0.tgz", - "integrity": "sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", "cpu": [ "s390x" ], @@ -3021,9 +4345,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.0.tgz", - "integrity": "sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", "cpu": [ "x64" ], @@ -3034,9 +4358,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.0.tgz", - "integrity": "sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", "cpu": [ "x64" ], @@ -3046,10 +4370,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.0.tgz", - "integrity": "sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", "cpu": [ "arm64" ], @@ -3060,9 +4397,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.0.tgz", - "integrity": "sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", "cpu": [ "ia32" ], @@ -3073,9 +4410,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.0.tgz", - "integrity": "sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", "cpu": [ "x64" ], @@ -3085,6 +4422,43 @@ "win32" ] }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -3092,15 +4466,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.12.14.tgz", - "integrity": "sha512-CJSn2vstd17ddWIHBsjuD4OQnn9krQfaq6EO+w9YfId5DKznyPmzxAARlOXG99cC8/3Kli8ysKy6phL43bSr0w==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.23" + "@swc/types": "^0.1.24" }, "engines": { "node": ">=10" @@ -3110,16 +4484,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.12.14", - "@swc/core-darwin-x64": "1.12.14", - "@swc/core-linux-arm-gnueabihf": "1.12.14", - "@swc/core-linux-arm64-gnu": "1.12.14", - "@swc/core-linux-arm64-musl": "1.12.14", - "@swc/core-linux-x64-gnu": "1.12.14", - "@swc/core-linux-x64-musl": "1.12.14", - "@swc/core-win32-arm64-msvc": "1.12.14", - "@swc/core-win32-ia32-msvc": "1.12.14", - "@swc/core-win32-x64-msvc": "1.12.14" + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -3131,9 +4505,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.12.14.tgz", - "integrity": "sha512-HNukQoOKgMsHSETj8vgGGKK3SEcH7Cz6k4bpntCxBKNkO3sH7RcBTDulWGGHJfZaDNix7Rw2ExUVWtLZlzkzXg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", "cpu": [ "arm64" ], @@ -3148,9 +4522,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.12.14.tgz", - "integrity": "sha512-4Ttf3Obtk3MvFrR0e04qr6HfXh4L1Z+K3dRej63TAFuYpo+cPXeOZdPUddAW73lSUGkj+61IHnGPoXD3OQYy4Q==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", "cpu": [ "x64" ], @@ -3165,9 +4539,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.12.14.tgz", - "integrity": "sha512-zhJOH2KWjtQpzJ27Xjw/RKLVOa1aiEJC2b70xbCwEX6ZTVAl8tKbhkZ3GMphhfVmLJ9gf/2UQR58oxVnsXqX5Q==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", "cpu": [ "arm" ], @@ -3182,9 +4556,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.12.14.tgz", - "integrity": "sha512-akUAe1YrBqZf1EDdUxahQ8QZnJi8Ts6Ya0jf6GBIMvnXL4Y6QIuvKTRwfNxy7rJ+x9zpzP1Vlh14ZZkSKZ1EGA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", "cpu": [ "arm64" ], @@ -3199,9 +4573,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.12.14.tgz", - "integrity": "sha512-ZkOOIpSMXuPAjfOXEIAEQcrPOgLi6CaXvA5W+GYnpIpFG21Nd0qb0WbwFRv4K8BRtl993Q21v0gPpOaFHU+wdA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", "cpu": [ "arm64" ], @@ -3216,9 +4590,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.12.14.tgz", - "integrity": "sha512-71EPPccwJiJUxd2aMwNlTfom2mqWEWYGdbeTju01tzSHsEuD7E6ePlgC3P3ngBqB3urj41qKs87z7zPOswT5Iw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", "cpu": [ "x64" ], @@ -3233,9 +4607,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.12.14.tgz", - "integrity": "sha512-nImF1hZJqKTcl0WWjHqlelOhvuB9rU9kHIw/CmISBUZXogjLIvGyop1TtJNz0ULcz2Oxr3Q2YpwfrzsgvgbGkA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", "cpu": [ "x64" ], @@ -3250,9 +4624,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.12.14.tgz", - "integrity": "sha512-sABFQFxSuStFoxvEWZUHWYldtB1B4A9eDNFd4Ty50q7cemxp7uoscFoaCqfXSGNBwwBwpS5EiPB6YN4y6hqmLQ==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", "cpu": [ "arm64" ], @@ -3267,9 +4641,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.12.14.tgz", - "integrity": "sha512-KBznRB02NASkpepRdWIK4f1AvmaJCDipKWdW1M1xV9QL2tE4aySJFojVuG1+t0tVDkjRfwcZjycQfRoJ4RjD7Q==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", "cpu": [ "ia32" ], @@ -3284,9 +4658,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.12.14", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.12.14.tgz", - "integrity": "sha512-SymoP2CJHzrYaFKjWvuQljcF7BkTpzaS1vpywv7K9EzdTb5N8qPDvNd+PhWUqBz9JHBhbJxpaeTDQBXF/WWPmw==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", "cpu": [ "x64" ], @@ -3308,34 +4682,47 @@ "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.23", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.23.tgz", - "integrity": "sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==", + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } }, - "node_modules/@tailwindcss/node": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", - "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "enhanced-resolve": "^5.18.1", - "jiti": "^2.4.2", + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", "lightningcss": "1.30.1", - "magic-string": "^0.30.17", + "magic-string": "^0.30.18", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.11" + "tailwindcss": "4.1.13" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", - "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3346,24 +4733,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-arm64": "4.1.11", - "@tailwindcss/oxide-darwin-x64": "4.1.11", - "@tailwindcss/oxide-freebsd-x64": "4.1.11", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", - "@tailwindcss/oxide-linux-x64-musl": "4.1.11", - "@tailwindcss/oxide-wasm32-wasi": "4.1.11", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", - "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", "cpu": [ "arm64" ], @@ -3377,9 +4764,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", - "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", "cpu": [ "arm64" ], @@ -3393,9 +4780,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", - "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", "cpu": [ "x64" ], @@ -3409,9 +4796,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", - "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", "cpu": [ "x64" ], @@ -3425,9 +4812,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", - "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", "cpu": [ "arm" ], @@ -3441,9 +4828,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", - "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", "cpu": [ "arm64" ], @@ -3457,9 +4844,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", - "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", "cpu": [ "arm64" ], @@ -3473,9 +4860,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", - "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", "cpu": [ "x64" ], @@ -3489,9 +4876,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", - "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", "cpu": [ "x64" ], @@ -3505,9 +4892,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -3522,75 +4909,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", "cpu": [ "arm64" ], @@ -3604,9 +4937,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", "cpu": [ "x64" ], @@ -3620,19 +4953,36 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", - "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.11", - "@tailwindcss/oxide": "4.1.11", - "tailwindcss": "4.1.11" + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "tailwindcss": "4.1.13" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3687,6 +5037,19 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -3706,6 +5069,16 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3735,6 +5108,23 @@ "@types/send": "*" } }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -3759,6 +5149,16 @@ "@types/node": "*" } }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -3782,14 +5182,26 @@ } }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "license": "MIT", "dependencies": { "undici-types": "~7.10.0" } }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/qrcode": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", @@ -3812,9 +5224,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", - "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3822,15 +5234,25 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", - "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "version": "19.1.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", + "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/send": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", @@ -3872,9 +5294,9 @@ } }, "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.119", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.119.tgz", - "integrity": "sha512-d0F6m9itIPaKnrvEMlzE48UjwZaAnFW7Jwibacw9MNdqadjKNpUm9tfJYDwmShJmgqcoqYUX3EMKO1+RWiuuNg==", + "version": "18.19.124", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.124.tgz", + "integrity": "sha512-hY4YWZFLs3ku6D2Gqo3RchTd9VRCcrjqp/I0mmohYeUVA5Y8eCXKJEasHxLAJVZRJuQogfd1GiJ9lgogBgKeuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3888,6 +5310,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -3898,18 +5328,29 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", - "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz", + "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/type-utils": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/type-utils": "8.43.0", + "@typescript-eslint/utils": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3923,7 +5364,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.40.0", + "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -3939,16 +5380,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", - "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz", + "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4" }, "engines": { @@ -3964,14 +5405,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz", + "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", + "@typescript-eslint/tsconfig-utils": "^8.43.0", + "@typescript-eslint/types": "^8.43.0", "debug": "^4.3.4" }, "engines": { @@ -3986,14 +5427,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", - "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz", + "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0" + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4004,9 +5445,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz", + "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==", "dev": true, "license": "MIT", "engines": { @@ -4021,15 +5462,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", - "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz", + "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/utils": "8.43.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -4046,9 +5487,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz", + "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==", "dev": true, "license": "MIT", "engines": { @@ -4060,16 +5501,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz", + "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", + "@typescript-eslint/project-service": "8.43.0", + "@typescript-eslint/tsconfig-utils": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/visitor-keys": "8.43.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -4115,16 +5556,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", - "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz", + "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0" + "@typescript-eslint/scope-manager": "8.43.0", + "@typescript-eslint/types": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4139,13 +5580,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz", + "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.40.0", + "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -4157,9 +5598,9 @@ } }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.24.1.tgz", - "integrity": "sha512-o1m1a8eUS3fWERMbDFvN8t8sZUFPgDKNemmlQ5Ot2vKm+Ax84lKP1dhEFgkiOaZ1bDHk4T5h6SjHuTghrJHKww==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.1.tgz", + "integrity": "sha512-zxgA2QkvP3ZDKxTBc9UltNFTrSeFezGXcZtZj6qcsBxiMzowoEMP5mVwXcKjpzldpZVRuY+JCC+RsekEgid4vg==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -4184,9 +5625,9 @@ } }, "node_modules/@uiw/codemirror-extensions-hyper-link": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-hyper-link/-/codemirror-extensions-hyper-link-4.24.1.tgz", - "integrity": "sha512-qf3docpmsHHM0OKLO5m2Fc8t4G+pr1+k9QwrhlM2iolku/INbz+B1JzbRcSU0ow1EcxKtHRtCFE4Lnu6DwP7CQ==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-hyper-link/-/codemirror-extensions-hyper-link-4.25.1.tgz", + "integrity": "sha512-BVp+bnPI0LtqYXAPFWBqpLLLICoD8QsTAC/KQVRf7l+MO8FXCP0F/4WoM724eU4/2bcLefBkK1gBgCB1+Ug1CQ==", "license": "MIT", "funding": { "url": "https://jaywcjlove.github.io/#/sponsor" @@ -4197,34 +5638,13 @@ } }, "node_modules/@uiw/codemirror-extensions-langs": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.24.1.tgz", - "integrity": "sha512-8Q33k/UhNni2u5VvAHD+2mxe4hNIqZTNySSUcnJ7urV2lXXau+0fimsQlI+GQLF7gy5F1BUzIi+yvOMrEPK9Ig==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.1.tgz", + "integrity": "sha512-P9Sxk0w8WgxxoOK4hC2yNV2f3shE0CH8gmk8lG5rDrAYYyuUrTsTmJANXh30TuQWCPCkEXwXZZVy+dbTYAgvMQ==", "license": "MIT", "dependencies": { - "@codemirror/lang-angular": "^0.1.0", - "@codemirror/lang-cpp": "^6.0.0", - "@codemirror/lang-css": "^6.2.0", - "@codemirror/lang-html": "^6.4.0", - "@codemirror/lang-java": "^6.0.0", - "@codemirror/lang-javascript": "^6.1.0", - "@codemirror/lang-json": "^6.0.0", - "@codemirror/lang-less": "^6.0.1", - "@codemirror/lang-lezer": "^6.0.0", - "@codemirror/lang-liquid": "^6.0.1", - "@codemirror/lang-markdown": "^6.1.0", - "@codemirror/lang-php": "^6.0.0", - "@codemirror/lang-python": "^6.1.0", - "@codemirror/lang-rust": "^6.0.0", - "@codemirror/lang-sass": "^6.0.1", - "@codemirror/lang-sql": "^6.4.0", - "@codemirror/lang-vue": "^0.1.1", - "@codemirror/lang-wast": "^6.0.0", - "@codemirror/lang-xml": "^6.0.0", - "@codemirror/language-data": ">=6.0.0", - "@codemirror/legacy-modes": ">=6.0.0", - "@nextjournal/lang-clojure": "^1.0.0", - "@replit/codemirror-lang-csharp": "^6.1.0", + "@codemirror/language": "^6.0.0", + "@codemirror/language-data": "^6.5.1", "@replit/codemirror-lang-nix": "^6.0.1", "@replit/codemirror-lang-solidity": "^6.0.1", "@replit/codemirror-lang-svelte": "^6.0.0", @@ -4234,14 +5654,14 @@ "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "@codemirror/language-data": ">=6.0.0", - "@codemirror/legacy-modes": ">=6.0.0" + "@codemirror/language": ">=6.0.0", + "@codemirror/language-data": ">=6.0.0" } }, "node_modules/@uiw/codemirror-themes": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.24.1.tgz", - "integrity": "sha512-hduBbFNiWNW6nYa2/giKQ9YpzhWNw87BGpCjC+cXYMZ7bCD6q5DC6Hw+7z7ZwSzEaOQvV91lmirOjJ8hn9+pkg==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.1.tgz", + "integrity": "sha512-6o8tQ8bdq14RuVFpZ7l9u8KnuPq824uG3U1VV933Uhv8mfaxaoaOQSjv6T2bQUPhjH6ZlEu5+tAMkOfIL21eIQ==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -4258,16 +5678,16 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.24.1", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.24.1.tgz", - "integrity": "sha512-BivF4NLqbuBQK5gPVhSkOARi9nPXw8X5r25EnInPeY+I9l1dfEX8O9V6+0xHTlGHyUo0cNfGEF9t1KHEicUfJw==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.1.tgz", + "integrity": "sha512-eESBKHndoYkaEGlKCwRO4KrnTw1HkWBxVpEeqntoWTpoFEUYxdLWUYmkPBVk4/u8YzVy9g91nFfIRpqe5LjApg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.24.1", + "@uiw/codemirror-extensions-basic-setup": "4.25.1", "codemirror": "^6.0.0" }, "funding": { @@ -4279,22 +5699,32 @@ "@codemirror/theme-one-dark": ">=6.0.0", "@codemirror/view": ">=6.0.0", "codemirror": ">=6.0.0", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "react": ">=17.0.0", + "react-dom": ">=17.0.0" } }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.10.2.tgz", - "integrity": "sha512-xD3Rdvrt5LgANug7WekBn1KhcvLn1H3jNBfJRL3reeOIua/WnZOEV5qi5qIBq5T8R0jUDmRtxuvk4bPhzGHDWw==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.11", - "@swc/core": "^1.11.31" + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7.0.0-beta.0" + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" } }, "node_modules/@xterm/addon-attach": { @@ -4360,6 +5790,33 @@ "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -4409,6 +5866,43 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4426,6 +5920,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4450,6 +5954,221 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz", + "integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.0.12.tgz", + "integrity": "sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.2.18", + "@electron/fuses": "^1.8.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.7.0", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.0.11", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "plist": "3.1.0", + "resedit": "^1.7.0", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.0.12", + "electron-builder-squirrel-windows": "26.0.12" + } + }, + "node_modules/app-builder-lib/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/app-builder-lib/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -4470,6 +6189,110 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/args": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.3.tgz", + "integrity": "sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/args/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/args/node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/args/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/args/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/args/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/args/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/args/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/args/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -4482,6 +6305,16 @@ "node": ">=10" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -4491,12 +6324,70 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -4535,14 +6426,31 @@ "postcss": "^8.1.0" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -4631,6 +6539,20 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -4651,6 +6573,15 @@ "node": ">=18" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4676,9 +6607,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -4696,8 +6627,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -4732,6 +6663,29 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -4753,6 +6707,84 @@ "node": ">=10.0.0" } }, + "node_modules/builder-util": { + "version": "26.0.11", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.0.11.tgz", + "integrity": "sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz", + "integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -4773,6 +6805,204 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4822,9 +7052,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -4842,6 +7072,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4867,6 +7114,29 @@ "node": ">=18" } }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4879,6 +7149,60 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -4890,6 +7214,39 @@ "wrap-ansi": "^6.2.0" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4899,6 +7256,16 @@ "node": ">=6" } }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", @@ -4955,6 +7322,26 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4977,6 +7364,177 @@ "typedarray": "^0.0.6" } }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/concurrently/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/concurrently/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -5020,20 +7578,18 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie-signature": { + "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" }, "node_modules/cors": { "version": "2.8.5", @@ -5062,6 +7618,17 @@ "node": ">=10.0.0" } }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -5075,6 +7642,15 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -5119,6 +7695,54 @@ "node": ">= 8" } }, + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn-windows-exe/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5126,6 +7750,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -5192,6 +7829,90 @@ "dev": true, "license": "MIT" }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5219,6 +7940,14 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -5241,10 +7970,162 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.0.12.tgz", + "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, "node_modules/dotenv": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", - "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -5254,9 +8135,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.44.3", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.3.tgz", - "integrity": "sha512-8nIiYQxOpgUicEL04YFojJmvC4DNO4KoyXsEIqN44+g6gNBr6hmVpWk3uyAt4CaTiRGDwoU+alfqNNeonLAFOQ==", + "version": "0.44.5", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz", + "integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -5392,6 +8273,24 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -5407,13 +8306,510 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "38.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-38.0.0.tgz", + "integrity": "sha512-egljptiPJqbL/oamFCEY+g3RNeONWTVxZSGeyLqzK8xq106JhzuxnhJZ3sxt4DzJFaofbGyGJA37Oe9d+gVzYw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.0.12.tgz", + "integrity": "sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "dmg-builder": "26.0.12", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.0.12", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.0.12.tgz", + "integrity": "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.0.12", + "builder-util": "26.0.11", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-builder/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-icon-builder": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/electron-icon-builder/-/electron-icon-builder-2.0.1.tgz", + "integrity": "sha512-rg9BxW2kJi3TXsMFFNXWXrwQEd5dzXmeD+w7Pj3k3z7aYRePLxE89qU4lvL/rK1X/NTY5KDn3+Dbgm1TU2dGXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "args": "^5.0.1", + "icon-gen": "^2.0.0", + "jimp": "^0.16.1" + }, + "bin": { + "electron-icon-builder": "index.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-17.1.2.tgz", + "integrity": "sha512-XofXdikjYI7MVBcnXeoOvRR+yFFFHOLs3J7PF5KYQweigtgLshcH4W660PsvHr4lYZ03JBpLyEcUB8DzHZ+BNw==", + "deprecated": "Please use @electron/packager moving forward. There is no API change, just a package name change", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@electron/asar": "^3.2.1", + "@electron/get": "^2.0.0", + "@electron/notarize": "^1.2.3", + "@electron/osx-sign": "^1.0.5", + "@electron/universal": "^1.3.2", + "cross-spawn-windows-exe": "^1.2.0", + "debug": "^4.0.1", + "extract-zip": "^2.0.0", + "filenamify": "^4.1.0", + "fs-extra": "^11.1.0", + "galactus": "^1.0.0", + "get-package-info": "^1.0.0", + "junk": "^3.1.0", + "parse-author": "^2.0.0", + "plist": "^3.0.0", + "rcedit": "^3.0.1", + "resolve": "^1.1.6", + "semver": "^7.1.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "electron-packager": "bin/electron-packager.js" + }, + "engines": { + "node": ">= 14.17.5" + }, + "funding": { + "url": "https://github.com/electron/electron-packager?sponsor=1" + } + }, + "node_modules/electron-packager/node_modules/@electron/notarize": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz", + "integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager/node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-packager/node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/electron-packager/node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-packager/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-packager/node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/electron-packager/node_modules/fs-extra": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", + "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/electron-packager/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-packager/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-packager/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish": { + "version": "26.0.11", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.0.11.tgz", + "integrity": "sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.0.11", + "builder-util-runtime": "9.3.1", + "chalk": "^4.1.2", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.187", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz", - "integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==", + "version": "1.5.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz", + "integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==", "dev": true, "license": "ISC" }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "22.18.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz", + "integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -5429,6 +8825,16 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -5439,9 +8845,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -5451,6 +8857,33 @@ "node": ">=10.13.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5496,10 +8929,25 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -5509,32 +8957,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -5567,19 +9015,19 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -5753,6 +9201,32 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "dev": true + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -5762,6 +9236,13 @@ "node": ">=6" } }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -5804,6 +9285,54 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5865,6 +9394,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -5901,12 +9440,101 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/file-url": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/file-url/-/file-url-2.0.2.tgz", + "integrity": "sha512-x3989K8a1jM6vulMigE8VngH7C5nci0Ks5d9kVjUXmNF28gmiZUNujk5HjwaS8dAzN2QmUfX56riJKgN00dNRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5975,10 +9603,62 @@ "dev": true, "license": "ISC" }, + "node_modules/flora-colossus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", + "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/flora-colossus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/flora-colossus/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/flora-colossus/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -5995,6 +9675,46 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -6082,6 +9802,61 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6105,6 +9880,59 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/galactus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", + "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "flora-colossus": "^2.0.0", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/galactus/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/galactus/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/galactus/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6147,6 +9975,39 @@ "node": ">=6" } }, + "node_modules/get-package-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", + "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/get-package-info/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-package-info/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6160,12 +10021,71 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gifwrap": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.4.tgz", + "integrity": "sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "image-q": "^4.0.0", + "omggif": "^1.0.10" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6179,10 +10099,40 @@ "node": ">=10.13.0" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -6192,6 +10142,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6204,6 +10193,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -6217,6 +10232,31 @@ "dev": true, "license": "MIT" }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -6226,6 +10266,20 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -6253,6 +10307,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha512-jZ38TU/EBiGKrmyTNNZgnvCZHNowiRI4+w/I9noMlekHTZH3KyGgvJLmhSgykeAQ9j2SYPDosM0Bg3wHfzibAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^1.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6265,6 +10333,19 @@ "node": ">= 0.4" } }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -6274,6 +10355,13 @@ "void-elements": "3.1.0" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -6299,10 +10387,78 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/i18next": { - "version": "25.4.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz", - "integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz", + "integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==", "funding": [ { "type": "individual", @@ -6348,6 +10504,78 @@ "cross-fetch": "4.0.0" } }, + "node_modules/icon-gen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/icon-gen/-/icon-gen-2.1.0.tgz", + "integrity": "sha512-rqIVvq9MJ8X7wnJW0NO8Eau/+5RWV7AH6L5vEt/U5Ajv5WefdDNDxGwJhGokyHuyBWeX7JqRMQ03tG0gAco4Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^6.2.0", + "del": "^6.0.0", + "mkdirp": "^1.0.4", + "pngjs": "^6.0.0", + "svg2png": "4.1.1", + "uuid": "^8.3.1" + }, + "bin": { + "icon-gen": "dist/bin/index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/icon-gen/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/icon-gen/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/icon-gen/node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6390,6 +10618,23 @@ "node": ">= 4" } }, + "node_modules/image-q": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz", + "integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "16.9.1" + } + }, + "node_modules/image-q/node_modules/@types/node": { + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "dev": true, + "license": "MIT" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -6417,6 +10662,35 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6429,6 +10703,26 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6438,6 +10732,58 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6457,6 +10803,13 @@ "node": ">=8" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6470,6 +10823,23 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -6480,12 +10850,102 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz", + "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6493,15 +10953,84 @@ "dev": true, "license": "ISC" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jimp": { + "version": "0.16.13", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.16.13.tgz", + "integrity": "sha512-Bxz8q7V4rnCky9A0ktTNGA9SkNFVWRHodddI/DaAWZJzF7sVUlFYKQ60y9JGqrKpi48ECA/TnfMzzc5C70VByA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.7.2", + "@jimp/custom": "^0.16.13", + "@jimp/plugins": "^0.16.13", + "@jimp/types": "^0.16.13", + "regenerator-runtime": "^0.13.3" + } + }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/jose": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", @@ -6511,10 +11040,17 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", "license": "BSD-3-Clause" }, "node_modules/js-yaml": { @@ -6530,6 +11066,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6537,6 +11080,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6551,6 +11101,36 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -6573,6 +11153,57 @@ "npm": ">=6" } }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsprim/node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/jsprim/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", @@ -6594,6 +11225,13 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kew": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha512-IG6nm0+QtAMdXt9KvbgbGdvY50RSrw+U4sGZg+KlrSKPJEwVE5JVoI3d7RWfSMdBQneRheeAOj3lIjX5VL/9RQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6604,6 +11242,46 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6846,6 +11524,75 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/load-bmfont/node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/load-bmfont/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-bmfont/node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6862,6 +11609,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -6911,6 +11673,53 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.525.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", @@ -6921,12 +11730,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/make-error": { @@ -6936,6 +11745,130 @@ "dev": true, "license": "ISC" }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6990,6 +11923,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -7011,6 +11957,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -7023,6 +11979,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dev": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7054,6 +12019,190 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minizlib": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", @@ -7067,18 +12216,15 @@ } }, "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "mkdirp": "bin/cmd.js" } }, "node_modules/mkdirp-classic": { @@ -7087,6 +12233,16 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7141,18 +12297,6 @@ "node": ">= 0.6" } }, - "node_modules/multer/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/multer/node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -7224,9 +12368,9 @@ } }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -7235,6 +12379,24 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -7274,12 +12436,58 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", @@ -7290,6 +12498,39 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7311,6 +12552,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/omggif": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", + "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==", + "dev": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -7332,6 +12591,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7350,6 +12625,53 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -7382,6 +12704,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -7391,6 +12729,20 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7404,6 +12756,64 @@ "node": ">=6" } }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7422,6 +12832,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -7432,15 +12852,260 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", + "integrity": "sha512-PIiRzBhW85xco2fuj41FmsyuYHKjKuXWmhjy3A/Y+CMpN/63TV+s9uzfVhsUwFe0G77xWtHBG8xmXf5BqEUEuQ==", + "deprecated": "this package is now deprecated", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "es6-promise": "^4.0.3", + "extract-zip": "^1.6.5", + "fs-extra": "^1.0.0", + "hasha": "^2.2.0", + "kew": "^0.7.0", + "progress": "^1.1.8", + "request": "^2.81.0", + "request-progress": "^2.0.1", + "which": "^1.2.10" + }, + "bin": { + "phantomjs": "bin/phantomjs" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/extract-zip": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", + "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "concat-stream": "^1.6.2", + "debug": "^2.6.9", + "mkdirp": "^0.5.4", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/fs-extra": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", + "integrity": "sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^2.1.0", + "klaw": "^1.0.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt/node_modules/progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/phantomjs-prebuilt/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/phantomjs-prebuilt/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/phin": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/phin/-/phin-2.9.3.tgz", + "integrity": "sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7460,6 +13125,84 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true, + "license": "MIT" + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -7522,6 +13265,36 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -7558,6 +13331,80 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7577,6 +13424,19 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -7650,6 +13510,19 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7660,18 +13533,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/rc": { @@ -7698,31 +13587,44 @@ "node": ">=0.10.0" } }, + "node_modules/rcedit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-3.1.0.tgz", + "integrity": "sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn-windows-exe": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.1.1" } }, "node_modules/react-hook-form": { - "version": "7.60.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz", - "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -7809,15 +13711,25 @@ } }, "node_modules/react-resizable-panels": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.3.tgz", - "integrity": "sha512-7HA8THVBHTzhDK4ON0tvlGXyMAJN1zBeRpuyyremSikgYh2ku6ltD7tsGQOcXx4NKPrZtYCm/5CBr+dkruTGQw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.5.tgz", + "integrity": "sha512-3z1yN25DMTXLg2wfyFrW32r5k4WEcUa3F7cJ2EgtNK07lnOs4mpM8yWLGunCpkhcQRwJX4fqoLcIh/pHPxzlmQ==", "license": "MIT", "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/react-simple-keyboard": { + "version": "3.8.122", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.122.tgz", + "integrity": "sha512-9u8Boglwtoa/SpZO3UyyEhs17z3vYTRFWS93Ihc6E8JFcCpa5kzt11IwWo5qa9KZqutqWuD5ara1mf5+WJYVGQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -7849,6 +13761,121 @@ "@xterm/xterm": "^5.5.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7863,6 +13890,174 @@ "node": ">= 6" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-progress": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha512-dxdraeZVUNEn9AvLrxkgB2k6buTlym71dJk1fk4v8j3Ou3RKNm07BcgbHdj2lLgYGfqX71F+awb1MR+tWPFJzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7878,6 +14073,52 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7888,6 +14129,43 @@ "node": ">=4" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -7899,10 +14177,46 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rollup": { - "version": "4.45.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.0.tgz", - "integrity": "sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==", + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -7915,26 +14229,27 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.45.0", - "@rollup/rollup-android-arm64": "4.45.0", - "@rollup/rollup-darwin-arm64": "4.45.0", - "@rollup/rollup-darwin-x64": "4.45.0", - "@rollup/rollup-freebsd-arm64": "4.45.0", - "@rollup/rollup-freebsd-x64": "4.45.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.45.0", - "@rollup/rollup-linux-arm-musleabihf": "4.45.0", - "@rollup/rollup-linux-arm64-gnu": "4.45.0", - "@rollup/rollup-linux-arm64-musl": "4.45.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.45.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.45.0", - "@rollup/rollup-linux-riscv64-gnu": "4.45.0", - "@rollup/rollup-linux-riscv64-musl": "4.45.0", - "@rollup/rollup-linux-s390x-gnu": "4.45.0", - "@rollup/rollup-linux-x64-gnu": "4.45.0", - "@rollup/rollup-linux-x64-musl": "4.45.0", - "@rollup/rollup-win32-arm64-msvc": "4.45.0", - "@rollup/rollup-win32-ia32-msvc": "4.45.0", - "@rollup/rollup-win32-x64-msvc": "4.45.0", + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", "fsevents": "~2.3.2" } }, @@ -7978,6 +14293,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -8004,6 +14329,23 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -8022,6 +14364,14 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -8044,6 +14394,23 @@ "node": ">= 18" } }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -8094,6 +14461,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -8166,6 +14546,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -8211,6 +14598,99 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -8221,6 +14701,16 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8230,6 +14720,53 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/speakeasy": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz", @@ -8242,10 +14779,26 @@ "node": ">= 0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sql.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz", + "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/ssh2": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", - "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.6", @@ -8256,7 +14809,76 @@ }, "optionalDependencies": { "cpu-features": "~0.0.10", - "nan": "^2.20.0" + "nan": "^2.23.0" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" } }, "node_modules/statuses": { @@ -8299,6 +14921,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -8311,6 +14949,30 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8324,12 +14986,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/style-mod": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "license": "MIT" }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8342,6 +15058,283 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg2png": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/svg2png/-/svg2png-4.1.1.tgz", + "integrity": "sha512-9tOp9Ugjlunuf1ugqkhiYboTmTaTI7p48dz5ZjNA5NQJ5xS1NLTZZ1tF8vkJOIBb/ZwxGJsKZvRWqVpo4q9z9Q==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "file-url": "^2.0.0", + "phantomjs-prebuilt": "^2.1.14", + "pn": "^1.0.0", + "yargs": "^6.5.0" + }, + "bin": { + "svg2png": "bin/svg2png-cli.js" + } + }, + "node_modules/svg2png/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/svg2png/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svg2png/node_modules/y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/svg2png/node_modules/yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha512-6/QWTdisjnu5UHUzQGst+UOEuEVwIzFVGBjq3jMTFNs5WJQsH/X6nMURSaScIdF5txylr1Ao9bvbWiKi2yXbwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^4.2.0" + } + }, + "node_modules/svg2png/node_modules/yargs-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha512-+QQWqC2xeL0N5/TE+TY6OGEqyNRM+g2/r712PDNYgiCdXYCApXf1vzfmDSLBxfGRwV+moTq/V8FnMI24JCm2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^3.0.0" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -8353,18 +15346,22 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", - "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -8418,14 +15415,152 @@ "node": ">=6" } }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/timm": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz", + "integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -8435,10 +15570,13 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -8449,9 +15587,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -8460,6 +15598,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8482,12 +15640,87 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -8564,9 +15797,9 @@ } }, "node_modules/tw-animate-css": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz", - "integrity": "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", + "integrity": "sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==", "dev": true, "license": "MIT", "funding": { @@ -8592,6 +15825,20 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -8627,16 +15874,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", - "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.43.0.tgz", + "integrity": "sha512-FyRGJKUGvcFekRRcBKFBlAhnp4Ng8rhe8tuvvkR9OiU0gfd4vyvTRQHEckO6VDlH57jbeUQem2IpqPq9kLJH+w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.40.0", - "@typescript-eslint/parser": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0" + "@typescript-eslint/eslint-plugin": "8.43.0", + "@typescript-eslint/parser": "8.43.0", + "@typescript-eslint/typescript-estree": "8.43.0", + "@typescript-eslint/utils": "8.43.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8656,6 +15903,42 @@ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "license": "MIT" }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8758,12 +16041,39 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/utif": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", + "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.5" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -8771,6 +16081,17 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validator": { "version": "13.15.15", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", @@ -8789,10 +16110,26 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vite": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", - "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -8800,7 +16137,7 @@ "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" @@ -8907,6 +16244,36 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/wait-on": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.4.tgz", + "integrity": "sha512-8f9LugAGo4PSc0aLbpKVCVtzayd36sSCp4WLpVngkYq6PK87H79zt77/tlCU6eKCLqR46iFvcl0PU5f+DmtkwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.11.0", + "joi": "^17.13.3", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -8978,6 +16345,25 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -9005,6 +16391,60 @@ } } }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -9116,6 +16556,17 @@ "node": ">=8" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -9140,9 +16591,9 @@ } }, "node_modules/zod": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.5.tgz", - "integrity": "sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 81530bff..78bd621d 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,24 @@ { "name": "termix", "private": true, - "version": "0.0.0", + "version": "1.6.0", + "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities", + "author": "Karmaa", + "main": "electron/main.cjs", "type": "module", "scripts": { + "clean": "npx prettier . --write", "dev": "vite", - "build": "vite build", + "build": "vite build && tsc -p tsconfig.node.json", "build:backend": "tsc -p tsconfig.node.json", - "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/starter.js", + "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "electron": "electron .", + "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"", + "build:win-portable": "npm run build && electron-builder --win --dir", + "build:win-installer": "npm run build && electron-builder --win --publish=never", + "build:linux-portable": "npm run build && electron-builder --linux --dir" }, "dependencies": { "@hookform/resolvers": "^5.1.1", @@ -73,6 +82,7 @@ "react-hook-form": "^7.60.0", "react-i18next": "^15.7.3", "react-resizable-panels": "^3.0.3", + "react-simple-keyboard": "^3.8.120", "react-xtermjs": "^1.0.10", "sonner": "^2.0.7", "speakeasy": "^2.0.0", @@ -96,14 +106,21 @@ "@types/ws": "^8.18.1", "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", + "concurrently": "^9.2.1", + "electron": "^38.0.0", + "electron-builder": "^26.0.12", + "electron-icon-builder": "^2.0.1", + "electron-packager": "^17.1.2", "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "prettier": "3.6.2", "ts-node": "^10.9.2", "tw-animate-css": "^1.3.5", "typescript": "~5.9.2", "typescript-eslint": "^8.40.0", - "vite": "^7.1.3" + "vite": "^7.1.5", + "wait-on": "^8.0.4" } } diff --git a/public/favicon.ico b/public/favicon.ico index 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 (
- ) + ); } // Commented out mobile behavior to keep sidebar always visible @@ -222,7 +222,7 @@ function Sidebar({ "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset" ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]" - : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)" + : "group-data-[collapsible=icon]:w-(--sidebar-width-icon)", )} />
@@ -249,7 +249,7 @@ function Sidebar({
- ) + ); } function SidebarTrigger({ @@ -257,7 +257,7 @@ function SidebarTrigger({ onClick, ...props }: React.ComponentProps) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return ( - ) + ); } function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar() + const { toggleSidebar } = useSidebar(); return (