diff --git a/.commitlintrc.json b/.commitlintrc.json
new file mode 100644
index 00000000..cba65ea5
--- /dev/null
+++ b/.commitlintrc.json
@@ -0,0 +1,21 @@
+{
+ "extends": ["@commitlint/config-conventional"],
+ "rules": {
+ "type-enum": [
+ 2,
+ "always",
+ [
+ "feat",
+ "fix",
+ "docs",
+ "style",
+ "refactor",
+ "perf",
+ "test",
+ "chore",
+ "revert"
+ ]
+ ],
+ "subject-case": [0]
+ }
+}
diff --git a/.dockerignore b/.dockerignore
index 628b48d3..46be46b6 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,29 +1,24 @@
-# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-# Build outputs
dist
build
.next
.nuxt
-# Development files
.env.local
.env.development.local
.env.test.local
.env.production.local
-# IDE and editor files
.vscode
.idea
*.swp
*.swo
*~
-# OS generated files
.DS_Store
.DS_Store?
._*
@@ -32,98 +27,67 @@ build
ehthumbs.db
Thumbs.db
-# Git
.git
.gitignore
-# Documentation
README.md
README-CN.md
CONTRIBUTING.md
LICENSE
-# Docker files (avoid copying docker files into docker)
-# docker/ - commented out to allow entrypoint.sh to be copied
-
-# Repository images
repo-images/
-# Uploads directory
uploads/
-# Electron files (not needed for Docker)
electron/
electron-builder.json
-# Development and build artifacts
*.log
*.tmp
*.temp
-# Font files (we'll optimize these in Dockerfile)
-# public/fonts/*.ttf
-
-# Logs
logs
*.log
-# Runtime data
pids
*.pid
*.seed
*.pid.lock
-# Coverage directory used by tools like istanbul
coverage
-# nyc test coverage
.nyc_output
-# Dependency directories
jspm_packages/
-# Optional npm cache directory
.npm
-# Optional eslint cache
.eslintcache
-# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
-# Optional REPL history
.node_repl_history
-# Output of 'npm pack'
*.tgz
-# Yarn Integrity file
.yarn-integrity
-# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
-# next.js build output
.next
-# nuxt.js build output
.nuxt
-# vuepress build output
.vuepress/dist
-# Serverless directories
.serverless
-# FuseBox cache
.fusebox/
-# DynamoDB Local files
.dynamodb/
-# TernJS port file
-.tern-port
\ No newline at end of file
+.tern-port
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..e6478bbc
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{js,jsx,ts,tsx,json,css,scss,md,yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..5350c239
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,31 @@
+* text=auto eol=lf
+
+*.js text eol=lf
+*.jsx text eol=lf
+*.ts text eol=lf
+*.tsx text eol=lf
+*.json text eol=lf
+*.css text eol=lf
+*.scss text eol=lf
+*.html text eol=lf
+*.md text eol=lf
+*.yaml text eol=lf
+*.yml text eol=lf
+
+*.sh text eol=lf
+*.bash text eol=lf
+
+*.bat text eol=crlf
+*.cmd text eol=crlf
+*.ps1 text eol=crlf
+
+*.png binary
+*.jpg binary
+*.jpeg binary
+*.gif binary
+*.ico binary
+*.svg binary
+*.woff binary
+*.woff2 binary
+*.ttf binary
+*.eot binary
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 80ab5da6..a3d02ef9 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -6,11 +6,14 @@ on:
version:
description: "Version to build (e.g., 1.8.0)"
required: true
- production:
- description: "Is this a production build?"
+ build_type:
+ description: "Build type"
required: true
- default: false
- type: boolean
+ default: "Development"
+ type: choice
+ options:
+ - Development
+ - Production
jobs:
build:
@@ -33,29 +36,25 @@ jobs:
id: tags
run: |
VERSION=${{ github.event.inputs.version }}
- PROD=${{ github.event.inputs.production }}
+ BUILD_TYPE=${{ github.event.inputs.build_type }}
TAGS=()
ALL_TAGS=()
- if [ "$PROD" = "true" ]; then
- # Production build → push release + latest to both GHCR and Docker Hub
+ if [ "$BUILD_TYPE" = "Production" ]; then
TAGS+=("release-$VERSION" "latest")
for tag in "${TAGS[@]}"; do
ALL_TAGS+=("ghcr.io/lukegus/termix:$tag")
ALL_TAGS+=("docker.io/bugattiguy527/termix:$tag")
done
else
- # Dev build → push only dev-x.x.x to GHCR
TAGS+=("dev-$VERSION")
for tag in "${TAGS[@]}"; do
ALL_TAGS+=("ghcr.io/lukegus/termix:$tag")
done
fi
- echo "ALL_TAGS=${ALL_TAGS[*]}" >> $GITHUB_ENV
- echo "All tags to build:"
- printf '%s\n' "${ALL_TAGS[@]}"
+ echo "ALL_TAGS=$(printf '%s\n' "${ALL_TAGS[@]}")" >> $GITHUB_ENV
- name: Login to GHCR
uses: docker/login-action@v3
@@ -65,7 +64,7 @@ jobs:
password: ${{ secrets.GHCR_TOKEN }}
- name: Login to Docker Hub (prod only)
- if: ${{ github.event.inputs.production == 'true' }}
+ if: ${{ github.event.inputs.build_type == 'Production' }}
uses: docker/login-action@v3
with:
username: bugattiguy527
diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml
index b50a8905..62bf5769 100644
--- a/.github/workflows/electron.yml
+++ b/.github/workflows/electron.yml
@@ -28,6 +28,8 @@ jobs:
build-windows:
runs-on: windows-latest
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'windows' || github.event.inputs.build_type == ''
+ permissions:
+ contents: write
steps:
- name: Checkout repository
@@ -43,7 +45,6 @@ jobs:
- name: Install dependencies
run: |
- # Retry npm ci up to 3 times on failure
$maxAttempts = 3
$attempt = 1
while ($attempt -le $maxAttempts) {
@@ -55,7 +56,6 @@ jobs:
Write-Error "npm ci failed after $maxAttempts attempts"
exit 1
}
- Write-Host "npm ci attempt $attempt failed, retrying in 10 seconds..."
Start-Sleep -Seconds 10
$attempt++
}
@@ -66,79 +66,79 @@ jobs:
run: |
$VERSION = (Get-Content package.json | ConvertFrom-Json).version
echo "version=$VERSION" >> $env:GITHUB_OUTPUT
- echo "Building version: $VERSION"
- name: Build Windows (All Architectures)
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm run build && npx electron-builder --win --x64 --ia32
- name: List release files
run: |
- echo "Contents of release directory:"
dir release
- name: Upload Windows x64 NSIS Installer
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_x64_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_windows_x64_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_x64_nsis
- path: release/*_x64_*_nsis.exe
+ path: release/termix_windows_x64_nsis.exe
retention-days: 30
- name: Upload Windows ia32 NSIS Installer
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_ia32_*_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_windows_ia32_nsis.exe') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_ia32_nsis
- path: release/*_ia32_*_nsis.exe
+ path: release/termix_windows_ia32_nsis.exe
retention-days: 30
- name: Upload Windows x64 MSI Installer
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_x64_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_windows_x64_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_x64_msi
- path: release/*_x64_*_msi.msi
+ path: release/termix_windows_x64_msi.msi
retention-days: 30
- name: Upload Windows ia32 MSI Installer
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_ia32_*_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_windows_ia32_msi.msi') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_ia32_msi
- path: release/*_ia32_*_msi.msi
+ path: release/termix_windows_ia32_msi.msi
retention-days: 30
- name: Create Windows x64 Portable zip
if: hashFiles('release/win-unpacked/*') != ''
run: |
- $VERSION = "${{ steps.package-version.outputs.version }}"
- Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_${VERSION}_portable.zip"
+ Compress-Archive -Path "release\win-unpacked\*" -DestinationPath "termix_windows_x64_portable.zip"
- name: Create Windows ia32 Portable zip
if: hashFiles('release/win-ia32-unpacked/*') != ''
run: |
- $VERSION = "${{ steps.package-version.outputs.version }}"
- Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_${VERSION}_portable.zip"
+ Compress-Archive -Path "release\win-ia32-unpacked\*" -DestinationPath "termix_windows_ia32_portable.zip"
- name: Upload Windows x64 Portable
uses: actions/upload-artifact@v4
- if: hashFiles('termix_windows_x64_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('termix_windows_x64_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_x64_portable
- path: termix_windows_x64_*_portable.zip
+ path: termix_windows_x64_portable.zip
retention-days: 30
- name: Upload Windows ia32 Portable
uses: actions/upload-artifact@v4
- if: hashFiles('termix_windows_ia32_*_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('termix_windows_ia32_portable.zip') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_windows_ia32_portable
- path: termix_windows_ia32_*_portable.zip
+ path: termix_windows_ia32_portable.zip
retention-days: 30
build-linux:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.inputs.build_type == 'all' || github.event.inputs.build_type == 'linux' || github.event.inputs.build_type == ''
+ permissions:
+ contents: write
steps:
- name: Checkout repository
@@ -152,19 +152,21 @@ jobs:
node-version: "20"
cache: "npm"
+ - name: Install system dependencies for AppImage
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libfuse2
+
- name: Install dependencies
run: |
- rm -f package-lock.json
- # Retry npm install up to 3 times on failure
- for i in 1 2 3; do
- if npm install; then
+ for i in 1 2 3;
+ do
+ if npm ci; then
break
else
if [ $i -eq 3 ]; then
- echo "npm install failed after 3 attempts"
exit 1
fi
- echo "npm install attempt $i failed, retrying in 10 seconds..."
sleep 10
fi
done
@@ -172,107 +174,120 @@ jobs:
npm install --force @rollup/rollup-linux-arm64-gnu
npm install --force @rollup/rollup-linux-arm-gnueabihf
- - name: Build Linux (All Architectures)
- run: npm run build && npx electron-builder --linux --x64 --arm64 --armv7l
+ - name: Build Linux x64
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ DEBUG: electron-builder
+ run: npm run build && npx electron-builder --linux --x64
- - name: Rename tar.gz files to match convention
+ - name: Build Linux arm64 and armv7l
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: npx electron-builder --linux --arm64 --armv7l
+
+ - name: Rename Linux artifacts for consistency
run: |
- VERSION=$(node -p "require('./package.json').version")
cd release
- # Rename x64 tar.gz if it exists
- if [ -f "termix-${VERSION}-x64.tar.gz" ]; then
- mv "termix-${VERSION}-x64.tar.gz" "termix_linux_x64_${VERSION}_portable.tar.gz"
- echo "Renamed x64 tar.gz"
+ if [ -f "termix_linux_amd64_deb.deb" ]; then
+ mv "termix_linux_amd64_deb.deb" "termix_linux_x64_deb.deb"
fi
- # Rename arm64 tar.gz if it exists
- if [ -f "termix-${VERSION}-arm64.tar.gz" ]; then
- mv "termix-${VERSION}-arm64.tar.gz" "termix_linux_arm64_${VERSION}_portable.tar.gz"
- echo "Renamed arm64 tar.gz"
- fi
-
- # Rename armv7l tar.gz if it exists
- if [ -f "termix-${VERSION}-armv7l.tar.gz" ]; then
- mv "termix-${VERSION}-armv7l.tar.gz" "termix_linux_armv7l_${VERSION}_portable.tar.gz"
- echo "Renamed armv7l tar.gz"
+ if [ -f "termix_linux_x86_64_appimage.AppImage" ]; then
+ mv "termix_linux_x86_64_appimage.AppImage" "termix_linux_x64_appimage.AppImage"
fi
cd ..
- name: List release files
run: |
- echo "Contents of release directory:"
ls -la release/
+ - name: Debug electron-builder output
+ if: always()
+ run: |
+ if [ -f "release/builder-debug.yml" ]; then
+ cat release/builder-debug.yml
+ fi
+
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_x64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_x64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_x64_appimage
- path: release/*_x64_*_appimage.AppImage
+ path: release/termix_linux_x64_appimage.AppImage
retention-days: 30
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_arm64_*_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_arm64_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_arm64_appimage
- path: release/*_arm64_*_appimage.AppImage
+ path: release/termix_linux_arm64_appimage.AppImage
+ retention-days: 30
+
+ - name: Upload Linux armv7l AppImage
+ uses: actions/upload-artifact@v4
+ if: hashFiles('release/termix_linux_armv7l_appimage.AppImage') != '' && github.event.inputs.artifact_destination != 'none'
+ with:
+ name: termix_linux_armv7l_appimage
+ path: release/termix_linux_armv7l_appimage.AppImage
retention-days: 30
- name: Upload Linux x64 DEB
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_x64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_x64_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_x64_deb
- path: release/*_x64_*_deb.deb
+ path: release/termix_linux_x64_deb.deb
retention-days: 30
- name: Upload Linux arm64 DEB
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_arm64_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_arm64_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_arm64_deb
- path: release/*_arm64_*_deb.deb
+ path: release/termix_linux_arm64_deb.deb
retention-days: 30
- name: Upload Linux armv7l DEB
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_armv7l_*_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_armv7l_deb.deb') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_armv7l_deb
- path: release/*_armv7l_*_deb.deb
+ path: release/termix_linux_armv7l_deb.deb
retention-days: 30
- name: Upload Linux x64 tar.gz
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_x64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_x64_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_x64_portable
- path: release/*_x64_*_portable.tar.gz
+ path: release/termix_linux_x64_portable.tar.gz
retention-days: 30
- name: Upload Linux arm64 tar.gz
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_arm64_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_arm64_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_arm64_portable
- path: release/*_arm64_*_portable.tar.gz
+ path: release/termix_linux_arm64_portable.tar.gz
retention-days: 30
- name: Upload Linux armv7l tar.gz
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_armv7l_*_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_linux_armv7l_portable.tar.gz') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_linux_armv7l_portable
- path: release/*_armv7l_*_portable.tar.gz
+ path: release/termix_linux_armv7l_portable.tar.gz
retention-days: 30
build-macos:
runs-on: macos-latest
if: github.event.inputs.build_type == 'macos' || github.event.inputs.build_type == 'all'
needs: []
+ permissions:
+ contents: write
steps:
- name: Checkout repository
@@ -288,16 +303,14 @@ jobs:
- name: Install dependencies
run: |
- # Retry npm ci up to 3 times on failure
- for i in 1 2 3; do
+ for i in 1 2 3;
+ do
if npm ci; then
break
else
if [ $i -eq 3 ]; then
- echo "npm ci failed after 3 attempts"
exit 1
fi
- echo "npm ci attempt $i failed, retrying in 10 seconds..."
sleep 10
fi
done
@@ -309,9 +322,6 @@ jobs:
run: |
if [ -n "${{ secrets.MAC_BUILD_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.MAC_P12_PASSWORD }}" ]; then
echo "has_certs=true" >> $GITHUB_OUTPUT
- else
- echo "has_certs=false" >> $GITHUB_OUTPUT
- echo "⚠️ Code signing certificates not configured. MAS build will be unsigned."
fi
- name: Import Code Signing Certificates
@@ -326,69 +336,47 @@ jobs:
INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
- # Decode certificates
echo -n "$MAC_BUILD_CERTIFICATE_BASE64" | base64 --decode -o $APP_CERT_PATH
if [ -n "$MAC_INSTALLER_CERTIFICATE_BASE64" ]; then
- echo "Decoding installer certificate..."
echo -n "$MAC_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $INSTALLER_CERT_PATH
- else
- echo "⚠️ MAC_INSTALLER_CERTIFICATE_BASE64 is empty"
fi
- # Create and configure keychain
security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
- # Import application certificate
- echo "Importing application certificate..."
security import $APP_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
- # Import installer certificate if it exists
if [ -f "$INSTALLER_CERT_PATH" ]; then
- echo "Importing installer certificate..."
security import $INSTALLER_CERT_PATH -P "$MAC_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
- else
- echo "⚠️ Installer certificate file not found, skipping import"
fi
security list-keychain -d user -s $KEYCHAIN_PATH
- echo "Imported certificates:"
security find-identity -v -p codesigning $KEYCHAIN_PATH
- name: Build macOS App Store Package
if: steps.check_certs.outputs.has_certs == 'true'
env:
ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES: true
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
- # Get current version for display
CURRENT_VERSION=$(node -p "require('./package.json').version")
BUILD_VERSION="${{ github.run_number }}"
- echo "✅ Package version: $CURRENT_VERSION (unchanged)"
- echo "✅ Build number for Apple: $BUILD_VERSION"
-
- # Build MAS with custom buildVersion
npm run build && npx electron-builder --mac mas --universal --config.buildVersion="$BUILD_VERSION"
- name: Clean up MAS keychain before DMG build
if: steps.check_certs.outputs.has_certs == 'true'
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
- echo "Cleaned up MAS keychain"
- name: Check for Developer ID Certificates
id: check_dev_id_certs
run: |
if [ -n "${{ secrets.DEVELOPER_ID_CERTIFICATE_BASE64 }}" ] && [ -n "${{ secrets.DEVELOPER_ID_P12_PASSWORD }}" ]; then
echo "has_dev_id_certs=true" >> $GITHUB_OUTPUT
- echo "✅ Developer ID certificates configured for DMG signing"
- else
- echo "has_dev_id_certs=false" >> $GITHUB_OUTPUT
- echo "⚠️ Developer ID certificates not configured. DMG will be unsigned."
- echo "Add DEVELOPER_ID_CERTIFICATE_BASE64 and DEVELOPER_ID_P12_PASSWORD secrets to enable DMG signing."
fi
- name: Import Developer ID Certificates
@@ -403,34 +391,24 @@ jobs:
DEV_INSTALLER_CERT_PATH=$RUNNER_TEMP/dev_installer_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/dev-signing.keychain-db
- # Decode Developer ID certificate
echo -n "$DEVELOPER_ID_CERTIFICATE_BASE64" | base64 --decode -o $DEV_CERT_PATH
if [ -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" ]; then
- echo "Decoding Developer ID installer certificate..."
echo -n "$DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64" | base64 --decode -o $DEV_INSTALLER_CERT_PATH
- else
- echo "⚠️ DEVELOPER_ID_INSTALLER_CERTIFICATE_BASE64 is empty (optional)"
fi
- # Create and configure keychain
security create-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$MAC_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
- # Import Developer ID Application certificate
- echo "Importing Developer ID Application certificate..."
security import $DEV_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
- # Import Developer ID Installer certificate if it exists
if [ -f "$DEV_INSTALLER_CERT_PATH" ]; then
- echo "Importing Developer ID Installer certificate..."
security import $DEV_INSTALLER_CERT_PATH -P "$DEVELOPER_ID_P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
fi
security list-keychain -d user -s $KEYCHAIN_PATH
- echo "Imported Developer ID certificates:"
security find-identity -v -p codesigning $KEYCHAIN_PATH
- name: Build macOS DMG
@@ -441,52 +419,48 @@ jobs:
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
- # Build DMG without running npm run build again (already built above or skip if no certs)
- if [ "${{ steps.check_certs.outputs.has_certs }}" == "true" ]; then
- # Frontend already built, just package DMG
- npx electron-builder --mac dmg --universal --x64 --arm64
- else
- # No certs, need to build frontend first
- npm run build && npx electron-builder --mac dmg --universal --x64 --arm64
+ if [ "${{ steps.check_certs.outputs.has_certs }}" != "true" ]; then
+ npm run build
fi
+ export GH_TOKEN="${{ secrets.GITHUB_TOKEN }}"
+ npx electron-builder --mac dmg --universal --x64 --arm64 --publish never
- name: List release directory
if: steps.check_certs.outputs.has_certs == 'true'
run: |
- echo "Contents of release directory:"
ls -R release/ || echo "Release directory not found"
- name: Upload macOS MAS PKG
- if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/*_*_*_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit')
+ if: steps.check_certs.outputs.has_certs == 'true' && hashFiles('release/termix_macos_universal_mas.pkg') != '' && (github.event.inputs.artifact_destination == 'file' || github.event.inputs.artifact_destination == 'release' || github.event.inputs.artifact_destination == 'submit')
uses: actions/upload-artifact@v4
with:
- name: termix_macos_mas
- path: release/*_*_*_mas.pkg
+ name: termix_macos_universal_mas
+ path: release/termix_macos_universal_mas.pkg
retention-days: 30
if-no-files-found: warn
- name: Upload macOS Universal DMG
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_universal_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_macos_universal_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_macos_universal_dmg
- path: release/*_universal_*_dmg.dmg
+ path: release/termix_macos_universal_dmg.dmg
retention-days: 30
- name: Upload macOS x64 DMG
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_x64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_macos_x64_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_macos_x64_dmg
- path: release/*_x64_*_dmg.dmg
+ path: release/termix_macos_x64_dmg.dmg
retention-days: 30
- name: Upload macOS arm64 DMG
uses: actions/upload-artifact@v4
- if: hashFiles('release/*_arm64_*_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
+ if: hashFiles('release/termix_macos_arm64_dmg.dmg') != '' && github.event.inputs.artifact_destination != 'none'
with:
name: termix_macos_arm64_dmg
- path: release/*_arm64_*_dmg.dmg
+ path: release/termix_macos_arm64_dmg.dmg
retention-days: 30
- name: Check for App Store Connect API credentials
@@ -495,51 +469,35 @@ jobs:
run: |
if [ -n "${{ secrets.APPLE_KEY_ID }}" ] && [ -n "${{ secrets.APPLE_ISSUER_ID }}" ] && [ -n "${{ secrets.APPLE_KEY_CONTENT }}" ]; then
echo "has_credentials=true" >> $GITHUB_OUTPUT
- if [ "${{ github.event.inputs.artifact_destination }}" == "submit" ]; then
- echo "✅ App Store Connect API credentials found. Will deploy to TestFlight."
- else
- echo "ℹ️ App Store Connect API credentials found, but store submission is disabled."
- fi
- else
- echo "has_credentials=false" >> $GITHUB_OUTPUT
- echo "⚠️ App Store Connect API credentials not configured. Skipping deployment."
- echo "Add APPLE_KEY_ID, APPLE_ISSUER_ID, and APPLE_KEY_CONTENT secrets to enable automatic deployment."
fi
- name: Setup Ruby for Fastlane
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit'
uses: ruby/setup-ruby@v1
with:
- ruby-version: '3.2'
+ ruby-version: "3.2"
bundler-cache: false
- name: Install Fastlane
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit'
run: |
gem install fastlane -N
- fastlane --version
- name: Deploy to App Store Connect (TestFlight)
if: steps.check_asc_creds.outputs.has_credentials == 'true' && github.event.inputs.artifact_destination == 'submit'
run: |
PKG_FILE=$(find release -name "*.pkg" -type f | head -n 1)
if [ -z "$PKG_FILE" ]; then
- echo "Error: No .pkg file found in release directory"
exit 1
fi
- echo "Found package: $PKG_FILE"
- # Create API key file
mkdir -p ~/private_keys
echo "${{ secrets.APPLE_KEY_CONTENT }}" | base64 --decode > ~/private_keys/AuthKey_${{ secrets.APPLE_KEY_ID }}.p8
- # Upload to App Store Connect using xcrun altool
xcrun altool --upload-app -f "$PKG_FILE" \
--type macos \
--apiKey "${{ secrets.APPLE_KEY_ID }}" \
--apiIssuer "${{ secrets.APPLE_ISSUER_ID }}"
-
- echo "✅ Upload complete! Build will appear in App Store Connect after processing (10-30 minutes)"
continue-on-error: true
- name: Clean up keychains
@@ -566,7 +524,6 @@ jobs:
run: |
$VERSION = (Get-Content package.json | ConvertFrom-Json).version
echo "version=$VERSION" >> $env:GITHUB_OUTPUT
- echo "Building Chocolatey package for version: $VERSION"
- name: Download Windows x64 MSI artifact
uses: actions/download-artifact@v4
@@ -584,8 +541,6 @@ jobs:
echo "msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT
echo "checksum=$CHECKSUM" >> $env:GITHUB_OUTPUT
- echo "MSI File: $MSI_NAME"
- echo "SHA256: $CHECKSUM"
- name: Prepare Chocolatey package
run: |
@@ -593,33 +548,20 @@ jobs:
$CHECKSUM = "${{ steps.msi-info.outputs.checksum }}"
$MSI_NAME = "${{ steps.msi-info.outputs.msi_name }}"
- # Construct the download URL with the actual release tag format
$DOWNLOAD_URL = "https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$MSI_NAME"
- # Copy chocolatey files to build directory
New-Item -ItemType Directory -Force -Path "choco-build"
Copy-Item -Path "chocolatey\*" -Destination "choco-build" -Recurse -Force
- # Update chocolateyinstall.ps1 with actual values
$installScript = Get-Content "choco-build\tools\chocolateyinstall.ps1" -Raw -Encoding UTF8
$installScript = $installScript -replace 'DOWNLOAD_URL_PLACEHOLDER', $DOWNLOAD_URL
$installScript = $installScript -replace 'CHECKSUM_PLACEHOLDER', $CHECKSUM
[System.IO.File]::WriteAllText("$PWD\choco-build\tools\chocolateyinstall.ps1", $installScript, [System.Text.UTF8Encoding]::new($false))
- # Update nuspec with version (preserve UTF-8 encoding without BOM)
$nuspec = Get-Content "choco-build\termix-ssh.nuspec" -Raw -Encoding UTF8
$nuspec = $nuspec -replace 'VERSION_PLACEHOLDER', $VERSION
[System.IO.File]::WriteAllText("$PWD\choco-build\termix-ssh.nuspec", $nuspec, [System.Text.UTF8Encoding]::new($false))
- echo "Chocolatey package prepared for version $VERSION"
- echo "Download URL: $DOWNLOAD_URL"
-
- # Verify the nuspec is valid
- echo ""
- echo "Verifying nuspec content:"
- Get-Content "choco-build\termix-ssh.nuspec" -Head 10
- echo ""
-
- name: Install Chocolatey
run: |
Set-ExecutionPolicy Bypass -Scope Process -Force
@@ -629,29 +571,17 @@ jobs:
- name: Pack Chocolatey package
run: |
cd choco-build
- echo "Packing Chocolatey package..."
choco pack termix-ssh.nuspec
if ($LASTEXITCODE -ne 0) {
- echo "❌ Failed to pack Chocolatey package"
- exit 1
+ throw "Chocolatey push failed with exit code $LASTEXITCODE"
}
- echo ""
- echo "✅ Package created successfully"
- echo "Package contents:"
- Get-ChildItem *.nupkg | ForEach-Object { echo $_.Name }
-
- name: Check for Chocolatey API Key
id: check_choco_key
run: |
if ("${{ secrets.CHOCOLATEY_API_KEY }}" -ne "") {
echo "has_key=true" >> $env:GITHUB_OUTPUT
- echo "✅ Chocolatey API key found. Will push to Chocolatey."
- } else {
- echo "has_key=false" >> $env:GITHUB_OUTPUT
- echo "⚠️ Chocolatey API key not configured. Package will be created but not pushed."
- echo "Add CHOCOLATEY_API_KEY secret to enable automatic submission."
}
- name: Push to Chocolatey
@@ -664,29 +594,10 @@ jobs:
try {
choco push "termix-ssh.$VERSION.nupkg" --source https://push.chocolatey.org/
if ($LASTEXITCODE -eq 0) {
- echo ""
- echo "✅ Package pushed to Chocolatey successfully!"
- echo "View at: https://community.chocolatey.org/packages/termix-ssh/$VERSION"
} else {
throw "Chocolatey push failed with exit code $LASTEXITCODE"
}
} catch {
- echo ""
- echo "❌ Failed to push to Chocolatey"
- echo ""
- echo "Common reasons:"
- echo "1. Package ID 'termix-ssh' is already owned by another user"
- echo "2. You need to register/claim the package ID first"
- echo "3. API key doesn't have push permissions"
- echo ""
- echo "Solutions:"
- echo "1. Check if package exists: https://community.chocolatey.org/packages/termix-ssh"
- echo "2. If it exists and is yours, contact Chocolatey support to claim it"
- echo "3. Register a new package ID at: https://community.chocolatey.org/"
- echo ""
- echo "The package artifact has been saved for manual submission."
- echo ""
- exit 1
}
- name: Upload Chocolatey package as artifact
@@ -716,7 +627,6 @@ jobs:
RELEASE_DATE=$(date +%Y-%m-%d)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release_date=$RELEASE_DATE" >> $GITHUB_OUTPUT
- echo "Building Flatpak submission for version: $VERSION"
- name: Download Linux x64 AppImage artifact
uses: actions/download-artifact@v4
@@ -735,12 +645,10 @@ jobs:
run: |
VERSION="${{ steps.package-version.outputs.version }}"
- # x64 AppImage
APPIMAGE_X64_FILE=$(find artifact-x64 -name "*.AppImage" -type f | head -n 1)
APPIMAGE_X64_NAME=$(basename "$APPIMAGE_X64_FILE")
CHECKSUM_X64=$(sha256sum "$APPIMAGE_X64_FILE" | awk '{print $1}')
- # arm64 AppImage
APPIMAGE_ARM64_FILE=$(find artifact-arm64 -name "*.AppImage" -type f | head -n 1)
APPIMAGE_ARM64_NAME=$(basename "$APPIMAGE_ARM64_FILE")
CHECKSUM_ARM64=$(sha256sum "$APPIMAGE_ARM64_FILE" | awk '{print $1}')
@@ -750,11 +658,6 @@ jobs:
echo "appimage_arm64_name=$APPIMAGE_ARM64_NAME" >> $GITHUB_OUTPUT
echo "checksum_arm64=$CHECKSUM_ARM64" >> $GITHUB_OUTPUT
- echo "x64 AppImage: $APPIMAGE_X64_NAME"
- echo "x64 SHA256: $CHECKSUM_X64"
- echo "arm64 AppImage: $APPIMAGE_ARM64_NAME"
- echo "arm64 SHA256: $CHECKSUM_ARM64"
-
- name: Install ImageMagick for icon generation
run: |
sudo apt-get update
@@ -769,101 +672,26 @@ jobs:
APPIMAGE_X64_NAME="${{ steps.appimage-info.outputs.appimage_x64_name }}"
APPIMAGE_ARM64_NAME="${{ steps.appimage-info.outputs.appimage_arm64_name }}"
- # Create submission directory
mkdir -p flatpak-submission
- # Copy Flatpak files to submission directory
cp flatpak/com.karmaa.termix.yml flatpak-submission/
cp flatpak/com.karmaa.termix.desktop flatpak-submission/
cp flatpak/com.karmaa.termix.metainfo.xml flatpak-submission/
cp flatpak/flathub.json flatpak-submission/
- # Copy and prepare icons
cp public/icon.svg flatpak-submission/com.karmaa.termix.svg
convert public/icon.png -resize 256x256 flatpak-submission/icon-256.png
convert public/icon.png -resize 128x128 flatpak-submission/icon-128.png
- # Update manifest with version and checksums
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.yml
sed -i "s/CHECKSUM_X64_PLACEHOLDER/$CHECKSUM_X64/g" flatpak-submission/com.karmaa.termix.yml
sed -i "s/CHECKSUM_ARM64_PLACEHOLDER/$CHECKSUM_ARM64/g" flatpak-submission/com.karmaa.termix.yml
- # Update metainfo with version and date
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak-submission/com.karmaa.termix.metainfo.xml
sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak-submission/com.karmaa.termix.metainfo.xml
- echo "✅ Flatpak submission files prepared for version $VERSION"
- echo "x64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_X64_NAME"
- echo "arm64 Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$APPIMAGE_ARM64_NAME"
-
- - name: Create submission instructions
- run: |
- cat > flatpak-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF'
- # Flathub Submission Instructions for Termix
-
- ## Automatic Submission (Recommended)
-
- All files needed for Flathub submission are in this artifact. Follow these steps:
-
- 1. **Fork the Flathub repository**:
- - Go to https://github.com/flathub/flathub
- - Click "Fork" button
-
- 2. **Clone your fork**:
- ```bash
- git clone https://github.com/YOUR-USERNAME/flathub.git
- cd flathub
- git checkout -b com.karmaa.termix
- ```
-
- 3. **Copy all files from this artifact** to the root of your flathub fork
-
- 4. **Commit and push**:
- ```bash
- git add .
- git commit -m "Add Termix ${{ steps.package-version.outputs.version }}"
- git push origin com.karmaa.termix
- ```
-
- 5. **Create Pull Request**:
- - Go to https://github.com/YOUR-USERNAME/flathub
- - Click "Compare & pull request"
- - Submit PR to flathub/flathub
-
- ## Files in this submission:
-
- - `com.karmaa.termix.yml` - Flatpak manifest
- - `com.karmaa.termix.desktop` - Desktop entry
- - `com.karmaa.termix.metainfo.xml` - AppStream metadata
- - `flathub.json` - Flathub configuration
- - `com.karmaa.termix.svg` - SVG icon
- - `icon-256.png` - 256x256 icon
- - `icon-128.png` - 128x128 icon
-
- ## Version Information:
-
- - Version: ${{ steps.package-version.outputs.version }}
- - Release Date: ${{ steps.package-version.outputs.release_date }}
- - x64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_x64 }}
- - arm64 AppImage SHA256: ${{ steps.appimage-info.outputs.checksum_arm64 }}
-
- ## After Submission:
-
- 1. Flathub maintainers will review your submission (usually 1-5 days)
- 2. They may request changes - be responsive to feedback
- 3. Once approved, Termix will be available via: `flatpak install flathub com.karmaa.termix`
-
- ## Resources:
-
- - [Flathub Submission Guidelines](https://docs.flathub.org/docs/for-app-authors/submission)
- - [Flatpak Documentation](https://docs.flatpak.org/)
- EOF
-
- echo "✅ Created submission instructions"
-
- name: List submission files
run: |
- echo "Flatpak submission files:"
ls -la flatpak-submission/
- name: Upload Flatpak submission as artifact
@@ -873,19 +701,6 @@ jobs:
path: flatpak-submission/*
retention-days: 30
- - name: Display next steps
- run: |
- echo ""
- echo "🎉 Flatpak submission files ready!"
- echo ""
- echo "📦 Download the 'flatpak-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md"
- echo ""
- echo "Quick summary:"
- echo "1. Fork https://github.com/flathub/flathub"
- echo "2. Copy artifact files to your fork"
- echo "3. Create PR to flathub/flathub"
- echo ""
-
submit-to-homebrew:
runs-on: macos-latest
if: github.event.inputs.artifact_destination == 'submit'
@@ -904,7 +719,6 @@ jobs:
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- echo "Building Homebrew Cask for version: $VERSION"
- name: Download macOS Universal DMG artifact
uses: actions/download-artifact@v4
@@ -922,8 +736,6 @@ jobs:
echo "dmg_name=$DMG_NAME" >> $GITHUB_OUTPUT
echo "checksum=$CHECKSUM" >> $GITHUB_OUTPUT
- echo "DMG File: $DMG_NAME"
- echo "SHA256: $CHECKSUM"
- name: Prepare Homebrew submission files
run: |
@@ -931,155 +743,24 @@ jobs:
CHECKSUM="${{ steps.dmg-info.outputs.checksum }}"
DMG_NAME="${{ steps.dmg-info.outputs.dmg_name }}"
- # Create submission directory
mkdir -p homebrew-submission/Casks/t
- # Copy Homebrew cask file
cp homebrew/termix.rb homebrew-submission/Casks/t/termix.rb
cp homebrew/README.md homebrew-submission/
- # Update cask with version and checksum
sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" homebrew-submission/Casks/t/termix.rb
sed -i '' "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" homebrew-submission/Casks/t/termix.rb
- echo "✅ Homebrew Cask prepared for version $VERSION"
- echo "Download URL: https://github.com/Termix-SSH/Termix/releases/download/release-$VERSION-tag/$DMG_NAME"
-
- name: Verify Cask syntax
run: |
- # Install Homebrew if not present (should be on macos-latest)
if ! command -v brew &> /dev/null; then
- echo "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
- # Basic syntax check
ruby -c homebrew-submission/Casks/t/termix.rb
- echo "✅ Cask syntax is valid"
-
- - name: Create submission instructions
- run: |
- cat > homebrew-submission/SUBMISSION_INSTRUCTIONS.md << 'EOF'
- # Homebrew Cask Submission Instructions for Termix
-
- ## Option 1: Submit to Official Homebrew Cask (Recommended)
-
- ### Prerequisites
- - macOS with Homebrew installed
- - GitHub account
-
- ### Steps
-
- 1. **Fork the Homebrew Cask repository**:
- - Go to https://github.com/Homebrew/homebrew-cask
- - Click "Fork" button
-
- 2. **Clone your fork**:
- ```bash
- git clone https://github.com/YOUR-USERNAME/homebrew-cask.git
- cd homebrew-cask
- git checkout -b termix
- ```
-
- 3. **Copy the cask file**:
- - Copy `Casks/t/termix.rb` from this artifact to your fork at `Casks/t/termix.rb`
- - Note: Casks are organized by first letter in subdirectories
-
- 4. **Test the cask locally**:
- ```bash
- brew install --cask ./Casks/t/termix.rb
- brew uninstall --cask termix
- ```
-
- 5. **Run audit checks**:
- ```bash
- brew audit --cask --online ./Casks/t/termix.rb
- brew style ./Casks/t/termix.rb
- ```
-
- 6. **Commit and push**:
- ```bash
- git add Casks/t/termix.rb
- git commit -m "Add Termix ${{ steps.package-version.outputs.version }}"
- git push origin termix
- ```
-
- 7. **Create Pull Request**:
- - Go to https://github.com/YOUR-USERNAME/homebrew-cask
- - Click "Compare & pull request"
- - Fill in the PR template
- - Submit to Homebrew/homebrew-cask
-
- ### PR Requirements
-
- Your PR should include:
- - Clear commit message: "Add Termix X.Y.Z" or "Update Termix to X.Y.Z"
- - All audit checks passing
- - Working download URL
- - Valid SHA256 checksum
-
- ## Option 2: Create Your Own Tap (Alternative)
-
- If you want more control and faster updates:
-
- 1. **Create a tap repository**:
- - Create repo: `Termix-SSH/homebrew-termix`
- - Add `Casks/termix.rb` to the repo
-
- 2. **Users install with**:
- ```bash
- brew tap termix-ssh/termix
- brew install --cask termix
- ```
-
- ### Advantages of Custom Tap
- - No approval process
- - Instant updates
- - Full control
- - Can include beta versions
-
- ### Disadvantages
- - Less discoverable
- - Users must add tap first
- - You maintain it yourself
-
- ## Files in this submission:
-
- - `Casks/t/termix.rb` - Homebrew Cask formula
- - `README.md` - Detailed documentation
- - `SUBMISSION_INSTRUCTIONS.md` - This file
-
- ## Version Information:
-
- - Version: ${{ steps.package-version.outputs.version }}
- - DMG SHA256: ${{ steps.dmg-info.outputs.checksum }}
- - DMG URL: https://github.com/Termix-SSH/Termix/releases/download/release-${{ steps.package-version.outputs.version }}-tag/${{ steps.dmg-info.outputs.dmg_name }}
-
- ## After Submission:
-
- ### Official Homebrew Cask:
- 1. Maintainers will review (usually 24-48 hours)
- 2. May request changes or fixes
- 3. Once merged, users can install with: `brew install --cask termix`
- 4. Homebrew bot will auto-update for future releases
-
- ### Custom Tap:
- 1. Push to your tap repository
- 2. Immediately available to users
- 3. Update the cask file for each new release
-
- ## Resources:
-
- - [Homebrew Cask Documentation](https://docs.brew.sh/Cask-Cookbook)
- - [Acceptable Casks](https://docs.brew.sh/Acceptable-Casks)
- - [How to Open a PR](https://docs.brew.sh/How-To-Open-a-Homebrew-Pull-Request)
- EOF
-
- echo "✅ Created submission instructions"
- name: List submission files
run: |
- echo "Homebrew submission files:"
find homebrew-submission -type f
- name: Upload Homebrew submission as artifact
@@ -1089,18 +770,6 @@ jobs:
path: homebrew-submission/*
retention-days: 30
- - name: Display next steps
- run: |
- echo ""
- echo "🍺 Homebrew Cask ready!"
- echo ""
- echo "📦 Download the 'homebrew-submission' artifact and follow SUBMISSION_INSTRUCTIONS.md"
- echo ""
- echo "Quick summary:"
- echo "Option 1 (Recommended): Fork https://github.com/Homebrew/homebrew-cask and submit PR"
- echo "Option 2 (Alternative): Create your own tap at Termix-SSH/homebrew-termix"
- echo ""
-
upload-to-release:
runs-on: blacksmith-4vcpu-ubuntu-2404
if: github.event.inputs.artifact_destination == 'release'
@@ -1114,52 +783,29 @@ jobs:
with:
path: artifacts
- - name: Get latest release
+ - name: Get latest release tag
id: get_release
run: |
- echo "Fetching latest release from ${{ github.repository }}..."
- LATEST_RELEASE=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName,name,isLatest -q '.[0]')
-
- if [ -z "$LATEST_RELEASE" ]; then
- echo "ERROR: No releases found in ${{ github.repository }}"
- exit 1
- fi
-
- RELEASE_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tagName')
- RELEASE_NAME=$(echo "$LATEST_RELEASE" | jq -r '.name')
-
- echo "tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
- echo "name=$RELEASE_NAME" >> $GITHUB_OUTPUT
- echo "Latest release: $RELEASE_NAME ($RELEASE_TAG)"
+ echo "RELEASE_TAG=$(gh release list --repo ${{ github.repository }} --limit 1 --json tagName -q '.[0].tagName')" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ github.token }}
- name: Display artifact structure
run: |
- echo "Artifact structure:"
ls -R artifacts/
- name: Upload artifacts to latest release
run: |
- RELEASE_TAG="${{ steps.get_release.outputs.tag }}"
- echo "Uploading artifacts to release: $RELEASE_TAG"
- echo ""
-
cd artifacts
for dir in */; do
- echo "Processing directory: $dir"
cd "$dir"
- for file in *; do
+ for file in *;
+ do
if [ -f "$file" ]; then
- echo "Uploading: $file"
gh release upload "$RELEASE_TAG" "$file" --repo ${{ github.repository }} --clobber
- echo "✓ $file uploaded successfully"
fi
done
cd ..
done
-
- echo ""
- echo "All artifacts uploaded to: https://github.com/${{ github.repository }}/releases/tag/$RELEASE_TAG"
env:
GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
new file mode 100644
index 00000000..5d0df61f
--- /dev/null
+++ b/.github/workflows/pr-check.yml
@@ -0,0 +1,35 @@
+name: PR Check
+
+on:
+ pull_request:
+ branches: [main, dev-*]
+
+jobs:
+ lint-and-build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Install dependencies
+ run: |
+ rm -rf node_modules package-lock.json
+ npm install
+
+ - name: Run ESLint
+ run: npx eslint .
+
+ - name: Run Prettier check
+ run: npx prettier --check .
+
+ - name: Type check
+ run: npx tsc --noEmit
+
+ - name: Build
+ run: npm run build
diff --git a/.gitignore b/.gitignore
index a3188d42..af4f217b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-# Logs
logs
*.log
npm-debug.log*
@@ -12,7 +11,6 @@ dist
dist-ssr
*.local
-# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
@@ -27,3 +25,6 @@ dist-ssr
/.claude/
/ssl/
.env
+/.mcp.json
+/nul
+/.vscode/
diff --git a/.husky/commit-msg b/.husky/commit-msg
new file mode 100644
index 00000000..0a4b97de
--- /dev/null
+++ b/.husky/commit-msg
@@ -0,0 +1 @@
+npx --no -- commitlint --edit $1
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 00000000..2312dc58
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+npx lint-staged
diff --git a/.nvmrc b/.nvmrc
index 2bd5a0a9..209e3ef4 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-22
+20
diff --git a/.prettierignore b/.prettierignore
index 1b8ac889..12156ff6 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,3 +1,18 @@
-# Ignore artifacts:
build
coverage
+dist
+dist-ssr
+release
+
+node_modules
+package-lock.json
+pnpm-lock.yaml
+yarn.lock
+
+db
+
+.env
+
+*.min.js
+*.min.css
+openapi.json
diff --git a/.prettierrc b/.prettierrc
index 0967ef42..fd873cbb 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1 +1,9 @@
-{}
+{
+ "semi": true,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "printWidth": 80,
+ "arrowParens": "always",
+ "endOfLine": "lf"
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3760e9d9..9db7959d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -103,4 +103,4 @@ This will start the backend and the frontend Vite server. You can access Termix
If you need help or want to request a feature with Termix, visit the [Issues](https://github.com/Termix-SSH/Support/issues) page, log in, and press `New Issue`.
Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support
-channel, however, response times may be longer.
\ No newline at end of file
+channel, however, response times may be longer.
diff --git a/DOWNLOADS.md b/DOWNLOADS.md
new file mode 100644
index 00000000..9aab0077
--- /dev/null
+++ b/DOWNLOADS.md
@@ -0,0 +1,61 @@
+# Termix Download Links
+
+## Windows
+
+| Architecture | Type | Download Link |
+| ------------ | -------- | ---------------------------------------------------------------------------------------------------------- |
+| x64 | NSIS | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_x64_nsis.exe) |
+| x64 | MSI | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_x64_msi.msi) |
+| x64 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_x64_portable.zip) |
+| ia32 | NSIS | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_ia32_nsis.exe) |
+| ia32 | MSI | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_ia32_msi.msi) |
+| ia32 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_ia32_portable.zip) |
+
+## Linux
+
+| Architecture | Type | Download Link |
+| ------------ | -------- | --------------------------------------------------------------------------------------------------------------- |
+| x64 | AppImage | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_x64_appimage.AppImage) |
+| x64 | DEB | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_x64_deb.deb) |
+| x64 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_x64_portable.tar.gz) |
+| arm64 | AppImage | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_arm64_appimage.AppImage) |
+| arm64 | DEB | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_arm64_deb.deb) |
+| arm64 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_arm64_portable.tar.gz) |
+| armv7l | AppImage | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_armv7l_appimage.AppImage) |
+| armv7l | DEB | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_armv7l_deb.deb) |
+| armv7l | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_armv7l_portable.tar.gz) |
+
+## macOS
+
+| Architecture | Type | Download Link |
+| ------------ | ------------- | -------------------------------------------------------------------------------------------------------- |
+| Universal | DMG | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_universal_dmg.dmg) |
+| Universal | Mac App Store | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_universal_mas.pkg) |
+| x64 | DMG | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_x64_dmg.dmg) |
+| arm64 | DMG | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_arm64_dmg.dmg) |
+
+---
+
+## All Platforms - Complete Download List
+
+| Platform | Architecture | Type | Download Link |
+| -------- | ------------ | ------------- | --------------------------------------------------------------------------------------------------------------- |
+| Windows | x64 | NSIS | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_x64_nsis.exe) |
+| Windows | x64 | MSI | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_x64_msi.msi) |
+| Windows | x64 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_x64_portable.zip) |
+| Windows | ia32 | NSIS | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_ia32_nsis.exe) |
+| Windows | ia32 | MSI | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_ia32_msi.msi) |
+| Windows | ia32 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_windows_ia32_portable.zip) |
+| Linux | x64 | AppImage | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_x64_appimage.AppImage) |
+| Linux | x64 | DEB | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_x64_deb.deb) |
+| Linux | x64 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_x64_portable.tar.gz) |
+| Linux | arm64 | AppImage | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_arm64_appimage.AppImage) |
+| Linux | arm64 | DEB | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_arm64_deb.deb) |
+| Linux | arm64 | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_arm64_portable.tar.gz) |
+| Linux | armv7l | AppImage | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_armv7l_appimage.AppImage) |
+| Linux | armv7l | DEB | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_armv7l_deb.deb) |
+| Linux | armv7l | Portable | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_linux_armv7l_portable.tar.gz) |
+| macOS | Universal | DMG | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_universal_dmg.dmg) |
+| macOS | Universal | Mac App Store | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_universal_mas.pkg) |
+| macOS | x64 | DMG | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_x64_dmg.dmg) |
+| macOS | arm64 | DMG | [Download](https://github.com/Termix-SSH/Termix/releases/latest/download/termix_macos_arm64_dmg.dmg) |
diff --git a/README-CN.md b/README-CN.md
index 65b82cb0..ea7fdde1 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -1,7 +1,7 @@
# 仓库统计
- 英文 |
+ 英文 |
中文
@@ -44,22 +44,24 @@
Termix 是一个开源、永久免费、自托管的一体化服务器管理平台。它提供了一个基于网页的解决方案,通过一个直观的界面管理你的服务器和基础设施。Termix
-提供 SSH 终端访问、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。
+提供 SSH 终端访问、SSH 隧道功能以及远程文件管理,还会陆续添加更多工具。Termix 是适用于所有平台的完美免费自托管 Termius 替代品。
# 功能
-- **SSH 终端访问** - 功能完整的终端,支持分屏(最多 4 个面板)和标签系统
-- **SSH 隧道管理** - 创建和管理 SSH 隧道,支持自动重连和健康监控
-- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图片、音频和视频。无缝上传、下载、重命名、删除和移动文件。
-- **SSH 主机管理器** - 保存、组织和管理 SSH 连接,支持标签和文件夹,轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥
-- **服务器统计** - 查看任意 SSH 服务器的 CPU、内存和硬盘使用情况
-- **用户认证** - 安全的用户管理,支持管理员控制、OIDC 和双因素认证(TOTP)
-- **数据库加密** - SQLite 数据库文件在静态时加密,支持自动加密/解密
-- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据,支持增量同步
+- **SSH 终端访问** - 功能齐全的终端,具有分屏支持(最多 4 个面板)和类似浏览器的选项卡系统。包括对自定义终端的支持,包括常见终端主题、字体和其他组件
+- **SSH 隧道管理** - 创建和管理 SSH 隧道,具有自动重新连接和健康监控功能
+- **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。无缝上传、下载、重命名、删除和移动文件
+- **SSH 主机管理器** - 保存、组织和管理您的 SSH 连接,支持标签和文件夹,并轻松保存可重用的登录信息,同时能够自动部署 SSH 密钥
+- **服务器统计** - 在任何 SSH 服务器上查看 CPU、内存和磁盘使用情况以及网络、正常运行时间和系统信息
+- **仪表板** - 在仪表板上一目了然地查看服务器信息
+- **用户认证** - 安全的用户管理,具有管理员控制以及 OIDC 和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。
+- **数据库加密** - 后端存储为加密的 SQLite 数据库文件
+- **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据
- **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向
-- **现代化界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁桌面/移动友好界面
-- **语言支持** - 内置英语、中文和德语支持
-- **平台支持** - 提供 Web 应用、桌面应用程序(Windows 和 Linux)以及 iOS 和 Android 专用移动应用。计划支持 macOS 和 iPadOS。
+- **现代用户界面** - 使用 React、Tailwind CSS 和 Shadcn 构建的简洁的桌面/移动设备友好界面
+- **语言** - 内置支持英语、中文、德语和葡萄牙语
+- **平台支持** - 可作为 Web 应用程序、桌面应用程序(Windows、Linux 和 macOS)以及适用于 iOS 和 Android 的专用移动/平板电脑应用程序。
+- **SSH 工具** - 创建可重用的命令片段,单击即可执行。在多个打开的终端上同时运行一个命令。
# 计划功能
@@ -69,14 +71,28 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平
支持的设备:
-- 网站(任何现代浏览器,如 Google、Safari 和 Firefox)
-- Windows(应用程序)
-- Linux(应用程序)
-- iOS(应用程序)
-- Android(应用程序)
-- iPadOS 和 macOS 正在开发中
+- 网站(任何平台上的任何现代浏览器,如 Chrome、Safari 和 Firefox)
+- Windows(x64/ia32)
+ - 便携版
+ - MSI 安装程序
+ - Chocolatey 软件包管理器
+- Linux(x64/ia32)
+ - 便携版
+ - AppImage
+ - Deb
+ - Flatpak
+- macOS(x64/ia32 on v12.0+)
+ - Apple App Store
+ - DMG
+ - Homebrew
+- iOS/iPadOS(v15.1+)
+ - Apple App Store
+ - ISO
+- Android(v7.0+)
+ - Google Play 商店
+ - APK
-访问 Termix [文档](https://docs.termix.site/install) 获取所有平台的安装信息。或者可以参考以下示例 docker-compose 文件:
+访问 Termix [文档](https://docs.termix.site/install) 了解有关如何在所有平台上安装 Termix 的更多信息。或者,在此处查看示例 Docker Compose 文件:
```yaml
services:
@@ -128,6 +144,7 @@ volumes:
你的浏览器不支持 video 标签。
+视频和图像可能已过时。
# 许可证
diff --git a/README.md b/README.md
index 071af00d..81af5534 100644
--- a/README.md
+++ b/README.md
@@ -43,24 +43,27 @@ If you would like, you can support the project here!\
-Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a web-based
+Termix is an open-source, forever-free, self-hosted all-in-one server management platform. It provides a multi-platform
solution for managing your servers and infrastructure through a single, intuitive interface. Termix offers SSH terminal
-access, SSH tunneling capabilities, and remote file management, with many more tools to come.
+access, SSH tunneling capabilities, and remote file management, with many more tools to come. Termix is the perfect
+free and self-hosted alternative to Termius available for all platforms.
# Features
-- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) and tab system
+- **SSH Terminal Access** - Full-featured terminal with split-screen support (up to 4 panels) with a browser-like tab system. Includes support for customizing the terminal including common terminal themes, fonts, and other components
- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring
-- **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly.
-- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders and easily save reusable login info while being able to automate the deploying of SSH keys
-- **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
-- **Database Encryption** - SQLite database files encrypted at rest with automatic encryption/decryption
-- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data with incremental sync
+- **Remote File Manager** - Manage files directly on remote servers with support for viewing and editing code, images, audio, and video. Upload, download, rename, delete, and move files seamlessly
+- **SSH Host Manager** - Save, organize, and manage your SSH connections with tags and folders, and easily save reusable login info while being able to automate the deployment of SSH keys
+- **Server Stats** - View CPU, memory, and disk usage along with network, uptime, and system information on any SSH server
+- **Dashboard** - View server information at a glance on your dashboard
+- **User Authentication** - Secure user management with admin controls and OIDC and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions.
+- **Database Encryption** - Backend stored as encrypted SQLite database files
+- **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data
- **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects
- **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn
-- **Languages** - Built-in support for English, Chinese, and German
-- **Platform Support** - Available as a web app, desktop application (Windows & Linux), and dedicated mobile app for iOS and Android. macOS and iPadOS support is planned.
+- **Languages** - Built-in support for English, Chinese, German, and Portuguese
+- **Platform Support** - Available as a web app, desktop application (Windows, Linux, and macOS), and dedicated mobile/tablet app for iOS and Android.
+- **SSH Tools** - Create reusable command snippets that execute with a single click. Run one command simultaneously across multiple open terminals.
# Planned Features
@@ -70,12 +73,26 @@ See [Projects](https://github.com/orgs/Termix-SSH/projects/2) for all planned fe
Supported Devices:
-- Website (any modern browser like Google, Safari, and Firefox)
-- Windows (app)
-- Linux (app)
-- iOS (app)
-- Android (app)
-- iPadOS and macOS are in progress
+- Website (any modern browser on any platform like Chrome, Safari, and Firefox)
+- Windows (x64/ia32)
+ - Portable
+ - MSI Installer
+ - Chocolatey Package Manager
+- Linux (x64/ia32)
+ - Portable
+ - AppImage
+ - Deb
+ - Flatpak
+- macOS (x64/ia32 on v12.0+)
+ - Apple App Store
+ - DMG
+ - Homebrew
+- iOS/iPadOS (v15.1+)
+ - Apple App Store
+ - ISO
+- Android (v7.0+)
+ - Google Play Store
+ - APK
Visit the Termix [Docs](https://docs.termix.site/install) for more information on how to install Termix on all platforms. Otherwise, view
a sample Docker Compose file here:
@@ -130,6 +147,7 @@ channel, however, response times may be longer.
Your browser does not support the video tag.
+Videos and images may be out of date.
# License
diff --git a/build/Termix_Mac_App_Store.provisionprofile b/build/Termix_Mac_App_Store.provisionprofile
new file mode 100644
index 00000000..8e87f405
Binary files /dev/null and b/build/Termix_Mac_App_Store.provisionprofile differ
diff --git a/build/entitlements.mac.inherit.plist b/build/entitlements.mac.inherit.plist
new file mode 100644
index 00000000..ee90b853
--- /dev/null
+++ b/build/entitlements.mac.inherit.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+
+
diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist
new file mode 100644
index 00000000..ee90b853
--- /dev/null
+++ b/build/entitlements.mac.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+
+
diff --git a/build/entitlements.mas.inherit.plist b/build/entitlements.mas.inherit.plist
new file mode 100644
index 00000000..eb23e9ac
--- /dev/null
+++ b/build/entitlements.mas.inherit.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.inherit
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/build/entitlements.mas.plist b/build/entitlements.mas.plist
new file mode 100644
index 00000000..c9ac7c4a
--- /dev/null
+++ b/build/entitlements.mas.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/build/notarize.cjs b/build/notarize.cjs
new file mode 100644
index 00000000..60067d22
--- /dev/null
+++ b/build/notarize.cjs
@@ -0,0 +1,31 @@
+const { notarize } = require('@electron/notarize');
+
+exports.default = async function notarizing(context) {
+ const { electronPlatformName, appOutDir } = context;
+
+ if (electronPlatformName !== 'darwin') {
+ return;
+ }
+
+ const appleId = process.env.APPLE_ID;
+ const appleIdPassword = process.env.APPLE_ID_PASSWORD;
+ const teamId = process.env.APPLE_TEAM_ID;
+
+ if (!appleId || !appleIdPassword || !teamId) {
+ return;
+ }
+
+ const appName = context.packager.appInfo.productFilename;
+
+ try {
+ await notarize({
+ appBundleId: 'com.karmaa.termix',
+ appPath: `${appOutDir}/${appName}.app`,
+ appleId: appleId,
+ appleIdPassword: appleIdPassword,
+ teamId: teamId,
+ });
+ } catch (error) {
+ console.error('Notarization failed:', error);
+ }
+};
diff --git a/chocolatey/termix-ssh.nuspec b/chocolatey/termix-ssh.nuspec
new file mode 100644
index 00000000..b5de0b13
--- /dev/null
+++ b/chocolatey/termix-ssh.nuspec
@@ -0,0 +1,35 @@
+
+
+
+ termix-ssh
+ VERSION_PLACEHOLDER
+ https://github.com/Termix-SSH/Termix
+ bugattiguy527
+ Termix SSH
+ bugattiguy527
+ https://github.com/Termix-SSH/Termix
+ https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.png
+ https://raw.githubusercontent.com/Termix-SSH/Termix/refs/heads/main/LICENSE
+ false
+ https://github.com/Termix-SSH/Termix
+ https://docs.termix.site/install
+ https://github.com/Termix-SSH/Support/issues
+ docker ssh self-hosted file-management ssh-tunnel termix server-management terminal
+ Termix is a web-based server management platform with SSH terminal, tunneling, and file editing capabilities.
+
+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
+- Remote file management
+- Server monitoring and management
+
+This package installs the desktop application version of Termix.
+
+ https://github.com/Termix-SSH/Termix/releases
+
+
+
+
+
diff --git a/chocolatey/tools/chocolateyinstall.ps1 b/chocolatey/tools/chocolateyinstall.ps1
new file mode 100644
index 00000000..c2335167
--- /dev/null
+++ b/chocolatey/tools/chocolateyinstall.ps1
@@ -0,0 +1,20 @@
+$ErrorActionPreference = 'Stop'
+
+$packageName = 'termix-ssh'
+$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+$url64 = 'DOWNLOAD_URL_PLACEHOLDER'
+$checksum64 = 'CHECKSUM_PLACEHOLDER'
+$checksumType64 = 'sha256'
+
+$packageArgs = @{
+ packageName = $packageName
+ fileType = 'msi'
+ url64bit = $url64
+ softwareName = 'Termix*'
+ checksum64 = $checksum64
+ checksumType64 = $checksumType64
+ silentArgs = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`""
+ validExitCodes = @(0, 3010, 1641)
+}
+
+Install-ChocolateyPackage @packageArgs
diff --git a/chocolatey/tools/chocolateyuninstall.ps1 b/chocolatey/tools/chocolateyuninstall.ps1
new file mode 100644
index 00000000..48a5e18c
--- /dev/null
+++ b/chocolatey/tools/chocolateyuninstall.ps1
@@ -0,0 +1,33 @@
+$ErrorActionPreference = 'Stop'
+
+$packageName = 'termix-ssh'
+$softwareName = 'Termix*'
+$installerType = 'msi'
+
+$silentArgs = '/qn /norestart'
+$validExitCodes = @(0, 3010, 1605, 1614, 1641)
+
+[array]$key = Get-UninstallRegistryKey -SoftwareName $softwareName
+
+if ($key.Count -eq 1) {
+ $key | % {
+ $file = "$($_.UninstallString)"
+
+ if ($installerType -eq 'msi') {
+ $silentArgs = "$($_.PSChildName) $silentArgs"
+ $file = ''
+ }
+
+ Uninstall-ChocolateyPackage -PackageName $packageName `
+ -FileType $installerType `
+ -SilentArgs "$silentArgs" `
+ -ValidExitCodes $validExitCodes `
+ -File "$file"
+ }
+} elseif ($key.Count -eq 0) {
+ Write-Warning "$packageName has already been uninstalled by other means."
+} elseif ($key.Count -gt 1) {
+ Write-Warning "$($key.Count) matches found!"
+ Write-Warning "To prevent accidental data loss, no programs will be uninstalled."
+ $key | % {Write-Warning "- $($_.DisplayName)"}
+}
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 8bf66283..c67b6686 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -2,16 +2,12 @@
FROM node:22-slim AS deps
WORKDIR /app
-RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
+RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
COPY package*.json ./
-ENV npm_config_target_platform=linux
-ENV npm_config_target_arch=x64
-ENV npm_config_target_libc=glibc
-
RUN rm -rf node_modules package-lock.json && \
- npm install --force && \
+ npm install --ignore-scripts --force && \
npm cache clean --force
# Stage 2: Build frontend
@@ -31,10 +27,6 @@ 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
@@ -47,10 +39,6 @@ RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt
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 rebuild better-sqlite3 bcryptjs --force && \
npm cache clean --force
@@ -82,8 +70,8 @@ COPY --chown=node:node package.json ./
VOLUME ["/app/data"]
-EXPOSE ${PORT} 30001 30002 30003 30004 30005
+EXPOSE ${PORT} 30001 30002 30003 30004 30005 30006
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
-CMD ["/entrypoint.sh"]
\ No newline at end of file
+CMD ["/entrypoint.sh"]
diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf
index f64e8e4e..e27032b0 100644
--- a/docker/nginx-https.conf
+++ b/docker/nginx-https.conf
@@ -34,7 +34,6 @@ http {
ssl_certificate_key ${SSL_KEY_PATH};
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
- add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
@@ -49,6 +48,15 @@ http {
log_not_found off;
}
+ location ~ ^/users/sessions(/.*)?$ {
+ proxy_pass http://127.0.0.1:30001;
+ 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 ~ ^/users(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -92,27 +100,36 @@ http {
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;
-
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
- location ~ ^/database(/.*)?$ {
- client_max_body_size 5G;
- client_body_timeout 300s;
-
+ location ~ ^/snippets(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
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 ~ ^/database(/.*)?$ {
+ client_max_body_size 5G;
+ client_body_timeout 300s;
+
+ proxy_pass http://127.0.0.1:30001;
+ 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;
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
-
+
proxy_request_buffering off;
proxy_buffering off;
}
@@ -120,18 +137,18 @@ http {
location ~ ^/db(/.*)?$ {
client_max_body_size 5G;
client_body_timeout 300s;
-
+
proxy_pass http://127.0.0.1:30001;
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;
-
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
-
+
proxy_request_buffering off;
proxy_buffering off;
}
@@ -216,18 +233,18 @@ http {
location /ssh/file_manager/ssh/ {
client_max_body_size 5G;
client_body_timeout 300s;
-
+
proxy_pass http://127.0.0.1:30004;
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;
-
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
-
+
proxy_request_buffering off;
proxy_buffering off;
}
@@ -259,9 +276,27 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
+ location ~ ^/uptime(/.*)?$ {
+ proxy_pass http://127.0.0.1:30006;
+ 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 ~ ^/activity(/.*)?$ {
+ proxy_pass http://127.0.0.1:30006;
+ 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;
+ }
+
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
-}
\ No newline at end of file
+}
diff --git a/docker/nginx.conf b/docker/nginx.conf
index c180c180..cf078022 100644
--- a/docker/nginx.conf
+++ b/docker/nginx.conf
@@ -23,13 +23,20 @@ http {
listen ${PORT};
server_name localhost;
- add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ root /usr/share/nginx/html;
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ try_files $uri =404;
+ }
+
location / {
root /usr/share/nginx/html;
index index.html index.htm;
+ try_files $uri $uri/ /index.html;
}
location ~* \.map$ {
@@ -38,6 +45,15 @@ http {
log_not_found off;
}
+ location ~ ^/users/sessions(/.*)?$ {
+ proxy_pass http://127.0.0.1:30001;
+ 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 ~ ^/users(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
@@ -81,27 +97,36 @@ http {
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;
-
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
- location ~ ^/database(/.*)?$ {
- client_max_body_size 5G;
- client_body_timeout 300s;
-
+ location ~ ^/snippets(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
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 ~ ^/database(/.*)?$ {
+ client_max_body_size 5G;
+ client_body_timeout 300s;
+
+ proxy_pass http://127.0.0.1:30001;
+ 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;
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
-
+
proxy_request_buffering off;
proxy_buffering off;
}
@@ -109,18 +134,18 @@ http {
location ~ ^/db(/.*)?$ {
client_max_body_size 5G;
client_body_timeout 300s;
-
+
proxy_pass http://127.0.0.1:30001;
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;
-
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
-
+
proxy_request_buffering off;
proxy_buffering off;
}
@@ -205,18 +230,18 @@ http {
location /ssh/file_manager/ssh/ {
client_max_body_size 5G;
client_body_timeout 300s;
-
+
proxy_pass http://127.0.0.1:30004;
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;
-
+
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
-
+
proxy_request_buffering off;
proxy_buffering off;
}
@@ -248,9 +273,27 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
+ location ~ ^/uptime(/.*)?$ {
+ proxy_pass http://127.0.0.1:30006;
+ 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 ~ ^/activity(/.*)?$ {
+ proxy_pass http://127.0.0.1:30006;
+ 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;
+ }
+
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
-}
\ No newline at end of file
+}
diff --git a/electron-builder.json b/electron-builder.json
index 9fb6d36b..218153e1 100644
--- a/electron-builder.json
+++ b/electron-builder.json
@@ -1,6 +1,7 @@
{
- "appId": "com.termix.app",
+ "appId": "com.karmaa.termix",
"productName": "Termix",
+ "publish": null,
"directories": {
"output": "release"
},
@@ -21,35 +22,53 @@
},
"buildDependenciesFromSource": false,
"nodeGypRebuild": false,
- "npmRebuild": false,
+ "npmRebuild": true,
"win": {
- "target": "nsis",
+ "target": [
+ {
+ "target": "nsis",
+ "arch": ["x64", "ia32"]
+ },
+ {
+ "target": "msi",
+ "arch": ["x64", "ia32"]
+ }
+ ],
"icon": "public/icon.ico",
"executableName": "Termix"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
- "artifactName": "${productName}-Setup-${version}.${ext}",
+ "artifactName": "termix_windows_${arch}_nsis.${ext}",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Termix",
"uninstallDisplayName": "Termix"
},
+ "msi": {
+ "artifactName": "termix_windows_${arch}_msi.${ext}"
+ },
"linux": {
+ "artifactName": "termix_linux_${arch}_portable.${ext}",
"target": [
{
"target": "AppImage",
- "arch": ["x64"]
+ "arch": ["x64", "arm64", "armv7l"]
+ },
+ {
+ "target": "deb",
+ "arch": ["x64", "arm64", "armv7l"]
},
{
"target": "tar.gz",
- "arch": ["x64"]
+ "arch": ["x64", "arm64", "armv7l"]
}
],
"icon": "public/icon.png",
"category": "Development",
"executableName": "termix",
+ "maintainer": "Termix ",
"desktop": {
"entry": {
"Name": "Termix",
@@ -58,5 +77,52 @@
"StartupWMClass": "termix"
}
}
+ },
+ "appImage": {
+ "artifactName": "termix_linux_${arch}_appimage.${ext}"
+ },
+ "deb": {
+ "artifactName": "termix_linux_${arch}_deb.${ext}"
+ },
+
+ "mac": {
+ "target": [
+ {
+ "target": "mas",
+ "arch": "universal"
+ },
+ {
+ "target": "dmg",
+ "arch": ["universal", "x64", "arm64"]
+ }
+ ],
+ "icon": "public/icon.icns",
+ "category": "public.app-category.developer-tools",
+ "hardenedRuntime": true,
+ "gatekeeperAssess": false,
+ "entitlements": "build/entitlements.mac.plist",
+ "entitlementsInherit": "build/entitlements.mac.inherit.plist",
+ "type": "distribution",
+ "minimumSystemVersion": "10.15"
+ },
+ "dmg": {
+ "artifactName": "termix_macos_${arch}_dmg.${ext}",
+ "sign": true
+ },
+ "afterSign": "build/notarize.cjs",
+ "mas": {
+ "provisioningProfile": "build/Termix_Mac_App_Store.provisionprofile",
+ "entitlements": "build/entitlements.mas.plist",
+ "entitlementsInherit": "build/entitlements.mas.inherit.plist",
+ "hardenedRuntime": false,
+ "gatekeeperAssess": false,
+ "asarUnpack": ["**/*.node"],
+ "type": "distribution",
+ "category": "public.app-category.developer-tools",
+ "artifactName": "termix_macos_${arch}_mas.${ext}",
+ "extendInfo": {
+ "ITSAppUsesNonExemptEncryption": false,
+ "NSAppleEventsUsageDescription": "Termix needs access to control other applications for terminal operations."
+ }
}
}
diff --git a/electron/main.cjs b/electron/main.cjs
index dadeee4b..8f623912 100644
--- a/electron/main.cjs
+++ b/electron/main.cjs
@@ -1,22 +1,34 @@
-const { app, BrowserWindow, shell, ipcMain, dialog } = require("electron");
+const {
+ app,
+ BrowserWindow,
+ shell,
+ ipcMain,
+ dialog,
+ Menu,
+} = require("electron");
const path = require("path");
const fs = require("fs");
const os = require("os");
+if (process.platform === "linux") {
+ app.commandLine.appendSwitch("--no-sandbox");
+ app.commandLine.appendSwitch("--disable-setuid-sandbox");
+ app.commandLine.appendSwitch("--disable-dev-shm-usage");
+
+ app.disableHardwareAcceleration();
+ app.commandLine.appendSwitch("--disable-gpu");
+ app.commandLine.appendSwitch("--disable-gpu-compositing");
+}
+
app.commandLine.appendSwitch("--ignore-certificate-errors");
app.commandLine.appendSwitch("--ignore-ssl-errors");
app.commandLine.appendSwitch("--ignore-certificate-errors-spki-list");
app.commandLine.appendSwitch("--enable-features=NetworkService");
-if (process.platform === "linux") {
- app.commandLine.appendSwitch("--no-sandbox");
- app.commandLine.appendSwitch("--disable-setuid-sandbox");
- app.commandLine.appendSwitch("--disable-dev-shm-usage");
-}
-
let mainWindow = null;
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
+const appRoot = isDev ? process.cwd() : path.join(__dirname, "..");
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
@@ -34,40 +46,131 @@ if (!gotTheLock) {
}
function createWindow() {
+ const appVersion = app.getVersion();
+ const electronVersion = process.versions.electron;
+ const platform =
+ process.platform === "win32"
+ ? "Windows"
+ : process.platform === "darwin"
+ ? "macOS"
+ : "Linux";
+
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"),
+ icon: path.join(appRoot, "public", "icon.png"),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
- webSecurity: true,
+ webSecurity: false,
preload: path.join(__dirname, "preload.js"),
+ partition: "persist:termix",
+ allowRunningInsecureContent: true,
+ webviewTag: true,
+ offscreen: false,
},
- show: false,
+ show: true,
});
if (process.platform !== "darwin") {
mainWindow.setMenuBarVisibility(false);
}
+ const customUserAgent = `Termix-Desktop/${appVersion} (${platform}; Electron/${electronVersion})`;
+ mainWindow.webContents.setUserAgent(customUserAgent);
+
+ mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
+ (details, callback) => {
+ details.requestHeaders["X-Electron-App"] = "true";
+
+ details.requestHeaders["User-Agent"] = customUserAgent;
+
+ callback({ requestHeaders: details.requestHeaders });
+ },
+ );
+
if (isDev) {
mainWindow.loadURL("http://localhost:5173");
mainWindow.webContents.openDevTools();
} else {
- const indexPath = path.join(__dirname, "..", "dist", "index.html");
- mainWindow.loadFile(indexPath);
+ const indexPath = path.join(appRoot, "dist", "index.html");
+ mainWindow.loadFile(indexPath).catch((err) => {
+ console.error("Failed to load file:", err);
+ });
}
+ mainWindow.webContents.session.webRequest.onHeadersReceived(
+ (details, callback) => {
+ const headers = details.responseHeaders;
+
+ if (headers) {
+ delete headers["x-frame-options"];
+ delete headers["X-Frame-Options"];
+
+ if (headers["content-security-policy"]) {
+ headers["content-security-policy"] = headers[
+ "content-security-policy"
+ ]
+ .map((value) => value.replace(/frame-ancestors[^;]*/gi, ""))
+ .filter((value) => value.trim().length > 0);
+
+ if (headers["content-security-policy"].length === 0) {
+ delete headers["content-security-policy"];
+ }
+ }
+ if (headers["Content-Security-Policy"]) {
+ headers["Content-Security-Policy"] = headers[
+ "Content-Security-Policy"
+ ]
+ .map((value) => value.replace(/frame-ancestors[^;]*/gi, ""))
+ .filter((value) => value.trim().length > 0);
+
+ if (headers["Content-Security-Policy"].length === 0) {
+ delete headers["Content-Security-Policy"];
+ }
+ }
+
+ if (headers["set-cookie"]) {
+ headers["set-cookie"] = headers["set-cookie"].map((cookie) => {
+ let modified = cookie.replace(
+ /;\s*SameSite=Strict/gi,
+ "; SameSite=None",
+ );
+ modified = modified.replace(
+ /;\s*SameSite=Lax/gi,
+ "; SameSite=None",
+ );
+ if (!modified.includes("SameSite=")) {
+ modified += "; SameSite=None";
+ }
+ if (
+ !modified.includes("Secure") &&
+ details.url.startsWith("https")
+ ) {
+ modified += "; Secure";
+ }
+ return modified;
+ });
+ }
+ }
+
+ callback({ responseHeaders: headers });
+ },
+ );
+
mainWindow.once("ready-to-show", () => {
mainWindow.show();
});
+ setTimeout(() => {
+ if (mainWindow && !mainWindow.isVisible()) {
+ mainWindow.show();
+ }
+ }, 3000);
+
mainWindow.webContents.on(
"did-fail-load",
(event, errorCode, errorDescription, validatedURL) => {
@@ -84,13 +187,6 @@ function createWindow() {
console.log("Frontend loaded successfully");
});
- mainWindow.on("close", (event) => {
- if (process.platform === "darwin") {
- event.preventDefault();
- mainWindow.hide();
- }
- });
-
mainWindow.on("closed", () => {
mainWindow = null;
});
@@ -462,21 +558,78 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
}
});
+function createMenu() {
+ if (process.platform === "darwin") {
+ const template = [
+ {
+ label: app.name,
+ submenu: [
+ { role: "about" },
+ { type: "separator" },
+ { role: "services" },
+ { type: "separator" },
+ { role: "hide" },
+ { role: "hideOthers" },
+ { role: "unhide" },
+ { type: "separator" },
+ { role: "quit" },
+ ],
+ },
+ {
+ label: "Edit",
+ submenu: [
+ { role: "undo" },
+ { role: "redo" },
+ { type: "separator" },
+ { role: "cut" },
+ { role: "copy" },
+ { role: "paste" },
+ { role: "selectAll" },
+ ],
+ },
+ {
+ label: "View",
+ submenu: [
+ { role: "reload" },
+ { role: "forceReload" },
+ { role: "toggleDevTools" },
+ { type: "separator" },
+ { role: "resetZoom" },
+ { role: "zoomIn" },
+ { role: "zoomOut" },
+ { type: "separator" },
+ { role: "togglefullscreen" },
+ ],
+ },
+ {
+ label: "Window",
+ submenu: [
+ { role: "minimize" },
+ { role: "zoom" },
+ { type: "separator" },
+ { role: "front" },
+ { type: "separator" },
+ { role: "window" },
+ ],
+ },
+ ];
+ const menu = Menu.buildFromTemplate(template);
+ Menu.setApplicationMenu(menu);
+ }
+}
+
app.whenReady().then(() => {
+ createMenu();
createWindow();
});
app.on("window-all-closed", () => {
- if (process.platform !== "darwin") {
- app.quit();
- }
+ app.quit();
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
- } else if (mainWindow) {
- mainWindow.show();
}
});
diff --git a/flatpak/com.karmaa.termix.desktop b/flatpak/com.karmaa.termix.desktop
new file mode 100644
index 00000000..3aabfd06
--- /dev/null
+++ b/flatpak/com.karmaa.termix.desktop
@@ -0,0 +1,11 @@
+[Desktop Entry]
+Name=Termix
+Comment=Web-based server management platform with SSH terminal, tunneling, and file editing
+Exec=termix %U
+Icon=com.karmaa.termix
+Terminal=false
+Type=Application
+Categories=Development;Network;System;
+Keywords=ssh;terminal;server;management;tunnel;
+StartupWMClass=termix
+StartupNotify=true
diff --git a/flatpak/com.karmaa.termix.metainfo.xml b/flatpak/com.karmaa.termix.metainfo.xml
new file mode 100644
index 00000000..0c3c6895
--- /dev/null
+++ b/flatpak/com.karmaa.termix.metainfo.xml
@@ -0,0 +1,77 @@
+
+
+ com.karmaa.termix
+ Termix
+ Web-based server management platform with SSH terminal, tunneling, and file editing
+
+ CC0-1.0
+ GPL-3.0-or-later
+
+ bugattiguy527
+
+
+
+ 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.
+
+ Features:
+
+ SSH terminal access with full terminal emulation
+ SSH tunneling capabilities for secure port forwarding
+ Remote file management with editor support
+ Server monitoring and management tools
+ Self-hosted solution - keep your data private
+ Modern, intuitive web interface
+
+
+
+ com.karmaa.termix.desktop
+
+
+
+ https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/screenshots/terminal.png
+ SSH Terminal Interface
+
+
+
+ https://github.com/Termix-SSH/Termix
+ https://github.com/Termix-SSH/Support/issues
+ https://docs.termix.site
+ https://github.com/Termix-SSH/Termix
+
+
+ moderate
+
+
+
+
+
+ Latest release of Termix
+
+ https://github.com/Termix-SSH/Termix/releases
+
+
+
+
+ Development
+ Network
+ System
+
+
+
+ ssh
+ terminal
+ server
+ management
+ tunnel
+ file-manager
+
+
+
+ termix
+
+
+
+ always
+
+
diff --git a/flatpak/com.karmaa.termix.yml b/flatpak/com.karmaa.termix.yml
new file mode 100644
index 00000000..4405a10f
--- /dev/null
+++ b/flatpak/com.karmaa.termix.yml
@@ -0,0 +1,69 @@
+app-id: com.karmaa.termix
+runtime: org.freedesktop.Platform
+runtime-version: "23.08"
+sdk: org.freedesktop.Sdk
+base: org.electronjs.Electron2.BaseApp
+base-version: "23.08"
+command: termix
+separate-locales: false
+
+finish-args:
+ - --socket=x11
+ - --socket=wayland
+ - --socket=pulseaudio
+ - --share=network
+ - --share=ipc
+ - --device=dri
+ - --filesystem=home
+ - --socket=ssh-auth
+ - --talk-name=org.freedesktop.Notifications
+ - --talk-name=org.freedesktop.secrets
+
+modules:
+ - name: termix
+ buildsystem: simple
+ build-commands:
+ - chmod +x termix.AppImage
+ - ./termix.AppImage --appimage-extract
+
+ - install -Dm755 squashfs-root/termix /app/bin/termix
+ - cp -r squashfs-root/resources /app/bin/
+ - cp -r squashfs-root/locales /app/bin/ || true
+
+ - install -Dm644 com.karmaa.termix.desktop /app/share/applications/com.karmaa.termix.desktop
+
+ - install -Dm644 com.karmaa.termix.metainfo.xml /app/share/metainfo/com.karmaa.termix.metainfo.xml
+
+ - install -Dm644 com.karmaa.termix.svg /app/share/icons/hicolor/scalable/apps/com.karmaa.termix.svg
+ - install -Dm644 icon-256.png /app/share/icons/hicolor/256x256/apps/com.karmaa.termix.png || true
+ - install -Dm644 icon-128.png /app/share/icons/hicolor/128x128/apps/com.karmaa.termix.png || true
+
+ sources:
+ - type: file
+ url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_x64_VERSION_PLACEHOLDER_appimage.AppImage
+ sha256: CHECKSUM_X64_PLACEHOLDER
+ dest-filename: termix.AppImage
+ only-arches:
+ - x86_64
+
+ - type: file
+ url: https://github.com/Termix-SSH/Termix/releases/download/release-VERSION_PLACEHOLDER-tag/termix_linux_arm64_VERSION_PLACEHOLDER_appimage.AppImage
+ sha256: CHECKSUM_ARM64_PLACEHOLDER
+ dest-filename: termix.AppImage
+ only-arches:
+ - aarch64
+
+ - type: file
+ path: com.karmaa.termix.desktop
+
+ - type: file
+ path: com.karmaa.termix.metainfo.xml
+
+ - type: file
+ path: com.karmaa.termix.svg
+
+ - type: file
+ path: icon-256.png
+
+ - type: file
+ path: icon-128.png
diff --git a/flatpak/flathub.json b/flatpak/flathub.json
new file mode 100644
index 00000000..43c4074e
--- /dev/null
+++ b/flatpak/flathub.json
@@ -0,0 +1,5 @@
+{
+ "only-arches": ["x86_64", "aarch64"],
+ "skip-icons-check": false,
+ "skip-appstream-check": false
+}
diff --git a/flatpak/prepare-flatpak.sh b/flatpak/prepare-flatpak.sh
new file mode 100644
index 00000000..05162b64
--- /dev/null
+++ b/flatpak/prepare-flatpak.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+VERSION="$1"
+CHECKSUM="$2"
+RELEASE_DATE="$3"
+
+if [ -z "$VERSION" ] || [ -z "$CHECKSUM" ] || [ -z "$RELEASE_DATE" ]; then
+ echo "Usage: $0 "
+ echo "Example: $0 1.8.0 abc123... 2025-10-26"
+ exit 1
+fi
+
+echo "Preparing Flatpak submission for version $VERSION"
+
+cp public/icon.svg flatpak/com.karmaa.termix.svg
+echo "✓ Copied SVG icon"
+
+if command -v convert &> /dev/null; then
+ convert public/icon.png -resize 256x256 flatpak/icon-256.png
+ convert public/icon.png -resize 128x128 flatpak/icon-128.png
+ echo "✓ Generated PNG icons"
+else
+ cp public/icon.png flatpak/icon-256.png
+ cp public/icon.png flatpak/icon-128.png
+ echo "⚠ ImageMagick not found, using original icon"
+fi
+
+sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.yml
+sed -i "s/CHECKSUM_PLACEHOLDER/$CHECKSUM/g" flatpak/com.karmaa.termix.yml
+echo "✓ Updated manifest with version $VERSION"
+
+sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" flatpak/com.karmaa.termix.metainfo.xml
+sed -i "s/DATE_PLACEHOLDER/$RELEASE_DATE/g" flatpak/com.karmaa.termix.metainfo.xml
diff --git a/homebrew/termix.rb b/homebrew/termix.rb
new file mode 100644
index 00000000..9522fa73
--- /dev/null
+++ b/homebrew/termix.rb
@@ -0,0 +1,24 @@
+cask "termix" do
+ version "VERSION_PLACEHOLDER"
+ sha256 "CHECKSUM_PLACEHOLDER"
+
+ url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_#{version}_dmg.dmg"
+ name "Termix"
+ desc "Web-based server management platform with SSH terminal, tunneling, and file editing"
+ homepage "https://github.com/Termix-SSH/Termix"
+
+ livecheck do
+ url :url
+ strategy :github_latest
+ end
+
+ app "Termix.app"
+
+ zap trash: [
+ "~/Library/Application Support/termix",
+ "~/Library/Caches/com.karmaa.termix",
+ "~/Library/Caches/com.karmaa.termix.ShipIt",
+ "~/Library/Preferences/com.karmaa.termix.plist",
+ "~/Library/Saved Application State/com.karmaa.termix.savedState",
+ ]
+end
diff --git a/index.html b/index.html
index 5c925758..b376f7cd 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,36 @@
Termix
+
diff --git a/package-lock.json b/package-lock.json
index bd087a93..011b4f07 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "termix",
- "version": "1.7.2",
+ "version": "1.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "termix",
- "version": "1.7.2",
+ "version": "1.8.0",
"dependencies": {
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.3.3",
@@ -25,11 +25,12 @@
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.8",
- "@tailwindcss/vite": "^4.1.11",
+ "@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.9",
"@types/jszip": "^3.4.0",
@@ -80,16 +81,21 @@
"react-simple-keyboard": "^3.8.120",
"react-syntax-highlighter": "^15.6.6",
"react-xtermjs": "^1.0.10",
+ "recharts": "^3.2.1",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"speakeasy": "^2.0.0",
"ssh2": "^1.16.0",
"tailwind-merge": "^3.3.1",
+ "tailwindcss": "^4.1.14",
"wait-on": "^9.0.1",
"ws": "^8.18.3",
"zod": "^4.0.5"
},
"devDependencies": {
+ "@commitlint/cli": "^20.1.0",
+ "@commitlint/config-conventional": "^20.0.0",
+ "@electron/notarize": "^2.5.0",
"@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
@@ -100,7 +106,7 @@
"@types/react-dom": "^19.1.6",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
- "@vitejs/plugin-react-swc": "^3.10.2",
+ "@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.2.1",
"electron": "^38.0.0",
"electron-builder": "^26.0.12",
@@ -108,12 +114,269 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
+ "husky": "^9.1.7",
+ "lint-staged": "^16.2.3",
"prettier": "3.6.2",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0",
"vite": "^7.1.5"
}
},
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/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/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/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/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
@@ -123,11 +386,60 @@
"node": ">=6.9.0"
}
},
- "node_modules/@codemirror/autocomplete": {
- "version": "6.19.0",
- "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.0.tgz",
- "integrity": "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg==",
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
"license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.19.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz",
+ "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==",
+ "license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -136,9 +448,9 @@
}
},
"node_modules/@codemirror/commands": {
- "version": "6.8.1",
- "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
- "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz",
+ "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
@@ -176,6 +488,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
@@ -198,10 +511,11 @@
}
},
"node_modules/@codemirror/lang-html": {
- "version": "6.4.10",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.10.tgz",
- "integrity": "sha512-h/SceTVsN5r+WE+TVP2g3KDvNoSzbSrtZXCKo4vkKdbfT5t4otuVgngGdFukOO/rwRD2++pCxoh6xD4TEVMkQA==",
+ "version": "6.4.11",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
+ "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
@@ -211,7 +525,7 @@
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
- "@lezer/html": "^1.3.0"
+ "@lezer/html": "^1.3.12"
}
},
"node_modules/@codemirror/lang-java": {
@@ -229,6 +543,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
@@ -239,6 +554,19 @@
"@lezer/javascript": "^1.0.0"
}
},
+ "node_modules/@codemirror/lang-jinja": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.0.tgz",
+ "integrity": "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/lang-html": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.2.0",
+ "@lezer/lr": "^1.4.0"
+ }
+ },
"node_modules/@codemirror/lang-json": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
@@ -279,9 +607,9 @@
}
},
"node_modules/@codemirror/lang-markdown": {
- "version": "6.3.4",
- "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.4.tgz",
- "integrity": "sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
+ "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
@@ -416,6 +744,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@@ -426,9 +755,9 @@
}
},
"node_modules/@codemirror/language-data": {
- "version": "6.5.1",
- "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.1.tgz",
- "integrity": "sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz",
+ "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-angular": "^0.1.0",
@@ -438,6 +767,7 @@
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-java": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
+ "@codemirror/lang-jinja": "^6.0.0",
"@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-less": "^6.0.0",
"@codemirror/lang-liquid": "^6.0.0",
@@ -465,9 +795,9 @@
}
},
"node_modules/@codemirror/lint": {
- "version": "6.8.5",
- "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
- "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.1.tgz",
+ "integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@@ -491,6 +821,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@@ -508,10 +839,11 @@
}
},
"node_modules/@codemirror/view": {
- "version": "6.38.4",
- "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.4.tgz",
- "integrity": "sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==",
+ "version": "6.38.6",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
+ "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@@ -519,6 +851,302 @@
"w3c-keyname": "^2.2.4"
}
},
+ "node_modules/@commitlint/cli": {
+ "version": "20.1.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.1.0.tgz",
+ "integrity": "sha512-pW5ujjrOovhq5RcYv5xCpb4GkZxkO2+GtOdBW2/qrr0Ll9tl3PX0aBBobGQl3mdZUbOBgwAexEQLeH6uxL0VYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/format": "^20.0.0",
+ "@commitlint/lint": "^20.0.0",
+ "@commitlint/load": "^20.1.0",
+ "@commitlint/read": "^20.0.0",
+ "@commitlint/types": "^20.0.0",
+ "tinyexec": "^1.0.0",
+ "yargs": "^17.0.0"
+ },
+ "bin": {
+ "commitlint": "cli.js"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/config-conventional": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.0.0.tgz",
+ "integrity": "sha512-q7JroPIkDBtyOkVe9Bca0p7kAUYxZMxkrBArCfuD3yN4KjRAenP9PmYwnn7rsw8Q+hHq1QB2BRmBh0/Z19ZoJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.0.0",
+ "conventional-changelog-conventionalcommits": "^7.0.2"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/config-validator": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.0.0.tgz",
+ "integrity": "sha512-BeyLMaRIJDdroJuYM2EGhDMGwVBMZna9UiIqV9hxj+J551Ctc6yoGuGSmghOy/qPhBSuhA6oMtbEiTmxECafsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.0.0",
+ "ajv": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/ensure": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.0.0.tgz",
+ "integrity": "sha512-WBV47Fffvabe68n+13HJNFBqiMH5U1Ryls4W3ieGwPC0C7kJqp3OVQQzG2GXqOALmzrgAB+7GXmyy8N9ct8/Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.0.0",
+ "lodash.camelcase": "^4.3.0",
+ "lodash.kebabcase": "^4.1.1",
+ "lodash.snakecase": "^4.1.1",
+ "lodash.startcase": "^4.4.0",
+ "lodash.upperfirst": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/execute-rule": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz",
+ "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/format": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.0.0.tgz",
+ "integrity": "sha512-zrZQXUcSDmQ4eGGrd+gFESiX0Rw+WFJk7nW4VFOmxub4mAATNKBQ4vNw5FgMCVehLUKG2OT2LjOqD0Hk8HvcRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.0.0",
+ "chalk": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/format/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@commitlint/is-ignored": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.0.0.tgz",
+ "integrity": "sha512-ayPLicsqqGAphYIQwh9LdAYOVAQ9Oe5QCgTNTj+BfxZb9b/JW222V5taPoIBzYnAP0z9EfUtljgBk+0BN4T4Cw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.0.0",
+ "semver": "^7.6.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/lint": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.0.0.tgz",
+ "integrity": "sha512-kWrX8SfWk4+4nCexfLaQT3f3EcNjJwJBsSZ5rMBw6JCd6OzXufFHgel2Curos4LKIxwec9WSvs2YUD87rXlxNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/is-ignored": "^20.0.0",
+ "@commitlint/parse": "^20.0.0",
+ "@commitlint/rules": "^20.0.0",
+ "@commitlint/types": "^20.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/load": {
+ "version": "20.1.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.1.0.tgz",
+ "integrity": "sha512-qo9ER0XiAimATQR5QhvvzePfeDfApi/AFlC1G+YN+ZAY8/Ua6IRrDrxRvQAr+YXUKAxUsTDSp9KXeXLBPsNRWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^20.0.0",
+ "@commitlint/execute-rule": "^20.0.0",
+ "@commitlint/resolve-extends": "^20.1.0",
+ "@commitlint/types": "^20.0.0",
+ "chalk": "^5.3.0",
+ "cosmiconfig": "^9.0.0",
+ "cosmiconfig-typescript-loader": "^6.1.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2",
+ "lodash.uniq": "^4.5.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/load/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@commitlint/message": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.0.0.tgz",
+ "integrity": "sha512-gLX4YmKnZqSwkmSB9OckQUrI5VyXEYiv3J5JKZRxIp8jOQsWjZgHSG/OgEfMQBK9ibdclEdAyIPYggwXoFGXjQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/parse": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.0.0.tgz",
+ "integrity": "sha512-j/PHCDX2bGM5xGcWObOvpOc54cXjn9g6xScXzAeOLwTsScaL4Y+qd0pFC6HBwTtrH92NvJQc+2Lx9HFkVi48cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^20.0.0",
+ "conventional-changelog-angular": "^7.0.0",
+ "conventional-commits-parser": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/read": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.0.0.tgz",
+ "integrity": "sha512-Ti7Y7aEgxsM1nkwA4ZIJczkTFRX/+USMjNrL9NXwWQHqNqrBX2iMi+zfuzZXqfZ327WXBjdkRaytJ+z5vNqTOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/top-level": "^20.0.0",
+ "@commitlint/types": "^20.0.0",
+ "git-raw-commits": "^4.0.0",
+ "minimist": "^1.2.8",
+ "tinyexec": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/resolve-extends": {
+ "version": "20.1.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.1.0.tgz",
+ "integrity": "sha512-cxKXQrqHjZT3o+XPdqDCwOWVFQiae++uwd9dUBC7f2MdV58ons3uUvASdW7m55eat5sRiQ6xUHyMWMRm6atZWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^20.0.0",
+ "@commitlint/types": "^20.0.0",
+ "global-directory": "^4.0.1",
+ "import-meta-resolve": "^4.0.0",
+ "lodash.mergewith": "^4.6.2",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/rules": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.0.0.tgz",
+ "integrity": "sha512-gvg2k10I/RfvHn5I5sxvVZKM1fl72Sqrv2YY/BnM7lMHcYqO0E2jnRWoYguvBfEcZ39t+rbATlciggVe77E4zA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/ensure": "^20.0.0",
+ "@commitlint/message": "^20.0.0",
+ "@commitlint/to-lines": "^20.0.0",
+ "@commitlint/types": "^20.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/to-lines": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-20.0.0.tgz",
+ "integrity": "sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/top-level": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.0.0.tgz",
+ "integrity": "sha512-drXaPSP2EcopukrUXvUXmsQMu3Ey/FuJDc/5oiW4heoCfoE5BdLQyuc7veGeE3aoQaTVqZnh4D5WTWe2vefYKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/types": {
+ "version": "20.0.0",
+ "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.0.0.tgz",
+ "integrity": "sha512-bVUNBqG6aznYcYjTjnc3+Cat/iBgbgpflxbIBTnsHTX0YVpnmINPEkSRWymT2Q8aSH3Y7aKnEbunilkYe8TybA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/conventional-commits-parser": "^5.0.0",
+ "chalk": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/types/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -537,6 +1165,41 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/@develar/schema-utils/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@develar/schema-utils/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/@develar/schema-utils/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@electron/asar": {
"version": "3.2.18",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.18.tgz",
@@ -583,45 +1246,6 @@
"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",
@@ -644,30 +1268,50 @@
"global-agent": "^3.0.0"
}
},
- "node_modules/@electron/get/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "node_modules/@electron/get/node_modules/fs-extra": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ms": "^2.1.3"
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
},
"engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "node": ">=6 <7 || >=8"
}
},
- "node_modules/@electron/get/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "node_modules/@electron/get/node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "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/get/node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
},
"node_modules/@electron/node-gyp": {
"version": "10.2.0-electron.1",
@@ -738,47 +1382,6 @@
"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/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.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/notarize": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
@@ -794,70 +1397,6 @@
"node": ">= 10.0.0"
}
},
- "node_modules/@electron/notarize/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/@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/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@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",
@@ -880,24 +1419,6 @@
"node": ">=12.0.0"
}
},
- "node_modules/@electron/osx-sign/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/@electron/osx-sign/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -926,36 +1447,6 @@
"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/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@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",
@@ -985,24 +1476,6 @@
"node": ">=12.13.0"
}
},
- "node_modules/@electron/rebuild/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/@electron/rebuild/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -1018,77 +1491,6 @@
"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/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@electron/rebuild/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.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/universal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
@@ -1118,24 +1520,6 @@
"balanced-match": "^1.0.0"
}
},
- "node_modules/@electron/universal/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/@electron/universal/node_modules/fs-extra": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
@@ -1151,19 +1535,6 @@
"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",
@@ -1180,23 +1551,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@electron/universal/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@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",
@@ -1204,7 +1558,6 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
- "peer": true,
"dependencies": {
"cross-dirname": "^0.1.0",
"debug": "^4.3.4",
@@ -1219,26 +1572,6 @@
"node": ">=14.14"
}
},
- "node_modules/@electron/windows-sign/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
"version": "11.3.2",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
@@ -1246,7 +1579,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -1256,46 +1588,10 @@
"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/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true
- },
- "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.10",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
- "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
+ "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
"cpu": [
"ppc64"
],
@@ -1309,9 +1605,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
- "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
+ "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
"cpu": [
"arm"
],
@@ -1325,9 +1621,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
- "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
+ "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
"cpu": [
"arm64"
],
@@ -1341,9 +1637,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
- "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
+ "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
"cpu": [
"x64"
],
@@ -1357,9 +1653,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
- "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
+ "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
"cpu": [
"arm64"
],
@@ -1373,9 +1669,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
- "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
+ "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
"cpu": [
"x64"
],
@@ -1389,9 +1685,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
- "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
+ "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
"cpu": [
"arm64"
],
@@ -1405,9 +1701,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
- "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
+ "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
"cpu": [
"x64"
],
@@ -1421,9 +1717,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
- "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
+ "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
"cpu": [
"arm"
],
@@ -1437,9 +1733,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
- "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
+ "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
"cpu": [
"arm64"
],
@@ -1453,9 +1749,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
- "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
+ "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
"cpu": [
"ia32"
],
@@ -1469,9 +1765,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
- "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
+ "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
"cpu": [
"loong64"
],
@@ -1485,9 +1781,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
- "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
+ "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
"cpu": [
"mips64el"
],
@@ -1501,9 +1797,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
- "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
+ "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
"cpu": [
"ppc64"
],
@@ -1517,9 +1813,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
- "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
+ "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
"cpu": [
"riscv64"
],
@@ -1533,9 +1829,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
- "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
+ "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
"cpu": [
"s390x"
],
@@ -1549,9 +1845,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
- "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
+ "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
"cpu": [
"x64"
],
@@ -1565,9 +1861,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
- "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
+ "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
"cpu": [
"arm64"
],
@@ -1581,9 +1877,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
- "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
+ "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
"cpu": [
"x64"
],
@@ -1597,9 +1893,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
- "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
+ "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
"cpu": [
"arm64"
],
@@ -1613,9 +1909,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
- "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
+ "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
"cpu": [
"x64"
],
@@ -1629,9 +1925,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
- "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
+ "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
"cpu": [
"arm64"
],
@@ -1645,9 +1941,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
- "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
+ "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
"cpu": [
"x64"
],
@@ -1661,9 +1957,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
- "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
+ "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
"cpu": [
"arm64"
],
@@ -1677,9 +1973,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
- "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
+ "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
"cpu": [
"ia32"
],
@@ -1693,9 +1989,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
- "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
+ "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
"cpu": [
"x64"
],
@@ -1741,9 +2037,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.12.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
- "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1751,13 +2047,13 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.21.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
- "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/object-schema": "^2.1.6",
+ "@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
@@ -1765,24 +2061,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@eslint/config-array/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/@eslint/config-array/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1796,27 +2074,23 @@
"node": "*"
}
},
- "node_modules/@eslint/config-array/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@eslint/config-helpers": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
- "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
"dev": true,
"license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
- "version": "0.15.2",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
- "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1850,22 +2124,21 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ms": "^2.1.3"
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
},
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
@@ -1881,6 +2154,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1894,17 +2174,10 @@
"node": "*"
}
},
- "node_modules/@eslint/eslintrc/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@eslint/js": {
- "version": "9.36.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz",
- "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==",
+ "version": "9.39.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz",
+ "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1915,9 +2188,9 @@
}
},
"node_modules/@eslint/object-schema": {
- "version": "2.1.6",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
- "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -1925,13 +2198,13 @@
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
- "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@eslint/core": "^0.15.2",
+ "@eslint/core": "^0.17.0",
"levn": "^0.4.1"
},
"engines": {
@@ -2014,9 +2287,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz",
- "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==",
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz",
+ "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
@@ -2157,19 +2430,6 @@
"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",
@@ -2208,22 +2468,6 @@
"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",
@@ -2242,27 +2486,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
- "node_modules/@isaacs/fs-minipass": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
- "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
- "license": "ISC",
- "dependencies": {
- "minipass": "^7.0.4"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@isaacs/fs-minipass/node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -2309,10 +2532,11 @@
}
},
"node_modules/@lezer/common": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
- "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
- "license": "MIT"
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz",
+ "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==",
+ "license": "MIT",
+ "peer": true
},
"node_modules/@lezer/cpp": {
"version": "1.1.3",
@@ -2348,12 +2572,13 @@
}
},
"node_modules/@lezer/highlight": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
- "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
+ "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
- "@lezer/common": "^1.0.0"
+ "@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/html": {
@@ -2383,6 +2608,7 @@
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
@@ -2405,14 +2631,15 @@
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
"integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.3.tgz",
- "integrity": "sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==",
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.5.1.tgz",
+ "integrity": "sha512-F3ZFnIfNAOy/jPSk6Q0e3bs7e9grfK/n5zerkKoc5COH6Guy3Zb0vrJwXzdck79K16goBhYBRAvhf+ksqe0cMg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
@@ -2524,70 +2751,6 @@
"node": ">= 10.0.0"
}
},
- "node_modules/@malept/flatpak-bundler/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/@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/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@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",
@@ -2595,9 +2758,9 @@
"license": "MIT"
},
"node_modules/@monaco-editor/loader": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
- "integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz",
+ "integrity": "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==",
"license": "MIT",
"dependencies": {
"state-local": "^1.0.6"
@@ -2627,25 +2790,25 @@
}
},
"node_modules/@mux/mux-player": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.6.1.tgz",
- "integrity": "sha512-QidL9CSkRBwa49ItphuDXWtarAiskP8AG/+vj5u0LsCa+VqObQxPfxE9t5S9YO/SDYHXqDMviMpmSzotSROGUQ==",
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.8.0.tgz",
+ "integrity": "sha512-2KcJdW4BBX8JDcXpclFKaNBsqpebtaEfTzwm5lPP1Lf6y5OMILvf2tqVCOczurREVFyaEoVD71vL0I5Vvqb1dA==",
"license": "MIT",
"dependencies": {
- "@mux/mux-video": "0.27.0",
- "@mux/playback-core": "0.31.0",
- "media-chrome": "~4.14.0",
- "player.style": "^0.2.0"
+ "@mux/mux-video": "0.27.2",
+ "@mux/playback-core": "0.31.2",
+ "media-chrome": "~4.15.1",
+ "player.style": "^0.3.0"
}
},
"node_modules/@mux/mux-player-react": {
- "version": "3.6.1",
- "resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.6.1.tgz",
- "integrity": "sha512-YKIieu9GmFI73+1EcAvd63ftZ0Z9ilGbWo2dGXqQeyCEcagIN0oEcXWUPuIuxhvYB0XXsxB8RBAD8SigHkCYAQ==",
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.8.0.tgz",
+ "integrity": "sha512-c9TKtK9nsSpXOuC1LVLmmHA+Zlpcx4mzgGaA7ZlukrGMfoXWvA90ROSVAAjXRA+UKSHdLIbvNofgG3P6rEE/4Q==",
"license": "MIT",
"dependencies": {
- "@mux/mux-player": "3.6.1",
- "@mux/playback-core": "0.31.0",
+ "@mux/mux-player": "3.8.0",
+ "@mux/playback-core": "0.31.2",
"prop-types": "^15.8.1"
},
"peerDependencies": {
@@ -2663,32 +2826,32 @@
}
},
"node_modules/@mux/mux-video": {
- "version": "0.27.0",
- "resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.27.0.tgz",
- "integrity": "sha512-Oi142YAcPKrmHTG+eaWHWaE7ucMHeJwx1FXABbLM2hMGj9MQ7kYjsD5J3meFlvuyz5UeVDsPLHeUJgeBXUZovg==",
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.27.2.tgz",
+ "integrity": "sha512-VAqSw/3kS/qBzjyFSX3wClIX5Kdk6eXXlhxIJRWlClYvUKGm9ruhd7HzkwZVOJguvUh5QbGoiGWBEW2xkNIXzw==",
"license": "MIT",
"dependencies": {
"@mux/mux-data-google-ima": "0.2.8",
- "@mux/playback-core": "0.31.0",
- "castable-video": "~1.1.10",
+ "@mux/playback-core": "0.31.2",
+ "castable-video": "~1.1.11",
"custom-media-element": "~1.4.5",
"media-tracks": "~0.3.3"
}
},
"node_modules/@mux/playback-core": {
- "version": "0.31.0",
- "resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.0.tgz",
- "integrity": "sha512-VADcrtS4O6fQBH8qmgavS6h7v7amzy2oCguu1NnLaVZ3Z8WccNXcF0s7jPRoRDyXWGShgtVhypW2uXjLpkPxyw==",
+ "version": "0.31.2",
+ "resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.2.tgz",
+ "integrity": "sha512-bhOVTGAuKCQuDzNOc3XvDq7vsgqy2DAacLP0WdJciUKjfZhs3oA11NbKG7qAN6akPnZVfgn0Jn/sJN8TRjE30A==",
"license": "MIT",
"dependencies": {
- "hls.js": "~1.6.6",
+ "hls.js": "~1.6.13",
"mux-embed": "^5.8.3"
}
},
"node_modules/@napi-rs/canvas": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
- "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz",
+ "integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==",
"license": "MIT",
"optional": true,
"workspaces": [
@@ -2698,22 +2861,22 @@
"node": ">= 10"
},
"optionalDependencies": {
- "@napi-rs/canvas-android-arm64": "0.1.80",
- "@napi-rs/canvas-darwin-arm64": "0.1.80",
- "@napi-rs/canvas-darwin-x64": "0.1.80",
- "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
- "@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
- "@napi-rs/canvas-linux-arm64-musl": "0.1.80",
- "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
- "@napi-rs/canvas-linux-x64-gnu": "0.1.80",
- "@napi-rs/canvas-linux-x64-musl": "0.1.80",
- "@napi-rs/canvas-win32-x64-msvc": "0.1.80"
+ "@napi-rs/canvas-android-arm64": "0.1.81",
+ "@napi-rs/canvas-darwin-arm64": "0.1.81",
+ "@napi-rs/canvas-darwin-x64": "0.1.81",
+ "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.81",
+ "@napi-rs/canvas-linux-arm64-gnu": "0.1.81",
+ "@napi-rs/canvas-linux-arm64-musl": "0.1.81",
+ "@napi-rs/canvas-linux-riscv64-gnu": "0.1.81",
+ "@napi-rs/canvas-linux-x64-gnu": "0.1.81",
+ "@napi-rs/canvas-linux-x64-musl": "0.1.81",
+ "@napi-rs/canvas-win32-x64-msvc": "0.1.81"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
- "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.81.tgz",
+ "integrity": "sha512-78Lz+AUi+MsWupyZjXwpwQrp1QCwncPvRZrdvrROcZ9Gq9grP7LfQZiGdR8LKyHIq3OR18mDP+JESGT15V1nXw==",
"cpu": [
"arm64"
],
@@ -2727,9 +2890,9 @@
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
- "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.81.tgz",
+ "integrity": "sha512-omejuKgHWKDGoh8rsgsyhm/whwxMaryTQjJTd9zD7hiB9/rzcEEJLHnzXWR5ysy4/tTjHaQotE6k2t8eodTLnA==",
"cpu": [
"arm64"
],
@@ -2743,9 +2906,9 @@
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
- "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.81.tgz",
+ "integrity": "sha512-EYfk+co6BElq5DXNH9PBLYDYwc4QsvIVbyrsVHsxVpn4p6Y3/s8MChgC69AGqj3vzZBQ1qx2CRCMtg5cub+XuQ==",
"cpu": [
"x64"
],
@@ -2759,9 +2922,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
- "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.81.tgz",
+ "integrity": "sha512-teh6Q74CyAcH31yLNQGR9MtXSFxlZa5CI6vvNUISI14gWIJWrhOwUAOly+KRe1aztWR0FWTVSPxM4p5y+06aow==",
"cpu": [
"arm"
],
@@ -2775,9 +2938,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
- "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.81.tgz",
+ "integrity": "sha512-AGEopHFYRzJOjxY+2G1RmHPRnuWvO3Qdhq7sIazlSjxb3Z6dZHg7OB/4ZimXaimPjDACm9qWa6t5bn9bhXvkcw==",
"cpu": [
"arm64"
],
@@ -2791,9 +2954,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
- "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.81.tgz",
+ "integrity": "sha512-Bj3m1cl4GIhsigkdwOxii4g4Ump3/QhNpx85IgAlCCYXpaly6mcsWpuDYEabfIGWOWhDUNBOndaQUPfWK1czOQ==",
"cpu": [
"arm64"
],
@@ -2807,9 +2970,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
- "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.81.tgz",
+ "integrity": "sha512-yg/5NkHykVdwPlD3XObwCa/EswkOwLHswJcI9rHrac+znHsmCSj5AMX/RTU9Z9F6lZTwL60JM2Esit33XhAMiw==",
"cpu": [
"riscv64"
],
@@ -2823,9 +2986,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
- "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.81.tgz",
+ "integrity": "sha512-tPfMpSEBuV5dJSKexO/UZxpOqnYTaNbG8aKa1ek8QsWu+4SJ/foWkaxscra/RUv85vepx6WWDjzBNbNJsTnO0w==",
"cpu": [
"x64"
],
@@ -2839,9 +3002,9 @@
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
- "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.81.tgz",
+ "integrity": "sha512-1L0xnYgzqn8Baef+inPvY4dKqdmw3KCBoe0NEDgezuBZN7MA5xElwifoG8609uNdrMtJ9J6QZarsslLRVqri7g==",
"cpu": [
"x64"
],
@@ -2855,9 +3018,9 @@
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
- "version": "0.1.80",
- "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
- "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
+ "version": "0.1.81",
+ "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.81.tgz",
+ "integrity": "sha512-57ryVbhm/z7RE9/UVcS7mrLPdlayLesy+9U0Uf6epCoeSGrs99tfieCcgZWFbIgmByQ1AZnNtFI2N6huqDLlWQ==",
"cpu": [
"x64"
],
@@ -2922,19 +3085,6 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
- "node_modules/@npmcli/fs/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@npmcli/move-file": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz",
@@ -3663,6 +3813,39 @@
}
}
},
+ "node_modules/@radix-ui/react-slider": {
+ "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.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-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "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-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@@ -3939,6 +4122,32 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.2.tgz",
+ "integrity": "sha512-ZAYu/NXkl/OhqTz7rfPaAhY0+e8Fr15jqNxte/2exKUxvHyQ/hcqmdekiN1f+Lcw3pE+34FCgX+26zcUE3duCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
"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",
@@ -3993,9 +4202,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz",
- "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
+ "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"cpu": [
"arm"
],
@@ -4006,9 +4215,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz",
- "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
+ "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"cpu": [
"arm64"
],
@@ -4019,9 +4228,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz",
- "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
+ "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"cpu": [
"arm64"
],
@@ -4032,9 +4241,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz",
- "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
+ "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"cpu": [
"x64"
],
@@ -4045,9 +4254,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz",
- "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
+ "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"cpu": [
"arm64"
],
@@ -4058,9 +4267,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz",
- "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
+ "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"cpu": [
"x64"
],
@@ -4071,9 +4280,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz",
- "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
+ "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"cpu": [
"arm"
],
@@ -4084,9 +4293,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz",
- "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
+ "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"cpu": [
"arm"
],
@@ -4097,9 +4306,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz",
- "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
+ "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"cpu": [
"arm64"
],
@@ -4110,9 +4319,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz",
- "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
+ "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"cpu": [
"arm64"
],
@@ -4123,9 +4332,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz",
- "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
+ "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"cpu": [
"loong64"
],
@@ -4136,9 +4345,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz",
- "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
+ "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"cpu": [
"ppc64"
],
@@ -4149,9 +4358,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz",
- "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
+ "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"cpu": [
"riscv64"
],
@@ -4162,9 +4371,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz",
- "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
+ "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"cpu": [
"riscv64"
],
@@ -4175,9 +4384,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz",
- "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
+ "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"cpu": [
"s390x"
],
@@ -4188,9 +4397,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz",
- "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
+ "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"cpu": [
"x64"
],
@@ -4201,9 +4410,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz",
- "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
+ "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"cpu": [
"x64"
],
@@ -4214,9 +4423,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz",
- "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
+ "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"cpu": [
"arm64"
],
@@ -4227,9 +4436,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz",
- "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
+ "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"cpu": [
"arm64"
],
@@ -4240,9 +4449,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz",
- "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
+ "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"cpu": [
"ia32"
],
@@ -4253,9 +4462,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz",
- "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
+ "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"cpu": [
"x64"
],
@@ -4266,9 +4475,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz",
- "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
+ "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"cpu": [
"x64"
],
@@ -4309,232 +4518,6 @@
"integrity": "sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==",
"license": "Apache-2.0"
},
- "node_modules/@swc/core": {
- "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.24"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/swc"
- },
- "optionalDependencies": {
- "@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"
- },
- "peerDependenciesMeta": {
- "@swc/helpers": {
- "optional": true
- }
- }
- },
- "node_modules/@swc/core-darwin-arm64": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-darwin-x64": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-arm-gnueabihf": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-arm64-gnu": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-arm64-musl": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-x64-gnu": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-linux-x64-musl": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-win32-arm64-msvc": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-win32-ia32-msvc": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/core-win32-x64-msvc": {
- "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"
- ],
- "dev": true,
- "license": "Apache-2.0 AND MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@swc/counter": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
- "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/@swc/types": {
- "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/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@@ -4549,52 +4532,47 @@
}
},
"node_modules/@tailwindcss/node": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz",
- "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
+ "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
- "jiti": "^2.6.0",
- "lightningcss": "1.30.1",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.30.2",
"magic-string": "^0.30.19",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.1.14"
+ "tailwindcss": "4.1.16"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz",
- "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==",
- "hasInstallScript": true,
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
+ "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
"license": "MIT",
- "dependencies": {
- "detect-libc": "^2.0.4",
- "tar": "^7.5.1"
- },
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.1.14",
- "@tailwindcss/oxide-darwin-arm64": "4.1.14",
- "@tailwindcss/oxide-darwin-x64": "4.1.14",
- "@tailwindcss/oxide-freebsd-x64": "4.1.14",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14",
- "@tailwindcss/oxide-linux-arm64-musl": "4.1.14",
- "@tailwindcss/oxide-linux-x64-gnu": "4.1.14",
- "@tailwindcss/oxide-linux-x64-musl": "4.1.14",
- "@tailwindcss/oxide-wasm32-wasi": "4.1.14",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14",
- "@tailwindcss/oxide-win32-x64-msvc": "4.1.14"
+ "@tailwindcss/oxide-android-arm64": "4.1.16",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.16",
+ "@tailwindcss/oxide-darwin-x64": "4.1.16",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.16",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.16",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.16",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz",
- "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
+ "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
"cpu": [
"arm64"
],
@@ -4608,9 +4586,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz",
- "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
+ "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
"cpu": [
"arm64"
],
@@ -4624,9 +4602,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz",
- "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
+ "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
"cpu": [
"x64"
],
@@ -4640,9 +4618,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz",
- "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
+ "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
"cpu": [
"x64"
],
@@ -4656,9 +4634,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz",
- "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
+ "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
"cpu": [
"arm"
],
@@ -4672,9 +4650,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz",
- "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
+ "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
"cpu": [
"arm64"
],
@@ -4688,9 +4666,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz",
- "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
+ "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
"cpu": [
"arm64"
],
@@ -4704,9 +4682,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz",
- "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
+ "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
"cpu": [
"x64"
],
@@ -4720,9 +4698,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz",
- "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
+ "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
"cpu": [
"x64"
],
@@ -4736,9 +4714,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz",
- "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
+ "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -4756,7 +4734,7 @@
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@emnapi/wasi-threads": "^1.1.0",
- "@napi-rs/wasm-runtime": "^1.0.5",
+ "@napi-rs/wasm-runtime": "^1.0.7",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
},
@@ -4765,9 +4743,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz",
- "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
+ "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
"cpu": [
"arm64"
],
@@ -4781,9 +4759,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz",
- "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
+ "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
"cpu": [
"x64"
],
@@ -4797,14 +4775,14 @@
}
},
"node_modules/@tailwindcss/vite": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz",
- "integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz",
+ "integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==",
"license": "MIT",
"dependencies": {
- "@tailwindcss/node": "4.1.14",
- "@tailwindcss/oxide": "4.1.14",
- "tailwindcss": "4.1.14"
+ "@tailwindcss/node": "4.1.16",
+ "@tailwindcss/oxide": "4.1.16",
+ "tailwindcss": "4.1.16"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
@@ -4820,6 +4798,51 @@
"node": ">= 10"
}
},
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
"node_modules/@types/bcryptjs": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
@@ -4832,6 +4855,7 @@
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/node": "*"
}
@@ -4868,10 +4892,20 @@
"@types/node": "*"
}
},
+ "node_modules/@types/conventional-commits-parser": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz",
+ "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/cookie-parser": {
- "version": "1.4.9",
- "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz",
- "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==",
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
+ "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
@@ -4887,6 +4921,69 @@
"@types/node": "*"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -4912,20 +5009,21 @@
}
},
"node_modules/@types/express": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
- "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz",
+ "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
- "@types/serve-static": "*"
+ "@types/serve-static": "^1"
}
},
"node_modules/@types/express-serve-static-core": {
- "version": "5.0.7",
- "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
- "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
+ "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -5034,12 +5132,13 @@
}
},
"node_modules/@types/node": {
- "version": "24.6.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.1.tgz",
- "integrity": "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==",
+ "version": "24.9.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
+ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
- "undici-types": "~7.13.0"
+ "undici-types": "~7.16.0"
}
},
"node_modules/@types/plist": {
@@ -5055,9 +5154,9 @@
}
},
"node_modules/@types/qrcode": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
- "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
+ "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -5076,22 +5175,24 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "19.1.17",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz",
- "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==",
+ "version": "19.2.2",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
+ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
- "version": "19.1.11",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.11.tgz",
- "integrity": "sha512-3BKc/yGdNTYQVVw4idqHtSOcFsgGuBbMveKCOgF8wQ5QtrYOc3jDIlzg3jef04zcXFIHLelyGlj0T+BJ8+KN+w==",
+ "version": "19.2.2",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
+ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
- "@types/react": "^19.0.0"
+ "@types/react": "^19.2.0"
}
},
"node_modules/@types/responselike": {
@@ -5105,24 +5206,33 @@
}
},
"node_modules/@types/send": {
- "version": "0.17.5",
- "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
- "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
"license": "MIT",
"dependencies": {
- "@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
- "version": "1.15.8",
- "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
- "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
- "@types/send": "*"
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
}
},
"node_modules/@types/speakeasy": {
@@ -5145,9 +5255,9 @@
}
},
"node_modules/@types/ssh2/node_modules/@types/node": {
- "version": "18.19.129",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz",
- "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==",
+ "version": "18.19.130",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5161,19 +5271,18 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/trusted-types": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
- "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==",
- "license": "MIT",
- "peer": true
- },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@types/verror": {
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
@@ -5204,17 +5313,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
- "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
+ "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.45.0",
- "@typescript-eslint/type-utils": "8.45.0",
- "@typescript-eslint/utils": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0",
+ "@typescript-eslint/scope-manager": "8.46.2",
+ "@typescript-eslint/type-utils": "8.46.2",
+ "@typescript-eslint/utils": "8.46.2",
+ "@typescript-eslint/visitor-keys": "8.46.2",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -5228,7 +5337,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.45.0",
+ "@typescript-eslint/parser": "^8.46.2",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@@ -5244,16 +5353,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz",
- "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
+ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "8.45.0",
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/typescript-estree": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0",
+ "@typescript-eslint/scope-manager": "8.46.2",
+ "@typescript-eslint/types": "8.46.2",
+ "@typescript-eslint/typescript-estree": "8.46.2",
+ "@typescript-eslint/visitor-keys": "8.46.2",
"debug": "^4.3.4"
},
"engines": {
@@ -5268,40 +5378,15 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@typescript-eslint/parser/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/parser/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@typescript-eslint/project-service": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz",
- "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
+ "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.45.0",
- "@typescript-eslint/types": "^8.45.0",
+ "@typescript-eslint/tsconfig-utils": "^8.46.2",
+ "@typescript-eslint/types": "^8.46.2",
"debug": "^4.3.4"
},
"engines": {
@@ -5315,40 +5400,15 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@typescript-eslint/project-service/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/project-service/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz",
- "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
+ "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0"
+ "@typescript-eslint/types": "8.46.2",
+ "@typescript-eslint/visitor-keys": "8.46.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5359,9 +5419,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz",
- "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
+ "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5376,15 +5436,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz",
- "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
+ "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/typescript-estree": "8.45.0",
- "@typescript-eslint/utils": "8.45.0",
+ "@typescript-eslint/types": "8.46.2",
+ "@typescript-eslint/typescript-estree": "8.46.2",
+ "@typescript-eslint/utils": "8.46.2",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -5400,35 +5460,10 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@typescript-eslint/type-utils/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/@typescript-eslint/type-utils/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@typescript-eslint/types": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz",
- "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
+ "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5440,16 +5475,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz",
- "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
+ "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.45.0",
- "@typescript-eslint/tsconfig-utils": "8.45.0",
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/visitor-keys": "8.45.0",
+ "@typescript-eslint/project-service": "8.46.2",
+ "@typescript-eslint/tsconfig-utils": "8.46.2",
+ "@typescript-eslint/types": "8.46.2",
+ "@typescript-eslint/visitor-keys": "8.46.2",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -5478,24 +5513,6 @@
"balanced-match": "^1.0.0"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -5512,37 +5529,17 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@typescript-eslint/utils": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz",
- "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
+ "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.45.0",
- "@typescript-eslint/types": "8.45.0",
- "@typescript-eslint/typescript-estree": "8.45.0"
+ "@typescript-eslint/scope-manager": "8.46.2",
+ "@typescript-eslint/types": "8.46.2",
+ "@typescript-eslint/typescript-estree": "8.46.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5557,13 +5554,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz",
- "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
+ "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.45.0",
+ "@typescript-eslint/types": "8.46.2",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -5664,18 +5661,25 @@
"weakmap-polyfill": "2.0.4"
}
},
- "node_modules/@vitejs/plugin-react-swc": {
- "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==",
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.27",
- "@swc/core": "^1.12.11"
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
- "vite": "^4 || ^5 || ^6 || ^7"
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@xmldom/xmldom": {
@@ -5731,7 +5735,8 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/7zip-bin": {
"version": "5.2.0",
@@ -5766,6 +5771,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5821,39 +5827,49 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"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==",
+ "node_modules/ansi-escapes": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz",
+ "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==",
"dev": true,
"license": "MIT",
- "peerDependencies": {
- "ajv": "^6.9.1"
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "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": ">=8"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
@@ -5927,24 +5943,6 @@
"electron-builder-squirrel-windows": "26.0.12"
}
},
- "node_modules/app-builder-lib/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/app-builder-lib/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -5973,77 +5971,6 @@
"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/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/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/app-builder-lib/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.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/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
@@ -6069,6 +5996,13 @@
"node": ">=10"
}
},
+ "node_modules/array-ify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz",
+ "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
@@ -6134,9 +6068,9 @@
}
},
"node_modules/axios": {
- "version": "1.12.2",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
- "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
+ "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -6187,6 +6121,16 @@
],
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.22",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.22.tgz",
+ "integrity": "sha512-/tk9kky/d8T8CTXIQYASLyhAxR5VwL3zct1oAoVTaOUHwrmsGnfbRwNdEq+vOl2BN8i3PcDdP0o4Q+jjKQoFbQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
"node_modules/bcp-47": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz",
@@ -6250,6 +6194,7 @@
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
@@ -6316,6 +6261,21 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/boolean": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
@@ -6349,6 +6309,41 @@
"node": ">=8"
}
},
+ "node_modules/browserslist": {
+ "version": "4.27.0",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
+ "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.19",
+ "caniuse-lite": "^1.0.30001751",
+ "electron-to-chromium": "^1.5.238",
+ "node-releases": "^2.0.26",
+ "update-browserslist-db": "^1.1.4"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -6444,49 +6439,6 @@
"node": ">=12.0.0"
}
},
- "node_modules/builder-util-runtime/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/builder-util-runtime/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/builder-util/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/builder-util/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -6502,36 +6454,6 @@
"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/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/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",
@@ -6636,34 +6558,6 @@
"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/cacheable-lookup": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
@@ -6741,6 +6635,27 @@
"node": ">=6"
}
},
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001752",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001752.tgz",
+ "integrity": "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
"node_modules/castable-video": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.1.11.tgz",
@@ -6881,16 +6796,19 @@
}
},
"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==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "restore-cursor": "^3.1.0"
+ "restore-cursor": "^5.0.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-spinners": {
@@ -6939,6 +6857,47 @@
"node": ">=12"
}
},
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/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/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
@@ -7027,6 +6986,13 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -7059,6 +7025,17 @@
"node": ">= 6"
}
},
+ "node_modules/compare-func": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz",
+ "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-ify": "^1.0.0",
+ "dot-prop": "^5.1.0"
+ }
+ },
"node_modules/compare-version": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
@@ -7235,6 +7212,58 @@
"node": ">= 0.6"
}
},
+ "node_modules/conventional-changelog-angular": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz",
+ "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/conventional-changelog-conventionalcommits": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz",
+ "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/conventional-commits-parser": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
+ "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-text-path": "^2.0.0",
+ "JSONStream": "^1.3.5",
+ "meow": "^12.0.1",
+ "split2": "^4.0.0"
+ },
+ "bin": {
+ "conventional-commits-parser": "cli.mjs"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
@@ -7282,6 +7311,52 @@
"node": ">= 0.10"
}
},
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cosmiconfig-typescript-loader": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz",
+ "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jiti": "^2.6.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "cosmiconfig": ">=9",
+ "typescript": ">=5"
+ }
+ },
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
@@ -7319,8 +7394,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true,
"license": "MIT",
- "optional": true,
- "peer": true
+ "optional": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@@ -7349,6 +7423,140 @@
"integrity": "sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==",
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dargs": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz",
+ "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/dash-video-element": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/dash-video-element/-/dash-video-element-0.2.0.tgz",
@@ -7388,12 +7596,20 @@
}
},
"node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
- "ms": "2.0.0"
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
}
},
"node_modules/decamelize": {
@@ -7405,6 +7621,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/decode-named-character-reference": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
@@ -7560,9 +7782,9 @@
}
},
"node_modules/detect-libc": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
- "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -7631,6 +7853,7 @@
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"app-builder-lib": "26.0.12",
"builder-util": "26.0.11",
@@ -7671,29 +7894,6 @@
"node": ">=0.10.0"
}
},
- "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",
@@ -7721,6 +7921,51 @@
"node": ">=8"
}
},
+ "node_modules/dmg-license/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/dmg-license/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/dompurify": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
+ "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)"
+ },
+ "node_modules/dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
@@ -7763,9 +8008,9 @@
}
},
"node_modules/drizzle-orm": {
- "version": "0.44.5",
- "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz",
- "integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==",
+ "version": "0.44.7",
+ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz",
+ "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
@@ -7940,9 +8185,9 @@
}
},
"node_modules/electron": {
- "version": "38.2.0",
- "resolved": "https://registry.npmjs.org/electron/-/electron-38.2.0.tgz",
- "integrity": "sha512-Cw5Mb+N5NxsG0Hc1qr8I65Kt5APRrbgTtEEn3zTod30UNJRnAE1xbGk/1NOaDn3ODzI/MYn6BzT9T9zreP7xWA==",
+ "version": "38.5.0",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-38.5.0.tgz",
+ "integrity": "sha512-dbC7V+eZweerYMJfxQldzHOg37a1VdNMCKxrJxlkp3cA30gOXtXSg4ZYs07L5+QwI19WOy1uyvtEUgbw1RRsCQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -8012,29 +8257,6 @@
"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-publish": {
"version": "26.0.11",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.0.11.tgz",
@@ -8067,28 +8289,12 @@
"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==",
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.244",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz",
+ "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==",
"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"
- }
+ "license": "ISC"
},
"node_modules/electron-winstaller": {
"version": "5.4.0",
@@ -8097,7 +8303,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@electron/asar": "^3.2.1",
"debug": "^4.1.1",
@@ -8112,32 +8317,12 @@
"@electron/windows-sign": "^1.1.2"
}
},
- "node_modules/electron-winstaller/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"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",
@@ -8147,18 +8332,30 @@
"node": ">=6 <7 || >=8"
}
},
- "node_modules/electron-winstaller/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "node_modules/electron-winstaller/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",
- "peer": true
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/electron-winstaller/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/electron/node_modules/@types/node": {
- "version": "22.18.8",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.8.tgz",
- "integrity": "sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==",
+ "version": "22.18.13",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz",
+ "integrity": "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8244,6 +8441,19 @@
"node": ">=6"
}
},
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/err-code": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
@@ -8251,6 +8461,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "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",
@@ -8296,6 +8516,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.41.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz",
+ "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -8305,9 +8535,9 @@
"optional": true
},
"node_modules/esbuild": {
- "version": "0.25.10",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
- "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
+ "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -8317,32 +8547,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.10",
- "@esbuild/android-arm": "0.25.10",
- "@esbuild/android-arm64": "0.25.10",
- "@esbuild/android-x64": "0.25.10",
- "@esbuild/darwin-arm64": "0.25.10",
- "@esbuild/darwin-x64": "0.25.10",
- "@esbuild/freebsd-arm64": "0.25.10",
- "@esbuild/freebsd-x64": "0.25.10",
- "@esbuild/linux-arm": "0.25.10",
- "@esbuild/linux-arm64": "0.25.10",
- "@esbuild/linux-ia32": "0.25.10",
- "@esbuild/linux-loong64": "0.25.10",
- "@esbuild/linux-mips64el": "0.25.10",
- "@esbuild/linux-ppc64": "0.25.10",
- "@esbuild/linux-riscv64": "0.25.10",
- "@esbuild/linux-s390x": "0.25.10",
- "@esbuild/linux-x64": "0.25.10",
- "@esbuild/netbsd-arm64": "0.25.10",
- "@esbuild/netbsd-x64": "0.25.10",
- "@esbuild/openbsd-arm64": "0.25.10",
- "@esbuild/openbsd-x64": "0.25.10",
- "@esbuild/openharmony-arm64": "0.25.10",
- "@esbuild/sunos-x64": "0.25.10",
- "@esbuild/win32-arm64": "0.25.10",
- "@esbuild/win32-ia32": "0.25.10",
- "@esbuild/win32-x64": "0.25.10"
+ "@esbuild/aix-ppc64": "0.25.11",
+ "@esbuild/android-arm": "0.25.11",
+ "@esbuild/android-arm64": "0.25.11",
+ "@esbuild/android-x64": "0.25.11",
+ "@esbuild/darwin-arm64": "0.25.11",
+ "@esbuild/darwin-x64": "0.25.11",
+ "@esbuild/freebsd-arm64": "0.25.11",
+ "@esbuild/freebsd-x64": "0.25.11",
+ "@esbuild/linux-arm": "0.25.11",
+ "@esbuild/linux-arm64": "0.25.11",
+ "@esbuild/linux-ia32": "0.25.11",
+ "@esbuild/linux-loong64": "0.25.11",
+ "@esbuild/linux-mips64el": "0.25.11",
+ "@esbuild/linux-ppc64": "0.25.11",
+ "@esbuild/linux-riscv64": "0.25.11",
+ "@esbuild/linux-s390x": "0.25.11",
+ "@esbuild/linux-x64": "0.25.11",
+ "@esbuild/netbsd-arm64": "0.25.11",
+ "@esbuild/netbsd-x64": "0.25.11",
+ "@esbuild/openbsd-arm64": "0.25.11",
+ "@esbuild/openbsd-x64": "0.25.11",
+ "@esbuild/openharmony-arm64": "0.25.11",
+ "@esbuild/sunos-x64": "0.25.11",
+ "@esbuild/win32-arm64": "0.25.11",
+ "@esbuild/win32-ia32": "0.25.11",
+ "@esbuild/win32-x64": "0.25.11"
}
},
"node_modules/escalade": {
@@ -8375,25 +8605,25 @@
}
},
"node_modules/eslint": {
- "version": "9.36.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
- "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
+ "version": "9.39.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz",
+ "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@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/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.36.0",
- "@eslint/plugin-kit": "^0.3.5",
+ "@eslint/js": "9.39.0",
+ "@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
- "@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
@@ -8449,9 +8679,9 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.22",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.22.tgz",
- "integrity": "sha512-atkAG6QaJMGoTLc4MDAP+rqZcfwQuTIh2IqHWFLy2TEjxr0MOK+5BSG4RzL2564AAPpZkDRsZXAUz68kjnU6Ug==",
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
+ "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -8488,22 +8718,61 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/eslint/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ms": "^2.1.3"
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/eslint/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
},
"engines": {
- "node": ">=6.0"
+ "node": ">=10"
},
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/minimatch": {
@@ -8519,12 +8788,31 @@
"node": "*"
}
},
- "node_modules/eslint/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "node_modules/eslint/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
},
"node_modules/espree": {
"version": "10.4.0",
@@ -8609,6 +8897,12 @@
"node": ">= 0.6"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "license": "MIT"
+ },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -8619,9 +8913,9 @@
}
},
"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==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
+ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
"dev": true,
"license": "Apache-2.0"
},
@@ -8696,23 +8990,6 @@
"node": ">=6.6.0"
}
},
- "node_modules/express/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/express/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -8734,12 +9011,6 @@
"node": ">= 0.8"
}
},
- "node_modules/express/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/express/node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -8827,31 +9098,6 @@
"@types/yauzl": "^2.9.1"
}
},
- "node_modules/extract-zip/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/extract-zip/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/extsprintf": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
@@ -8913,6 +9159,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -9051,41 +9314,19 @@
"node": ">= 0.8"
}
},
- "node_modules/finalhandler/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/finalhandler/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz",
+ "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
+ "locate-path": "^7.2.0",
+ "path-exists": "^5.0.0",
+ "unicorn-magic": "^0.1.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -9149,19 +9390,6 @@
"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/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
@@ -9244,18 +9472,19 @@
"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==",
+ "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": "^4.0.0",
- "universalify": "^0.1.0"
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
},
"engines": {
- "node": ">=6 <7 || >=8"
+ "node": ">=10"
}
},
"node_modules/fs-minipass": {
@@ -9301,6 +9530,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.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",
@@ -9310,6 +9549,19 @@
"node": "6.* || 8.* || >= 10.*"
}
},
+ "node_modules/get-east-asian-width": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+ "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -9372,6 +9624,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/git-raw-commits": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz",
+ "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dargs": "^8.0.0",
+ "meow": "^12.0.1",
+ "split2": "^4.0.0"
+ },
+ "bin": {
+ "git-raw-commits": "cli.mjs"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -9445,18 +9715,20 @@
"node": ">=10.0"
}
},
- "node_modules/global-agent/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "node_modules/global-directory": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
+ "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
"dev": true,
- "license": "ISC",
- "optional": true,
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "dependencies": {
+ "ini": "4.1.1"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globals": {
@@ -9745,9 +10017,9 @@
}
},
"node_modules/hls.js": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.13.tgz",
- "integrity": "sha512-hNEzjZNHf5bFrUNvdS4/1RjIanuJ6szpWNfTaX5I6WfGynWXGT7K/YQLYtemSvFExzeMdgdE4SsyVLJbd5PcZA==",
+ "version": "1.6.14",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz",
+ "integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
"license": "Apache-2.0"
},
"node_modules/hosted-git-info": {
@@ -9763,6 +10035,26 @@
"node": ">=10"
}
},
+ "node_modules/hosted-git-info/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/hosted-git-info/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/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
@@ -9844,31 +10136,6 @@
"node": ">= 14"
}
},
- "node_modules/http-proxy-agent/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/http-proxy-agent/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
@@ -9897,31 +10164,6 @@
"node": ">= 14"
}
},
- "node_modules/https-proxy-agent/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/https-proxy-agent/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@@ -9932,10 +10174,26 @@
"ms": "^2.0.0"
}
},
+ "node_modules/husky": {
+ "version": "9.1.7",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
+ "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "husky": "bin.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
"node_modules/i18next": {
- "version": "25.5.3",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz",
- "integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==",
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz",
+ "integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==",
"funding": [
{
"type": "individual",
@@ -9951,6 +10209,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -10038,6 +10297,16 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -10055,6 +10324,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/imsc": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz",
@@ -10116,10 +10406,14 @@
"license": "ISC"
},
"node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
+ "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
},
"node_modules/inline-style-parser": {
"version": "0.2.4",
@@ -10127,6 +10421,15 @@
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
"license": "MIT"
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ip-address": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
@@ -10170,6 +10473,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "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",
@@ -10262,6 +10572,16 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
@@ -10280,6 +10600,19 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
+ "node_modules/is-text-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz",
+ "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "text-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -10414,6 +10747,19 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -10421,10 +10767,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
@@ -10457,15 +10810,45 @@
}
},
"node_modules/jsonfile": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
- "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "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/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+ "dev": true,
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/JSONStream": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
+ "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+ "dev": true,
+ "license": "(MIT OR Apache-2.0)",
+ "dependencies": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ },
+ "bin": {
+ "JSONStream": "bin.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -10488,24 +10871,6 @@
"npm": ">=6"
}
},
- "node_modules/jsonwebtoken/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/jsonwebtoken/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -10580,9 +10945,9 @@
}
},
"node_modules/lightningcss": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
- "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
+ "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -10595,22 +10960,43 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
- "lightningcss-darwin-arm64": "1.30.1",
- "lightningcss-darwin-x64": "1.30.1",
- "lightningcss-freebsd-x64": "1.30.1",
- "lightningcss-linux-arm-gnueabihf": "1.30.1",
- "lightningcss-linux-arm64-gnu": "1.30.1",
- "lightningcss-linux-arm64-musl": "1.30.1",
- "lightningcss-linux-x64-gnu": "1.30.1",
- "lightningcss-linux-x64-musl": "1.30.1",
- "lightningcss-win32-arm64-msvc": "1.30.1",
- "lightningcss-win32-x64-msvc": "1.30.1"
+ "lightningcss-android-arm64": "1.30.2",
+ "lightningcss-darwin-arm64": "1.30.2",
+ "lightningcss-darwin-x64": "1.30.2",
+ "lightningcss-freebsd-x64": "1.30.2",
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
+ "lightningcss-linux-arm64-gnu": "1.30.2",
+ "lightningcss-linux-arm64-musl": "1.30.2",
+ "lightningcss-linux-x64-gnu": "1.30.2",
+ "lightningcss-linux-x64-musl": "1.30.2",
+ "lightningcss-win32-arm64-msvc": "1.30.2",
+ "lightningcss-win32-x64-msvc": "1.30.2"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
- "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [
"arm64"
],
@@ -10628,9 +11014,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
- "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
+ "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"cpu": [
"x64"
],
@@ -10648,9 +11034,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
- "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"cpu": [
"x64"
],
@@ -10668,9 +11054,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
- "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"cpu": [
"arm"
],
@@ -10688,9 +11074,9 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
- "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"cpu": [
"arm64"
],
@@ -10708,9 +11094,9 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
- "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"cpu": [
"arm64"
],
@@ -10728,9 +11114,9 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
- "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"cpu": [
"x64"
],
@@ -10748,9 +11134,9 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
- "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"cpu": [
"x64"
],
@@ -10768,9 +11154,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
- "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"cpu": [
"arm64"
],
@@ -10788,9 +11174,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
- "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"cpu": [
"x64"
],
@@ -10807,6 +11193,146 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lint-staged": {
+ "version": "16.2.6",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz",
+ "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^14.0.1",
+ "listr2": "^9.0.5",
+ "micromatch": "^4.0.8",
+ "nano-spawn": "^2.0.0",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.2",
+ "yaml": "^2.8.1"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/commander": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
+ "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
+ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^5.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/listr2/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/listr2/node_modules/cli-truncate": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
+ "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^7.1.0",
+ "string-width": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/slice-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+ "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
+ "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
@@ -10826,16 +11352,16 @@
}
},
"node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
+ "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "p-locate": "^5.0.0"
+ "p-locate": "^6.0.0"
},
"engines": {
- "node": ">=10"
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -10847,6 +11373,13 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -10883,6 +11416,13 @@
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
+ "node_modules/lodash.kebabcase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+ "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -10890,12 +11430,47 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
+ "node_modules/lodash.snakecase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.startcase": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
+ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.upperfirst": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -10913,6 +11488,72 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/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/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+ "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+ "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
"node_modules/longest-streak": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
@@ -10960,16 +11601,13 @@
}
},
"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==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"license": "ISC",
"dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
+ "yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
@@ -10982,9 +11620,9 @@
}
},
"node_modules/magic-string": {
- "version": "0.30.19",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
- "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -11049,24 +11687,6 @@
"node": ">= 6.0.0"
}
},
- "node_modules/make-fetch-happen/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/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",
@@ -11106,13 +11726,6 @@
"node": ">=12"
}
},
- "node_modules/make-fetch-happen/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/make-fetch-happen/node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
@@ -11133,6 +11746,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/matcher": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -11439,9 +12064,9 @@
}
},
"node_modules/media-chrome": {
- "version": "4.14.0",
- "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.14.0.tgz",
- "integrity": "sha512-IEdFb4blyF15vLvQzLIn6USJBv7Kf2ne+TfLQKBYI5Z0f9VEBVZz5MKy4Uhi0iA9lStl2S9ENIujJRuJIa5OiA==",
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.15.1.tgz",
+ "integrity": "sha512-Hxqr0qQ67ewmRaLJBqe5ayu53txFX+DODb9xBSHgTbw7j+gITGZ4llbPPEmqMlDnatw7IsF+AUh9rJYbpnn4ZQ==",
"license": "MIT",
"dependencies": {
"ce-la-react": "^0.3.0"
@@ -11462,6 +12087,19 @@
"node": ">= 0.6"
}
},
+ "node_modules/meow": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz",
+ "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
@@ -12064,29 +12702,6 @@
],
"license": "MIT"
},
- "node_modules/micromark/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/micromark/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -12145,6 +12760,19 @@
"node": ">=6"
}
},
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@@ -12156,11 +12784,11 @@
}
},
"node_modules/minimatch": {
- "version": "10.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
- "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
@@ -12263,6 +12891,13 @@
"node": ">=8"
}
},
+ "node_modules/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/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
@@ -12277,6 +12912,13 @@
"node": ">= 8"
}
},
+ "node_modules/minizlib/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/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -12297,19 +12939,20 @@
"license": "MIT"
},
"node_modules/monaco-editor": {
- "version": "0.53.0",
- "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz",
- "integrity": "sha512-0WNThgC6CMWNXXBxTbaYYcunj08iB5rnx4/G56UOPeL9UVIUGGHA1GR0EWIh9Ebabj7NpCRawQ5b0hfN1jQmYQ==",
+ "version": "0.54.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz",
+ "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==",
"license": "MIT",
"peer": true,
"dependencies": {
- "@types/trusted-types": "^1.0.6"
+ "dompurify": "3.1.7",
+ "marked": "14.0.0"
}
},
"node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/multer": {
@@ -12355,6 +12998,19 @@
"license": "MIT",
"optional": true
},
+ "node_modules/nano-spawn": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
+ "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1"
+ }
+ },
"node_modules/nanoid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
@@ -12412,9 +13068,9 @@
}
},
"node_modules/node-abi": {
- "version": "3.77.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
- "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==",
+ "version": "3.80.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz",
+ "integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
@@ -12423,18 +13079,6 @@
"node": ">=10"
}
},
- "node_modules/node-abi/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "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",
@@ -12453,19 +13097,6 @@
"semver": "^7.3.5"
}
},
- "node_modules/node-api-version/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -12504,6 +13135,13 @@
"url": "https://opencollective.com/node-fetch"
}
},
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/nopt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",
@@ -12587,16 +13225,16 @@
}
},
"node_modules/onetime": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "mimic-fn": "^2.1.0"
+ "mimic-function": "^5.0.0"
},
"engines": {
- "node": ">=6"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -12644,6 +13282,79 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/ora/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ora/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/ora/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/ora/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/ora/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/ora/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/p-cancelable": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
@@ -12671,16 +13382,45 @@
}
},
"node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
+ "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "p-limit": "^3.0.2"
+ "p-limit": "^4.0.0"
},
"engines": {
- "node": ">=10"
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
+ "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/yocto-queue": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz",
+ "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@@ -12762,6 +13502,25 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -12778,12 +13537,13 @@
"license": "MIT"
},
"node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
+ "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=8"
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/path-is-absolute": {
@@ -12851,15 +13611,15 @@
}
},
"node_modules/pdfjs-dist": {
- "version": "5.3.93",
- "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.93.tgz",
- "integrity": "sha512-w3fQKVL1oGn8FRyx5JUG5tnbblggDqyx2XzA5brsJ5hSuS+I0NdnJANhmeWKLjotdbPQucLBug5t0MeWr0AAdg==",
+ "version": "5.4.296",
+ "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz",
+ "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"
},
"optionalDependencies": {
- "@napi-rs/canvas": "^0.1.71"
+ "@napi-rs/canvas": "^0.1.80"
}
},
"node_modules/pe-library": {
@@ -12903,10 +13663,23 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/player.style": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/player.style/-/player.style-0.2.0.tgz",
- "integrity": "sha512-Ngoaz49TClptMr8HDA2IFmjT3Iq6R27QEUH/C+On33L59RSF3dCLefBYB1Au2RDZQJ6oVFpc1sXaPVpp7fEzzA==",
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/player.style/-/player.style-0.3.0.tgz",
+ "integrity": "sha512-ny1TbqA2ZsUd6jzN+F034+UMXVK7n5SrwepsrZ2gIqVz00Hn0ohCUbbUdst/2IOFCy0oiTbaOXkSFxRw1RmSlg==",
"license": "MIT",
"workspaces": [
".",
@@ -12916,13 +13689,13 @@
"themes/*"
],
"dependencies": {
- "media-chrome": "~4.13.0"
+ "media-chrome": "~4.14.0"
}
},
"node_modules/player.style/node_modules/media-chrome": {
- "version": "4.13.1",
- "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.13.1.tgz",
- "integrity": "sha512-jPPwYrFkM4ky27/xNYEeyRPOBC7qvru4Oydy7vQHMHplXLQJmjtcauhlLPvG0O5kkYFEaOBXv5zGYes/UxOoVw==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.14.0.tgz",
+ "integrity": "sha512-IEdFb4blyF15vLvQzLIn6USJBv7Kf2ne+TfLQKBYI5Z0f9VEBVZz5MKy4Uhi0iA9lStl2S9ENIujJRuJIa5OiA==",
"license": "MIT",
"dependencies": {
"ce-la-react": "^0.3.0"
@@ -13005,7 +13778,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"commander": "^9.4.0"
},
@@ -13023,7 +13795,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"engines": {
"node": "^12.20.0 || >=14"
}
@@ -13147,6 +13918,12 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@@ -13213,6 +13990,15 @@
"node": ">=10.13.0"
}
},
+ "node_modules/qrcode/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/qrcode/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -13276,6 +14062,27 @@
"node": ">=8"
}
},
+ "node_modules/qrcode/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/qrcode/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@@ -13419,6 +14226,12 @@
"rc": "cli.js"
}
},
+ "node_modules/rc/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
@@ -13429,24 +14242,26 @@
}
},
"node_modules/react": {
- "version": "19.1.1",
- "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
- "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "19.1.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
- "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
+ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
- "scheduler": "^0.26.0"
+ "scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.1.1"
+ "react": "^19.2.0"
}
},
"node_modules/react-h5-audio-player": {
@@ -13464,10 +14279,11 @@
}
},
"node_modules/react-hook-form": {
- "version": "7.63.0",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
- "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
+ "version": "7.66.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
+ "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -13515,10 +14331,11 @@
}
},
"node_modules/react-is": {
- "version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
+ "version": "19.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
+ "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
+ "license": "MIT",
+ "peer": true
},
"node_modules/react-markdown": {
"version": "10.1.0",
@@ -13548,9 +14365,9 @@
}
},
"node_modules/react-pdf": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.1.0.tgz",
- "integrity": "sha512-iUI1YqWgwwZcsXjrehTp3Yi8nT/bvTaWULaRMMyJWvoqqSlopk4LQQ9GDqUnDtX3gzT2glrqrLbjIPl56a+Q3w==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.2.0.tgz",
+ "integrity": "sha512-zk0DIL31oCh8cuQycM0SJKfwh4Onz0/Nwi6wTOjgtEjWGUY6eM+/vuzvOP3j70qtEULn7m1JtaeGzud1w5fY2Q==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
@@ -13558,7 +14375,7 @@
"make-cancellable-promise": "^2.0.0",
"make-event-props": "^2.0.0",
"merge-refs": "^2.0.0",
- "pdfjs-dist": "5.3.93",
+ "pdfjs-dist": "5.4.296",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
@@ -13609,6 +14426,40 @@
"react-dom": "^17.0.2 || ^18 || ^19"
}
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react-remove-scroll": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
@@ -13667,9 +14518,9 @@
}
},
"node_modules/react-simple-keyboard": {
- "version": "3.8.125",
- "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.125.tgz",
- "integrity": "sha512-8+PbmGA2auM7V57hapHsKV7IJcJVl0QNNW09RJQ7xCiohHuZNKvqrxGvisxhhr7X8C8TKulxbqdxjZbFelwO7w==",
+ "version": "3.8.132",
+ "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.132.tgz",
+ "integrity": "sha512-GoXK+6SRu72Jn8qT8fy+PxstIdZEACyIi/7zy0qXcrB6EJaN6zZk0/w3Sv3ALLwXqQd/3t3yUL4DQOwoNO1cbw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
@@ -13737,31 +14588,6 @@
"read-binary-file-arch": "cli.js"
}
},
- "node_modules/read-binary-file-arch/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/read-binary-file-arch/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -13783,6 +14609,49 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
+ "node_modules/recharts": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz",
+ "integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/refractor": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
@@ -13974,6 +14843,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -13998,6 +14877,12 @@
"url": "https://github.com/sponsors/jet2jet"
}
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
@@ -14006,13 +14891,13 @@
"license": "MIT"
},
"node_modules/resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=4"
+ "node": ">=8"
}
},
"node_modules/responselike": {
@@ -14029,17 +14914,20 @@
}
},
"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==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/retry": {
@@ -14063,6 +14951,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -14100,9 +14995,9 @@
}
},
"node_modules/rollup": {
- "version": "4.52.3",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz",
- "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==",
+ "version": "4.52.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
+ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -14115,28 +15010,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.52.3",
- "@rollup/rollup-android-arm64": "4.52.3",
- "@rollup/rollup-darwin-arm64": "4.52.3",
- "@rollup/rollup-darwin-x64": "4.52.3",
- "@rollup/rollup-freebsd-arm64": "4.52.3",
- "@rollup/rollup-freebsd-x64": "4.52.3",
- "@rollup/rollup-linux-arm-gnueabihf": "4.52.3",
- "@rollup/rollup-linux-arm-musleabihf": "4.52.3",
- "@rollup/rollup-linux-arm64-gnu": "4.52.3",
- "@rollup/rollup-linux-arm64-musl": "4.52.3",
- "@rollup/rollup-linux-loong64-gnu": "4.52.3",
- "@rollup/rollup-linux-ppc64-gnu": "4.52.3",
- "@rollup/rollup-linux-riscv64-gnu": "4.52.3",
- "@rollup/rollup-linux-riscv64-musl": "4.52.3",
- "@rollup/rollup-linux-s390x-gnu": "4.52.3",
- "@rollup/rollup-linux-x64-gnu": "4.52.3",
- "@rollup/rollup-linux-x64-musl": "4.52.3",
- "@rollup/rollup-openharmony-arm64": "4.52.3",
- "@rollup/rollup-win32-arm64-msvc": "4.52.3",
- "@rollup/rollup-win32-ia32-msvc": "4.52.3",
- "@rollup/rollup-win32-x64-gnu": "4.52.3",
- "@rollup/rollup-win32-x64-msvc": "4.52.3",
+ "@rollup/rollup-android-arm-eabi": "4.52.5",
+ "@rollup/rollup-android-arm64": "4.52.5",
+ "@rollup/rollup-darwin-arm64": "4.52.5",
+ "@rollup/rollup-darwin-x64": "4.52.5",
+ "@rollup/rollup-freebsd-arm64": "4.52.5",
+ "@rollup/rollup-freebsd-x64": "4.52.5",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.5",
+ "@rollup/rollup-linux-arm64-musl": "4.52.5",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.5",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.5",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.5",
+ "@rollup/rollup-linux-x64-gnu": "4.52.5",
+ "@rollup/rollup-linux-x64-musl": "4.52.5",
+ "@rollup/rollup-openharmony-arm64": "4.52.5",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.5",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.5",
+ "@rollup/rollup-win32-x64-gnu": "4.52.5",
+ "@rollup/rollup-win32-x64-msvc": "4.52.5",
"fsevents": "~2.3.2"
}
},
@@ -14156,29 +15051,6 @@
"node": ">= 18"
}
},
- "node_modules/router/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/router/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -14256,19 +15128,21 @@
"license": "ISC"
},
"node_modules/scheduler": {
- "version": "0.26.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
- "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"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,
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
}
},
"node_modules/semver-compare": {
@@ -14301,29 +15175,6 @@
"node": ">= 18"
}
},
- "node_modules/send/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/send/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/serialize-error": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
@@ -14483,11 +15334,17 @@
}
},
"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==",
+ "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"
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
},
"node_modules/simple-concat": {
"version": "1.0.1",
@@ -14547,19 +15404,6 @@
"node": ">=10"
}
},
- "node_modules/simple-update-notifier/node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
@@ -14630,31 +15474,6 @@
"node": ">= 6.0.0"
}
},
- "node_modules/socks-proxy-agent/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/socks-proxy-agent/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
@@ -14717,6 +15536,16 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
"node_modules/spotify-audio-element": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/spotify-audio-element/-/spotify-audio-element-1.0.3.tgz",
@@ -14809,6 +15638,16 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -14839,6 +15678,50 @@
"node": ">=8"
}
},
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -14854,15 +15737,19 @@
}
},
"node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "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": "^5.0.1"
+ "ansi-regex": "^6.0.1"
},
"engines": {
- "node": ">=8"
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
@@ -14879,6 +15766,16 @@
"node": ">=8"
}
},
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -14893,24 +15790,24 @@
}
},
"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==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
+ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/style-to-js": {
- "version": "1.1.17",
- "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz",
- "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==",
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz",
+ "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==",
"license": "MIT",
"dependencies": {
- "style-to-object": "1.0.9"
+ "style-to-object": "1.0.11"
}
},
"node_modules/style-to-object": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz",
- "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==",
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.11.tgz",
+ "integrity": "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==",
"license": "MIT",
"dependencies": {
"inline-style-parser": "0.2.4"
@@ -14929,31 +15826,6 @@
"node": ">= 8.0"
}
},
- "node_modules/sumchecker/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/sumchecker/node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/super-media-element": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/super-media-element/-/super-media-element-1.4.2.tgz",
@@ -14983,15 +15855,15 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
- "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
+ "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
"license": "MIT"
},
"node_modules/tapable": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
- "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"license": "MIT",
"engines": {
"node": ">=6"
@@ -15002,19 +15874,21 @@
}
},
"node_modules/tar": {
- "version": "7.5.1",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
- "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
+ "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": {
- "@isaacs/fs-minipass": "^4.0.0",
- "chownr": "^3.0.0",
- "minipass": "^7.1.2",
- "minizlib": "^3.1.0",
- "yallist": "^5.0.0"
+ "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": ">=18"
+ "node": ">=10"
}
},
"node_modules/tar-fs": {
@@ -15065,44 +15939,22 @@
"node": ">= 6"
}
},
- "node_modules/tar/node_modules/chownr": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
- "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
- },
"node_modules/tar/node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "dev": true,
"license": "ISC",
"engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/tar/node_modules/minizlib": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
- "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
- "license": "MIT",
- "dependencies": {
- "minipass": "^7.1.2"
- },
- "engines": {
- "node": ">= 18"
+ "node": ">=8"
}
},
"node_modules/tar/node_modules/yallist": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
- "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=18"
- }
+ "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/temp": {
"version": "0.9.4",
@@ -15110,7 +15962,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
@@ -15145,36 +15996,12 @@
"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/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -15189,7 +16016,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
- "peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -15197,6 +16023,26 @@
"rimraf": "bin.js"
}
},
+ "node_modules/text-extensions": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz",
+ "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tiktok-video-element": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/tiktok-video-element/-/tiktok-video-element-0.1.1.tgz",
@@ -15229,6 +16075,13 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
+ "node_modules/tinyexec": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
+ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -15267,6 +16120,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -15472,6 +16326,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15481,16 +16336,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.45.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.45.0.tgz",
- "integrity": "sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==",
+ "version": "8.46.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz",
+ "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.45.0",
- "@typescript-eslint/parser": "8.45.0",
- "@typescript-eslint/typescript-estree": "8.45.0",
- "@typescript-eslint/utils": "8.45.0"
+ "@typescript-eslint/eslint-plugin": "8.46.2",
+ "@typescript-eslint/parser": "8.46.2",
+ "@typescript-eslint/typescript-estree": "8.46.2",
+ "@typescript-eslint/utils": "8.46.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -15531,11 +16386,24 @@
}
},
"node_modules/undici-types": {
- "version": "7.13.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
- "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
+ "node_modules/unicorn-magic": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+ "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -15582,9 +16450,9 @@
}
},
"node_modules/unist-util-is": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
- "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0"
@@ -15636,9 +16504,9 @@
}
},
"node_modules/unist-util-visit-parents": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
- "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
+ "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
@@ -15650,13 +16518,13 @@
}
},
"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==",
+ "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": ">= 4.0.0"
+ "node": ">= 10.0.0"
}
},
"node_modules/unpipe": {
@@ -15668,6 +16536,37 @@
"node": ">= 0.8"
}
},
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -15721,6 +16620,15 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "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",
@@ -15795,6 +16703,28 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/vimeo-video-element": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vimeo-video-element/-/vimeo-video-element-1.6.0.tgz",
@@ -15805,10 +16735,11 @@
}
},
"node_modules/vite": {
- "version": "7.1.7",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
- "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
+ "version": "7.1.12",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
+ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -15900,6 +16831,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -16020,18 +16952,18 @@
}
},
"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==",
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
},
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
@@ -16056,6 +16988,67 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/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/wrap-ansi/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -16113,12 +17106,25 @@
}
},
"node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
+ "devOptional": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -16179,9 +17185,9 @@
"license": "MIT"
},
"node_modules/zod": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz",
- "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
+ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 8b834d2f..bd71b467 100644
--- a/package.json
+++ b/package.json
@@ -1,26 +1,30 @@
{
"name": "termix",
"private": true,
- "version": "1.7.3",
+ "version": "1.8.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",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "lint": "eslint .",
+ "lint:fix": "eslint --fix .",
+ "type-check": "tsc --noEmit",
"dev": "vite",
"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/backend/starter.js",
"preview": "vite preview",
- "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron .\"",
+ "electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && 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",
"build:linux-appimage": "npm run build && electron-builder --linux AppImage",
"build:linux-targz": "npm run build && electron-builder --linux tar.gz",
- "test:encryption": "tsc -p tsconfig.node.json && node ./dist/backend/backend/utils/encryption-test.js",
- "migrate:encryption": "tsc -p tsconfig.node.json && node ./dist/backend/backend/utils/encryption-migration.js"
+ "build:mac": "npm run build && electron-builder --mac --universal"
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.7",
@@ -40,11 +44,12 @@
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.8",
- "@tailwindcss/vite": "^4.1.11",
+ "@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.9",
"@types/jszip": "^3.4.0",
@@ -95,16 +100,21 @@
"react-simple-keyboard": "^3.8.120",
"react-syntax-highlighter": "^15.6.6",
"react-xtermjs": "^1.0.10",
+ "recharts": "^3.2.1",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"speakeasy": "^2.0.0",
"ssh2": "^1.16.0",
"tailwind-merge": "^3.3.1",
+ "tailwindcss": "^4.1.14",
"wait-on": "^9.0.1",
"ws": "^8.18.3",
"zod": "^4.0.5"
},
"devDependencies": {
+ "@commitlint/cli": "^20.1.0",
+ "@commitlint/config-conventional": "^20.0.0",
+ "@electron/notarize": "^2.5.0",
"@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
@@ -115,7 +125,7 @@
"@types/react-dom": "^19.1.6",
"@types/ssh2": "^1.15.5",
"@types/ws": "^8.18.1",
- "@vitejs/plugin-react-swc": "^3.10.2",
+ "@vitejs/plugin-react": "^4.3.4",
"concurrently": "^9.2.1",
"electron": "^38.0.0",
"electron-builder": "^26.0.12",
@@ -123,9 +133,19 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
+ "husky": "^9.1.7",
+ "lint-staged": "^16.2.3",
"prettier": "3.6.2",
"typescript": "~5.9.2",
"typescript-eslint": "^8.40.0",
"vite": "^7.1.5"
+ },
+ "lint-staged": {
+ "*.{js,jsx,ts,tsx}": [
+ "prettier --write"
+ ],
+ "*.{json,css,md}": [
+ "prettier --write"
+ ]
}
}
diff --git a/public/icon-mac.png b/public/icon-mac.png
new file mode 100644
index 00000000..19f7681e
Binary files /dev/null and b/public/icon-mac.png differ
diff --git a/public/icon.icns b/public/icon.icns
index 8a97f2ae..7e6b7329 100644
Binary files a/public/icon.icns and b/public/icon.icns differ
diff --git a/repo-images/Image 1.png b/repo-images/Image 1.png
index 7a398fb4..fea7eb76 100644
Binary files a/repo-images/Image 1.png and b/repo-images/Image 1.png differ
diff --git a/repo-images/Image 2.png b/repo-images/Image 2.png
index e7a1f6b8..9a47b331 100644
Binary files a/repo-images/Image 2.png and b/repo-images/Image 2.png differ
diff --git a/repo-images/Image 3.png b/repo-images/Image 3.png
index 70077cdf..4179a57a 100644
Binary files a/repo-images/Image 3.png and b/repo-images/Image 3.png differ
diff --git a/repo-images/Image 4.png b/repo-images/Image 4.png
index 5cb16b6c..e2dfa99a 100644
Binary files a/repo-images/Image 4.png and b/repo-images/Image 4.png differ
diff --git a/repo-images/Image 5.png b/repo-images/Image 5.png
index 868e5c27..e44011e0 100644
Binary files a/repo-images/Image 5.png and b/repo-images/Image 5.png differ
diff --git a/repo-images/Image 6.png b/repo-images/Image 6.png
index 9d87ba0f..f6f70ba1 100644
Binary files a/repo-images/Image 6.png and b/repo-images/Image 6.png differ
diff --git a/src/backend/dashboard.ts b/src/backend/dashboard.ts
new file mode 100644
index 00000000..465ab9d8
--- /dev/null
+++ b/src/backend/dashboard.ts
@@ -0,0 +1,245 @@
+import express from "express";
+import cors from "cors";
+import cookieParser from "cookie-parser";
+import { getDb } from "./database/db/index.js";
+import { recentActivity, sshData } from "./database/db/schema.js";
+import { eq, and, desc } from "drizzle-orm";
+import { dashboardLogger } from "./utils/logger.js";
+import { SimpleDBOps } from "./utils/simple-db-ops.js";
+import { AuthManager } from "./utils/auth-manager.js";
+import type { AuthenticatedRequest } from "../types/index.js";
+
+const app = express();
+const authManager = AuthManager.getInstance();
+
+const serverStartTime = Date.now();
+
+const activityRateLimiter = new Map();
+const RATE_LIMIT_MS = 1000; // 1 second window
+
+app.use(
+ cors({
+ origin: (origin, callback) => {
+ if (!origin) return callback(null, true);
+
+ const allowedOrigins = [
+ "http://localhost:5173",
+ "http://localhost:3000",
+ "http://127.0.0.1:5173",
+ "http://127.0.0.1:3000",
+ ];
+
+ if (allowedOrigins.includes(origin)) {
+ return callback(null, true);
+ }
+
+ if (origin.startsWith("https://")) {
+ return callback(null, true);
+ }
+
+ if (origin.startsWith("http://")) {
+ return callback(null, true);
+ }
+
+ callback(new Error("Not allowed by CORS"));
+ },
+ credentials: true,
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
+ allowedHeaders: [
+ "Content-Type",
+ "Authorization",
+ "User-Agent",
+ "X-Electron-App",
+ ],
+ }),
+);
+app.use(cookieParser());
+app.use(express.json({ limit: "1mb" }));
+
+app.use(authManager.createAuthMiddleware());
+
+app.get("/uptime", async (req, res) => {
+ try {
+ const uptimeMs = Date.now() - serverStartTime;
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
+ const days = Math.floor(uptimeSeconds / 86400);
+ const hours = Math.floor((uptimeSeconds % 86400) / 3600);
+ const minutes = Math.floor((uptimeSeconds % 3600) / 60);
+
+ res.json({
+ uptimeMs,
+ uptimeSeconds,
+ formatted: `${days}d ${hours}h ${minutes}m`,
+ });
+ } catch (err) {
+ dashboardLogger.error("Failed to get uptime", err);
+ res.status(500).json({ error: "Failed to get uptime" });
+ }
+});
+
+app.get("/activity/recent", async (req, res) => {
+ try {
+ const userId = (req as AuthenticatedRequest).userId;
+
+ if (!SimpleDBOps.isUserDataUnlocked(userId)) {
+ return res.status(401).json({
+ error: "Session expired - please log in again",
+ code: "SESSION_EXPIRED",
+ });
+ }
+
+ const limit = Number(req.query.limit) || 20;
+
+ const activities = await SimpleDBOps.select(
+ getDb()
+ .select()
+ .from(recentActivity)
+ .where(eq(recentActivity.userId, userId))
+ .orderBy(desc(recentActivity.timestamp))
+ .limit(limit),
+ "recent_activity",
+ userId,
+ );
+
+ res.json(activities);
+ } catch (err) {
+ dashboardLogger.error("Failed to get recent activity", err);
+ res.status(500).json({ error: "Failed to get recent activity" });
+ }
+});
+
+app.post("/activity/log", async (req, res) => {
+ try {
+ const userId = (req as AuthenticatedRequest).userId;
+
+ if (!SimpleDBOps.isUserDataUnlocked(userId)) {
+ return res.status(401).json({
+ error: "Session expired - please log in again",
+ code: "SESSION_EXPIRED",
+ });
+ }
+
+ const { type, hostId, hostName } = req.body;
+
+ if (!type || !hostId || !hostName) {
+ return res.status(400).json({
+ error: "Missing required fields: type, hostId, hostName",
+ });
+ }
+
+ if (type !== "terminal" && type !== "file_manager") {
+ return res.status(400).json({
+ error: "Invalid activity type. Must be 'terminal' or 'file_manager'",
+ });
+ }
+
+ const rateLimitKey = `${userId}:${hostId}:${type}`;
+ const now = Date.now();
+ const lastLogged = activityRateLimiter.get(rateLimitKey);
+
+ if (lastLogged && now - lastLogged < RATE_LIMIT_MS) {
+ return res.json({
+ message: "Activity already logged recently (rate limited)",
+ });
+ }
+
+ activityRateLimiter.set(rateLimitKey, now);
+
+ if (activityRateLimiter.size > 10000) {
+ const entriesToDelete: string[] = [];
+ for (const [key, timestamp] of activityRateLimiter.entries()) {
+ if (now - timestamp > RATE_LIMIT_MS * 2) {
+ entriesToDelete.push(key);
+ }
+ }
+ entriesToDelete.forEach((key) => activityRateLimiter.delete(key));
+ }
+
+ const hosts = await SimpleDBOps.select(
+ getDb()
+ .select()
+ .from(sshData)
+ .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
+ "ssh_data",
+ userId,
+ );
+
+ if (hosts.length === 0) {
+ return res.status(404).json({ error: "Host not found" });
+ }
+
+ const result = (await SimpleDBOps.insert(
+ recentActivity,
+ "recent_activity",
+ {
+ userId,
+ type,
+ hostId,
+ hostName,
+ },
+ userId,
+ )) as unknown as { id: number };
+
+ const allActivities = await SimpleDBOps.select(
+ getDb()
+ .select()
+ .from(recentActivity)
+ .where(eq(recentActivity.userId, userId))
+ .orderBy(desc(recentActivity.timestamp)),
+ "recent_activity",
+ userId,
+ );
+
+ if (allActivities.length > 100) {
+ const toDelete = allActivities.slice(100);
+ for (const activity of toDelete) {
+ await SimpleDBOps.delete(recentActivity, "recent_activity", userId);
+ }
+ }
+
+ res.json({ message: "Activity logged", id: result.id });
+ } catch (err) {
+ dashboardLogger.error("Failed to log activity", err);
+ res.status(500).json({ error: "Failed to log activity" });
+ }
+});
+
+app.delete("/activity/reset", async (req, res) => {
+ try {
+ const userId = (req as AuthenticatedRequest).userId;
+
+ if (!SimpleDBOps.isUserDataUnlocked(userId)) {
+ return res.status(401).json({
+ error: "Session expired - please log in again",
+ code: "SESSION_EXPIRED",
+ });
+ }
+
+ await SimpleDBOps.delete(
+ recentActivity,
+ "recent_activity",
+ eq(recentActivity.userId, userId),
+ );
+
+ dashboardLogger.success("Recent activity cleared", {
+ operation: "reset_recent_activity",
+ userId,
+ });
+
+ res.json({ message: "Recent activity cleared" });
+ } catch (err) {
+ dashboardLogger.error("Failed to reset activity", err);
+ res.status(500).json({ error: "Failed to reset activity" });
+ }
+});
+
+const PORT = 30006;
+app.listen(PORT, async () => {
+ try {
+ await authManager.initialize();
+ } catch (err) {
+ dashboardLogger.error("Failed to initialize AuthManager", err, {
+ operation: "auth_init_error",
+ });
+ }
+});
diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts
index 48f7aaa0..661549b5 100644
--- a/src/backend/database/database.ts
+++ b/src/backend/database/database.ts
@@ -6,6 +6,7 @@ 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 snippetsRoutes from "./routes/snippets.js";
import cors from "cors";
import fetch from "node-fetch";
import fs from "fs";
@@ -31,6 +32,12 @@ import {
sshCredentialUsage,
settings,
} from "./db/schema.js";
+import type {
+ CacheEntry,
+ GitHubRelease,
+ GitHubAPIResponse,
+ AuthenticatedRequest,
+} from "../../types/index.js";
import { getDb } from "./db/index.js";
import Database from "better-sqlite3";
@@ -53,6 +60,10 @@ app.use(
"http://127.0.0.1:3000",
];
+ if (allowedOrigins.includes(origin)) {
+ return callback(null, true);
+ }
+
if (origin.startsWith("https://")) {
return callback(null, true);
}
@@ -61,10 +72,6 @@ app.use(
return callback(null, true);
}
- if (allowedOrigins.includes(origin)) {
- return callback(null, true);
- }
-
callback(new Error("Not allowed by CORS"));
},
credentials: true,
@@ -74,6 +81,8 @@ app.use(
"Authorization",
"User-Agent",
"X-Electron-App",
+ "Accept",
+ "Origin",
],
}),
);
@@ -105,17 +114,11 @@ const upload = multer({
},
});
-interface CacheEntry {
- data: any;
- timestamp: number;
- expiresAt: number;
-}
-
class GitHubCache {
private cache: Map = new Map();
private readonly CACHE_DURATION = 30 * 60 * 1000;
- set(key: string, data: any): void {
+ set(key: string, data: T): void {
const now = Date.now();
this.cache.set(key, {
data,
@@ -124,7 +127,7 @@ class GitHubCache {
});
}
- get(key: string): any | null {
+ get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
return null;
@@ -135,7 +138,7 @@ class GitHubCache {
return null;
}
- return entry.data;
+ return entry.data as T;
}
}
@@ -145,34 +148,16 @@ const GITHUB_API_BASE = "https://api.github.com";
const REPO_OWNER = "Termix-SSH";
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;
- name: string;
- size: number;
- download_count: number;
- browser_download_url: string;
- }>;
- prerelease: boolean;
- draft: boolean;
-}
-
-async function fetchGitHubAPI(
+async function fetchGitHubAPI(
endpoint: string,
cacheKey: string,
-): Promise {
- const cachedData = githubCache.get(cacheKey);
- if (cachedData) {
+): Promise> {
+ const cachedEntry = githubCache.get>(cacheKey);
+ if (cachedEntry) {
return {
- data: cachedData,
+ data: cachedEntry.data,
cached: true,
- cache_age: Date.now() - cachedData.timestamp,
+ cache_age: Date.now() - cachedEntry.timestamp,
};
}
@@ -191,8 +176,13 @@ async function fetchGitHubAPI(
);
}
- const data = await response.json();
- githubCache.set(cacheKey, data);
+ const data = (await response.json()) as T;
+ const cacheData: CacheEntry = {
+ data,
+ timestamp: Date.now(),
+ expiresAt: Date.now() + 30 * 60 * 1000,
+ };
+ githubCache.set(cacheKey, cacheData);
return {
data: data,
@@ -257,7 +247,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
localVersion = foundVersion;
break;
}
- } catch (error) {
+ } catch {
continue;
}
}
@@ -272,7 +262,7 @@ app.get("/version", authenticateJWT, async (req, res) => {
try {
const cacheKey = "latest_release";
- const releaseData = await fetchGitHubAPI(
+ const releaseData = await fetchGitHubAPI(
`/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
cacheKey,
);
@@ -323,12 +313,12 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
);
const cacheKey = `releases_rss_${page}_${per_page}`;
- const releasesData = await fetchGitHubAPI(
+ const releasesData = await fetchGitHubAPI(
`/repos/${REPO_OWNER}/${REPO_NAME}/releases?page=${page}&per_page=${per_page}`,
cacheKey,
);
- const rssItems = releasesData.data.map((release: GitHubRelease) => ({
+ const rssItems = releasesData.data.map((release) => ({
id: release.id,
title: release.name || release.tag_name,
description: release.body,
@@ -372,7 +362,6 @@ app.get("/releases/rss", authenticateJWT, async (req, res) => {
app.get("/encryption/status", requireAdmin, async (req, res) => {
try {
- const authManager = AuthManager.getInstance();
const securityStatus = {
initialized: true,
system: { hasSecret: true, isValid: true },
@@ -417,8 +406,6 @@ app.post("/encryption/initialize", requireAdmin, async (req, res) => {
app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
try {
- const authManager = AuthManager.getInstance();
-
apiLogger.warn("System JWT secret regenerated via API", {
operation: "jwt_regenerate_api",
});
@@ -440,8 +427,6 @@ app.post("/encryption/regenerate", requireAdmin, async (req, res) => {
app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
try {
- const authManager = AuthManager.getInstance();
-
apiLogger.warn("JWT secret regenerated via API", {
operation: "jwt_secret_regenerate_api",
});
@@ -462,7 +447,7 @@ app.post("/encryption/regenerate-jwt", requireAdmin, async (req, res) => {
app.post("/database/export", authenticateJWT, async (req, res) => {
try {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body;
if (!password) {
@@ -695,7 +680,7 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.authType,
decrypted.password || null,
decrypted.key || null,
- decrypted.keyPassword || null,
+ decrypted.key_password || null,
decrypted.keyType || null,
decrypted.autostartPassword || null,
decrypted.autostartKey || null,
@@ -738,9 +723,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => {
decrypted.username,
decrypted.password || null,
decrypted.key || null,
- decrypted.privateKey || null,
- decrypted.publicKey || null,
- decrypted.keyPassword || null,
+ decrypted.private_key || null,
+ decrypted.public_key || null,
+ decrypted.key_password || null,
decrypted.keyType || null,
decrypted.detectedKeyType || null,
decrypted.usageCount || 0,
@@ -916,19 +901,40 @@ app.post(
return res.status(400).json({ error: "No file uploaded" });
}
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body;
+ const mainDb = getDb();
- if (!password) {
- return res.status(400).json({
- error: "Password required for import",
- code: "PASSWORD_REQUIRED",
- });
+ const userRecords = await mainDb
+ .select()
+ .from(users)
+ .where(eq(users.id, userId));
+
+ if (!userRecords || userRecords.length === 0) {
+ return res.status(404).json({ error: "User not found" });
}
- const unlocked = await authManager.authenticateUser(userId, password);
- if (!unlocked) {
- return res.status(401).json({ error: "Invalid password" });
+ const isOidcUser = !!userRecords[0].is_oidc;
+
+ if (!isOidcUser) {
+ if (!password) {
+ return res.status(400).json({
+ error: "Password required for import",
+ code: "PASSWORD_REQUIRED",
+ });
+ }
+
+ const unlocked = await authManager.authenticateUser(userId, password);
+ if (!unlocked) {
+ return res.status(401).json({ error: "Invalid password" });
+ }
+ } else if (!DataCrypto.getUserDataKey(userId)) {
+ const oidcUnlocked = await authManager.authenticateOIDCUser(userId);
+ if (!oidcUnlocked) {
+ return res.status(403).json({
+ error: "Failed to unlock user data with SSO credentials",
+ });
+ }
}
apiLogger.info("Importing SQLite data", {
@@ -939,7 +945,13 @@ app.post(
mimetype: req.file.mimetype,
});
- const userDataKey = DataCrypto.getUserDataKey(userId);
+ let userDataKey = DataCrypto.getUserDataKey(userId);
+ if (!userDataKey && isOidcUser) {
+ const oidcUnlocked = await authManager.authenticateOIDCUser(userId);
+ if (oidcUnlocked) {
+ userDataKey = DataCrypto.getUserDataKey(userId);
+ }
+ }
if (!userDataKey) {
throw new Error("User data not unlocked");
}
@@ -968,7 +980,7 @@ app.post(
try {
importDb = new Database(req.file.path, { readonly: true });
- const tables = importDb
+ importDb
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
.all();
} catch (sqliteError) {
@@ -993,8 +1005,6 @@ app.post(
};
try {
- const mainDb = getDb();
-
try {
const importedHosts = importDb
.prepare("SELECT * FROM ssh_data")
@@ -1059,7 +1069,7 @@ app.post(
);
}
}
- } catch (tableError) {
+ } catch {
apiLogger.info("ssh_data table not found in import file, skipping");
}
@@ -1120,7 +1130,7 @@ app.post(
);
}
}
- } catch (tableError) {
+ } catch {
apiLogger.info(
"ssh_credentials table not found in import file, skipping",
);
@@ -1191,7 +1201,7 @@ app.post(
);
}
}
- } catch (tableError) {
+ } catch {
apiLogger.info(`${table} table not found in import file, skipping`);
}
}
@@ -1229,7 +1239,7 @@ app.post(
);
}
}
- } catch (tableError) {
+ } catch {
apiLogger.info(
"dismissed_alerts table not found in import file, skipping",
);
@@ -1270,7 +1280,7 @@ app.post(
);
}
}
- } catch (tableError) {
+ } catch {
apiLogger.info("settings table not found in import file, skipping");
}
} else {
@@ -1288,7 +1298,7 @@ app.post(
try {
fs.unlinkSync(req.file.path);
- } catch (cleanupError) {
+ } catch {
apiLogger.warn("Failed to clean up uploaded file", {
operation: "file_cleanup_warning",
filePath: req.file.path,
@@ -1314,7 +1324,7 @@ app.post(
if (req.file?.path && fs.existsSync(req.file.path)) {
try {
fs.unlinkSync(req.file.path);
- } catch (cleanupError) {
+ } catch {
apiLogger.warn("Failed to clean up uploaded file after error", {
operation: "file_cleanup_error",
filePath: req.file.path,
@@ -1324,7 +1334,7 @@ app.post(
apiLogger.error("SQLite import failed", error, {
operation: "sqlite_import_api_failed",
- userId: (req as any).userId,
+ userId: (req as AuthenticatedRequest).userId,
});
res.status(500).json({
error: "Failed to import SQLite data",
@@ -1336,12 +1346,8 @@ app.post(
app.post("/database/export/preview", authenticateJWT, async (req, res) => {
try {
- const userId = (req as any).userId;
- const {
- format = "encrypted",
- scope = "user_data",
- includeCredentials = true,
- } = req.body;
+ const userId = (req as AuthenticatedRequest).userId;
+ const { scope = "user_data", includeCredentials = true } = req.body;
const exportData = await UserDataExport.exportUserData(userId, {
format: "encrypted",
@@ -1411,13 +1417,14 @@ app.use("/users", userRoutes);
app.use("/ssh", sshRoutes);
app.use("/alerts", alertRoutes);
app.use("/credentials", credentialsRoutes);
+app.use("/snippets", snippetsRoutes);
app.use(
(
err: unknown,
req: express.Request,
res: express.Response,
- next: express.NextFunction,
+ _next: express.NextFunction,
) => {
apiLogger.error("Unhandled error in request", err, {
operation: "error_handler",
@@ -1430,7 +1437,6 @@ app.use(
);
const HTTP_PORT = 30001;
-const HTTPS_PORT = process.env.SSL_PORT || 8443;
async function initializeSecurity() {
try {
@@ -1443,13 +1449,6 @@ async function initializeSecurity() {
if (!isValid) {
throw new Error("Security system validation failed");
}
-
- const securityStatus = {
- initialized: true,
- system: { hasSecret: true, isValid: true },
- activeSessions: {},
- activeSessionCount: 0,
- };
} catch (error) {
databaseLogger.error("Failed to initialize security system", error, {
operation: "security_init_error",
@@ -1481,13 +1480,13 @@ app.get(
if (status.hasUnencryptedDb) {
try {
unencryptedSize = fs.statSync(dbPath).size;
- } catch (error) {}
+ } catch {}
}
if (status.hasEncryptedDb) {
try {
encryptedSize = fs.statSync(encryptedDbPath).size;
- } catch (error) {}
+ } catch {}
}
res.json({
diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts
index 88cca125..7b3b6138 100644
--- a/src/backend/database/db/index.ts
+++ b/src/backend/database/db/index.ts
@@ -12,10 +12,6 @@ import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
const dataDir = process.env.DATA_DIR || "./db/data";
const dbDir = path.resolve(dataDir);
if (!fs.existsSync(dbDir)) {
- databaseLogger.info(`Creating database directory`, {
- operation: "db_init",
- path: dbDir,
- });
fs.mkdirSync(dbDir, { recursive: true });
}
@@ -23,7 +19,7 @@ const enableFileEncryption = process.env.DB_FILE_ENCRYPTION !== "false";
const dbPath = path.join(dataDir, "db.sqlite");
const encryptedDbPath = `${dbPath}.encrypted`;
-let actualDbPath = ":memory:";
+const actualDbPath = ":memory:";
let memoryDatabase: Database.Database;
let isNewDatabase = false;
let sqlite: Database.Database;
@@ -31,7 +27,7 @@ let sqlite: Database.Database;
async function initializeDatabaseAsync(): Promise {
const systemCrypto = SystemCrypto.getInstance();
- const dbKey = await systemCrypto.getDatabaseKey();
+ await systemCrypto.getDatabaseKey();
if (enableFileEncryption) {
try {
if (DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath)) {
@@ -39,6 +35,13 @@ async function initializeDatabaseAsync(): Promise {
await DatabaseFileEncryption.decryptDatabaseToBuffer(encryptedDbPath);
memoryDatabase = new Database(decryptedBuffer);
+
+ try {
+ const sessionCount = memoryDatabase
+ .prepare("SELECT COUNT(*) as count FROM sessions")
+ .get() as { count: number };
+ } catch (countError) {
+ }
} else {
const migration = new DatabaseMigration(dataDir);
const migrationStatus = migration.checkMigrationStatus();
@@ -145,6 +148,18 @@ async function initializeCompleteDatabase(): Promise {
value TEXT NOT NULL
);
+ CREATE TABLE IF NOT EXISTS sessions (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ jwt_token TEXT NOT NULL,
+ device_type TEXT NOT NULL,
+ device_info TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ expires_at TEXT NOT NULL,
+ last_active_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,
@@ -165,6 +180,12 @@ async function initializeCompleteDatabase(): Promise {
tunnel_connections TEXT,
enable_file_manager INTEGER NOT NULL DEFAULT 1,
default_path TEXT,
+ autostart_password TEXT,
+ autostart_key TEXT,
+ autostart_key_password TEXT,
+ force_keyboard_interactive TEXT,
+ stats_config TEXT,
+ terminal_config TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
@@ -242,8 +263,39 @@ async function initializeCompleteDatabase(): Promise {
FOREIGN KEY (user_id) REFERENCES users (id)
);
+ CREATE TABLE IF NOT EXISTS snippets (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ content TEXT NOT NULL,
+ description 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 recent_activity (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ user_id TEXT NOT NULL,
+ type TEXT NOT NULL,
+ host_id INTEGER NOT NULL,
+ host_name TEXT NOT NULL,
+ timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users (id),
+ FOREIGN KEY (host_id) REFERENCES ssh_data (id)
+ );
+
`);
+ try {
+ sqlite.prepare("DELETE FROM sessions").run();
+ } catch (e) {
+ databaseLogger.warn("Could not clear sessions on startup", {
+ operation: "db_init_session_cleanup_failed",
+ error: e,
+ });
+ }
+
migrateSchema();
try {
@@ -263,6 +315,24 @@ async function initializeCompleteDatabase(): Promise {
error: e,
});
}
+
+ try {
+ const row = sqlite
+ .prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
+ .get();
+ if (!row) {
+ sqlite
+ .prepare(
+ "INSERT INTO settings (key, value) VALUES ('allow_password_login', 'true')",
+ )
+ .run();
+ }
+ } catch (e) {
+ databaseLogger.warn("Could not initialize allow_password_login setting", {
+ operation: "db_init",
+ error: e,
+ });
+ }
}
const addColumnIfNotExists = (
@@ -277,7 +347,7 @@ const addColumnIfNotExists = (
FROM ${table} LIMIT 1`,
)
.get();
- } catch (e) {
+ } catch {
try {
sqlite.exec(`ALTER TABLE ${table}
ADD COLUMN ${column} ${definition};`);
@@ -351,7 +421,10 @@ const migrateSchema = () => {
"updated_at",
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
);
-
+ addColumnIfNotExists("ssh_data", "force_keyboard_interactive", "TEXT");
+ addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
+ addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
+ addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT");
addColumnIfNotExists(
"ssh_data",
"credential_id",
@@ -361,6 +434,8 @@ const migrateSchema = () => {
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT");
+ addColumnIfNotExists("ssh_data", "stats_config", "TEXT");
+ addColumnIfNotExists("ssh_data", "terminal_config", "TEXT");
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
@@ -370,6 +445,33 @@ const migrateSchema = () => {
addColumnIfNotExists("file_manager_pinned", "host_id", "INTEGER NOT NULL");
addColumnIfNotExists("file_manager_shortcuts", "host_id", "INTEGER NOT NULL");
+ try {
+ sqlite
+ .prepare("SELECT id FROM sessions LIMIT 1")
+ .get();
+ } catch {
+ try {
+ sqlite.exec(`
+ CREATE TABLE IF NOT EXISTS sessions (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ jwt_token TEXT NOT NULL,
+ device_type TEXT NOT NULL,
+ device_info TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ expires_at TEXT NOT NULL,
+ last_active_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users (id)
+ );
+ `);
+ } catch (createError) {
+ databaseLogger.warn("Failed to create sessions table", {
+ operation: "schema_migration",
+ error: createError,
+ });
+ }
+ }
+
databaseLogger.success("Schema migration completed", {
operation: "schema_migration",
});
@@ -385,6 +487,13 @@ async function saveMemoryDatabaseToFile() {
fs.mkdirSync(dataDir, { recursive: true });
}
+ try {
+ const sessionCount = memoryDatabase
+ .prepare("SELECT COUNT(*) as count FROM sessions")
+ .get() as { count: number };
+ } catch (countError) {
+ }
+
if (enableFileEncryption) {
await DatabaseFileEncryption.encryptDatabaseFromBuffer(
buffer,
@@ -476,21 +585,25 @@ async function cleanupDatabase() {
for (const file of files) {
try {
fs.unlinkSync(path.join(tempDir, file));
- } catch {}
+ } catch {
+ }
}
try {
fs.rmdirSync(tempDir);
- } catch {}
+ } catch {
+ }
}
- } catch (error) {}
+ } catch {
+ }
}
process.on("exit", () => {
if (sqlite) {
try {
sqlite.close();
- } catch {}
+ } catch {
+ }
}
});
diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts
index eeac5c34..86af0d02 100644
--- a/src/backend/database/db/schema.ts
+++ b/src/backend/database/db/schema.ts
@@ -30,6 +30,23 @@ export const settings = sqliteTable("settings", {
value: text("value").notNull(),
});
+export const sessions = sqliteTable("sessions", {
+ id: text("id").primaryKey(),
+ userId: text("user_id")
+ .notNull()
+ .references(() => users.id),
+ jwtToken: text("jwt_token").notNull(),
+ deviceType: text("device_type").notNull(),
+ deviceInfo: text("device_info").notNull(),
+ createdAt: text("created_at")
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+ expiresAt: text("expires_at").notNull(),
+ lastActiveAt: text("last_active_at")
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+});
+
export const sshData = sqliteTable("ssh_data", {
id: integer("id").primaryKey({ autoIncrement: true }),
userId: text("user_id")
@@ -43,6 +60,7 @@ export const sshData = sqliteTable("ssh_data", {
tags: text("tags"),
pin: integer("pin", { mode: "boolean" }).notNull().default(false),
authType: text("auth_type").notNull(),
+ forceKeyboardInteractive: text("force_keyboard_interactive"),
password: text("password"),
key: text("key", { length: 8192 }),
@@ -65,6 +83,8 @@ export const sshData = sqliteTable("ssh_data", {
.notNull()
.default(true),
defaultPath: text("default_path"),
+ statsConfig: text("stats_config"),
+ terminalConfig: text("terminal_config"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
@@ -172,3 +192,34 @@ export const sshCredentialUsage = sqliteTable("ssh_credential_usage", {
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
});
+
+export const snippets = sqliteTable("snippets", {
+ id: integer("id").primaryKey({ autoIncrement: true }),
+ userId: text("user_id")
+ .notNull()
+ .references(() => users.id),
+ name: text("name").notNull(),
+ content: text("content").notNull(),
+ description: text("description"),
+ createdAt: text("created_at")
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+ updatedAt: text("updated_at")
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+});
+
+export const recentActivity = sqliteTable("recent_activity", {
+ id: integer("id").primaryKey({ autoIncrement: true }),
+ userId: text("user_id")
+ .notNull()
+ .references(() => users.id),
+ type: text("type").notNull(),
+ hostId: integer("host_id")
+ .notNull()
+ .references(() => sshData.id),
+ hostName: text("host_name").notNull(),
+ timestamp: text("timestamp")
+ .notNull()
+ .default(sql`CURRENT_TIMESTAMP`),
+});
diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts
index e0e01f1c..cf653109 100644
--- a/src/backend/database/routes/alerts.ts
+++ b/src/backend/database/routes/alerts.ts
@@ -1,3 +1,8 @@
+import type {
+ AuthenticatedRequest,
+ CacheEntry,
+ TermixAlert,
+} from "../../../types/index.js";
import express from "express";
import { db } from "../db/index.js";
import { dismissedAlerts } from "../db/schema.js";
@@ -6,17 +11,11 @@ import fetch from "node-fetch";
import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
-interface CacheEntry {
- data: any;
- timestamp: number;
- expiresAt: number;
-}
-
class AlertCache {
private cache: Map = new Map();
private readonly CACHE_DURATION = 5 * 60 * 1000;
- set(key: string, data: any): void {
+ set(key: string, data: T): void {
const now = Date.now();
this.cache.set(key, {
data,
@@ -25,7 +24,7 @@ class AlertCache {
});
}
- get(key: string): any | null {
+ get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
return null;
@@ -36,7 +35,7 @@ class AlertCache {
return null;
}
- return entry.data;
+ return entry.data as T;
}
}
@@ -47,20 +46,9 @@ const REPO_OWNER = "Termix-SSH";
const REPO_NAME = "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;
-}
-
async function fetchAlertsFromGitHub(): Promise {
const cacheKey = "termix_alerts";
- const cachedData = alertCache.get(cacheKey);
+ const cachedData = alertCache.get(cacheKey);
if (cachedData) {
return cachedData;
}
@@ -115,7 +103,7 @@ const authenticateJWT = authManager.createAuthMiddleware();
// GET /alerts
router.get("/", authenticateJWT, async (req, res) => {
try {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const allAlerts = await fetchAlertsFromGitHub();
@@ -148,7 +136,7 @@ router.get("/", authenticateJWT, async (req, res) => {
router.post("/dismiss", authenticateJWT, async (req, res) => {
try {
const { alertId } = req.body;
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!alertId) {
authLogger.warn("Missing alertId in dismiss request", { userId });
@@ -170,7 +158,7 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
return res.status(409).json({ error: "Alert already dismissed" });
}
- const result = await db.insert(dismissedAlerts).values({
+ await db.insert(dismissedAlerts).values({
userId,
alertId,
});
@@ -186,7 +174,7 @@ router.post("/dismiss", authenticateJWT, async (req, res) => {
// GET /alerts/dismissed/:userId
router.get("/dismissed", authenticateJWT, async (req, res) => {
try {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const dismissedAlertRecords = await db
.select({
@@ -211,7 +199,7 @@ router.get("/dismissed", authenticateJWT, async (req, res) => {
router.delete("/dismiss", authenticateJWT, async (req, res) => {
try {
const { alertId } = req.body;
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!alertId) {
return res.status(400).json({ error: "Alert ID is required" });
diff --git a/src/backend/database/routes/credentials.ts b/src/backend/database/routes/credentials.ts
index c856322f..2f0c3ce4 100644
--- a/src/backend/database/routes/credentials.ts
+++ b/src/backend/database/routes/credentials.ts
@@ -1,16 +1,15 @@
+import type { AuthenticatedRequest } from "../../../types/index.js";
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 type { Request, Response } from "express";
import { authLogger } from "../../utils/logger.js";
import { SimpleDBOps } from "../../utils/simple-db-ops.js";
import { AuthManager } from "../../utils/auth-manager.js";
import {
parseSSHKey,
parsePublicKey,
- detectKeyType,
validateKeyPair,
} from "../../utils/ssh-key-utils.js";
import crypto from "crypto";
@@ -29,7 +28,11 @@ function generateSSHKeyPair(
} {
try {
let ssh2Type = keyType;
- const options: any = {};
+ const options: {
+ bits?: number;
+ passphrase?: string;
+ cipher?: string;
+ } = {};
if (keyType === "ssh-rsa") {
ssh2Type = "rsa";
@@ -46,6 +49,7 @@ function generateSSHKeyPair(
options.cipher = "aes128-cbc";
}
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const keyPair = ssh2Utils.generateKeyPairSync(ssh2Type as any, options);
return {
@@ -64,7 +68,7 @@ function generateSSHKeyPair(
const router = express.Router();
-function isNonEmptyString(val: any): val is string {
+function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0;
}
@@ -79,7 +83,7 @@ router.post(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const {
name,
description,
@@ -226,7 +230,7 @@ router.get(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for credential fetch");
@@ -259,7 +263,7 @@ router.get(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId for credential folder fetch");
@@ -297,7 +301,7 @@ router.get(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
if (!isNonEmptyString(userId) || !id) {
@@ -328,19 +332,19 @@ router.get(
const output = formatCredentialOutput(credential);
if (credential.password) {
- (output as any).password = credential.password;
+ output.password = credential.password;
}
if (credential.key) {
- (output as any).key = credential.key;
+ output.key = credential.key;
}
if (credential.private_key) {
- (output as any).privateKey = credential.private_key;
+ output.privateKey = credential.private_key;
}
if (credential.public_key) {
- (output as any).publicKey = credential.public_key;
+ output.publicKey = credential.public_key;
}
if (credential.key_password) {
- (output as any).keyPassword = credential.key_password;
+ output.keyPassword = credential.key_password;
}
res.json(output);
@@ -361,7 +365,7 @@ router.put(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
const updateData = req.body;
@@ -385,7 +389,7 @@ router.put(
return res.status(404).json({ error: "Credential not found" });
}
- const updateFields: any = {};
+ const updateFields: Record = {};
if (updateData.name !== undefined)
updateFields.name = updateData.name.trim();
@@ -497,7 +501,7 @@ router.delete(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { id } = req.params;
if (!isNonEmptyString(userId) || !id) {
@@ -596,7 +600,7 @@ router.post(
"/:id/apply-to-host/:hostId",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { id: credentialId, hostId } = req.params;
if (!isNonEmptyString(userId) || !credentialId || !hostId) {
@@ -629,8 +633,8 @@ router.post(
.update(sshData)
.set({
credentialId: parseInt(credentialId),
- username: credential.username,
- authType: credential.auth_type || credential.authType,
+ username: credential.username as string,
+ authType: (credential.auth_type || credential.authType) as string,
password: null,
key: null,
key_password: null,
@@ -675,7 +679,7 @@ router.get(
"/:id/hosts",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { id: credentialId } = req.params;
if (!isNonEmptyString(userId) || !credentialId) {
@@ -707,7 +711,9 @@ router.get(
},
);
-function formatCredentialOutput(credential: any): any {
+function formatCredentialOutput(
+ credential: Record,
+): Record {
return {
id: credential.id,
name: credential.name,
@@ -731,7 +737,9 @@ function formatCredentialOutput(credential: any): any {
};
}
-function formatSSHHostOutput(host: any): any {
+function formatSSHHostOutput(
+ host: Record,
+): Record {
return {
id: host.id,
userId: host.userId,
@@ -751,7 +759,7 @@ function formatSSHHostOutput(host: any): any {
enableTerminal: !!host.enableTerminal,
enableTunnel: !!host.enableTunnel,
tunnelConnections: host.tunnelConnections
- ? JSON.parse(host.tunnelConnections)
+ ? JSON.parse(host.tunnelConnections as string)
: [],
enableFileManager: !!host.enableFileManager,
defaultPath: host.defaultPath,
@@ -766,7 +774,7 @@ router.put(
"/folders/rename",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { oldName, newName } = req.body;
if (!isNonEmptyString(oldName) || !isNonEmptyString(newName)) {
@@ -970,7 +978,7 @@ router.post(
try {
let privateKeyObj;
- let parseAttempts = [];
+ const parseAttempts = [];
try {
privateKeyObj = crypto.createPrivateKey({
@@ -1093,7 +1101,9 @@ router.post(
finalPublicKey = `${keyType} ${base64Data}`;
formatType = "ssh";
}
- } catch (sshError) {}
+ } catch {
+ // Ignore validation errors
+ }
const response = {
success: true,
@@ -1117,15 +1127,15 @@ router.post(
);
async function deploySSHKeyToHost(
- hostConfig: any,
+ hostConfig: Record,
publicKey: string,
- credentialData: any,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _credentialData: Record,
): Promise<{ success: boolean; message?: string; error?: string }> {
return new Promise((resolve) => {
const conn = new Client();
- let connectionTimeout: NodeJS.Timeout;
- connectionTimeout = setTimeout(() => {
+ const connectionTimeout = setTimeout(() => {
conn.destroy();
resolve({ success: false, error: "Connection timeout" });
}, 120000);
@@ -1158,7 +1168,9 @@ async function deploySSHKeyToHost(
}
});
- stream.on("data", (data) => {});
+ stream.on("data", () => {
+ // Ignore output
+ });
},
);
});
@@ -1175,7 +1187,9 @@ async function deploySSHKeyToHost(
if (parsed.data) {
actualPublicKey = parsed.data;
}
- } catch (e) {}
+ } catch {
+ // Ignore parse errors
+ }
const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) {
@@ -1202,7 +1216,7 @@ async function deploySSHKeyToHost(
output += data.toString();
});
- stream.on("close", (code) => {
+ stream.on("close", () => {
clearTimeout(checkTimeout);
const exists = output.trim() === "0";
resolveCheck(exists);
@@ -1229,7 +1243,9 @@ async function deploySSHKeyToHost(
if (parsed.data) {
actualPublicKey = parsed.data;
}
- } catch (e) {}
+ } catch {
+ // Ignore parse errors
+ }
const escapedKey = actualPublicKey
.replace(/\\/g, "\\\\")
@@ -1269,7 +1285,9 @@ async function deploySSHKeyToHost(
if (parsed.data) {
actualPublicKey = parsed.data;
}
- } catch (e) {}
+ } catch {
+ // Ignore parse errors
+ }
const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) {
@@ -1295,7 +1313,7 @@ async function deploySSHKeyToHost(
output += data.toString();
});
- stream.on("close", (code) => {
+ stream.on("close", () => {
clearTimeout(verifyTimeout);
const verified = output.trim() === "0";
resolveVerify(verified);
@@ -1356,7 +1374,7 @@ async function deploySSHKeyToHost(
});
try {
- const connectionConfig: any = {
+ const connectionConfig: Record = {
host: hostConfig.ip,
port: hostConfig.port || 22,
username: hostConfig.username,
@@ -1403,14 +1421,15 @@ async function deploySSHKeyToHost(
connectionConfig.password = hostConfig.password;
} else if (hostConfig.authType === "key" && hostConfig.privateKey) {
try {
+ const privateKey = hostConfig.privateKey as string;
if (
- !hostConfig.privateKey.includes("-----BEGIN") ||
- !hostConfig.privateKey.includes("-----END")
+ !privateKey.includes("-----BEGIN") ||
+ !privateKey.includes("-----END")
) {
throw new Error("Invalid private key format");
}
- const cleanKey = hostConfig.privateKey
+ const cleanKey = privateKey
.trim()
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
@@ -1465,7 +1484,7 @@ router.post(
}
try {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!userId) {
return res.status(401).json({
success: false,
@@ -1521,7 +1540,7 @@ router.post(
const hostData = targetHost[0];
- let hostConfig = {
+ const hostConfig = {
ip: hostData.ip,
port: hostData.port,
username: hostData.username,
@@ -1532,7 +1551,7 @@ router.post(
};
if (hostData.authType === "credential" && hostData.credentialId) {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!userId) {
return res.status(400).json({
success: false,
@@ -1546,7 +1565,7 @@ router.post(
db
.select()
.from(sshCredentials)
- .where(eq(sshCredentials.id, hostData.credentialId))
+ .where(eq(sshCredentials.id, hostData.credentialId as number))
.limit(1),
"ssh_credentials",
userId,
@@ -1571,7 +1590,7 @@ router.post(
error: "Host credential not found",
});
}
- } catch (error) {
+ } catch {
return res.status(500).json({
success: false,
error: "Failed to resolve host credentials",
@@ -1581,7 +1600,7 @@ router.post(
const deployResult = await deploySSHKeyToHost(
hostConfig,
- credData.publicKey,
+ credData.publicKey as string,
credData,
);
diff --git a/src/backend/database/routes/snippets.ts b/src/backend/database/routes/snippets.ts
new file mode 100644
index 00000000..89dc4513
--- /dev/null
+++ b/src/backend/database/routes/snippets.ts
@@ -0,0 +1,260 @@
+import type { AuthenticatedRequest } from "../../../types/index.js";
+import express from "express";
+import { db } from "../db/index.js";
+import { snippets } from "../db/schema.js";
+import { eq, and, desc, sql } from "drizzle-orm";
+import type { Request, Response } from "express";
+import { authLogger } from "../../utils/logger.js";
+import { AuthManager } from "../../utils/auth-manager.js";
+
+const router = express.Router();
+
+function isNonEmptyString(val: unknown): val is string {
+ return typeof val === "string" && val.trim().length > 0;
+}
+
+const authManager = AuthManager.getInstance();
+const authenticateJWT = authManager.createAuthMiddleware();
+const requireDataAccess = authManager.createDataAccessMiddleware();
+
+// Get all snippets for the authenticated user
+// GET /snippets
+router.get(
+ "/",
+ authenticateJWT,
+ requireDataAccess,
+ async (req: Request, res: Response) => {
+ const userId = (req as AuthenticatedRequest).userId;
+
+ if (!isNonEmptyString(userId)) {
+ authLogger.warn("Invalid userId for snippets fetch");
+ return res.status(400).json({ error: "Invalid userId" });
+ }
+
+ try {
+ const result = await db
+ .select()
+ .from(snippets)
+ .where(eq(snippets.userId, userId))
+ .orderBy(desc(snippets.updatedAt));
+
+ res.json(result);
+ } catch (err) {
+ authLogger.error("Failed to fetch snippets", err);
+ res.status(500).json({ error: "Failed to fetch snippets" });
+ }
+ },
+);
+
+// Get a specific snippet by ID
+// GET /snippets/:id
+router.get(
+ "/:id",
+ authenticateJWT,
+ requireDataAccess,
+ async (req: Request, res: Response) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ const { id } = req.params;
+ const snippetId = parseInt(id, 10);
+
+ if (!isNonEmptyString(userId) || isNaN(snippetId)) {
+ authLogger.warn("Invalid request for snippet fetch: invalid ID", {
+ userId,
+ id,
+ });
+ return res.status(400).json({ error: "Invalid request parameters" });
+ }
+
+ try {
+ const result = await db
+ .select()
+ .from(snippets)
+ .where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
+
+ if (result.length === 0) {
+ return res.status(404).json({ error: "Snippet not found" });
+ }
+
+ res.json(result[0]);
+ } catch (err) {
+ authLogger.error("Failed to fetch snippet", err);
+ res.status(500).json({
+ error: err instanceof Error ? err.message : "Failed to fetch snippet",
+ });
+ }
+ },
+);
+
+// Create a new snippet
+// POST /snippets
+router.post(
+ "/",
+ authenticateJWT,
+ requireDataAccess,
+ async (req: Request, res: Response) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ const { name, content, description } = req.body;
+
+ if (
+ !isNonEmptyString(userId) ||
+ !isNonEmptyString(name) ||
+ !isNonEmptyString(content)
+ ) {
+ authLogger.warn("Invalid snippet creation data validation failed", {
+ operation: "snippet_create",
+ userId,
+ hasName: !!name,
+ hasContent: !!content,
+ });
+ return res.status(400).json({ error: "Name and content are required" });
+ }
+
+ try {
+ const insertData = {
+ userId,
+ name: name.trim(),
+ content: content.trim(),
+ description: description?.trim() || null,
+ };
+
+ const result = await db.insert(snippets).values(insertData).returning();
+
+ authLogger.success(`Snippet created: ${name} by user ${userId}`, {
+ operation: "snippet_create_success",
+ userId,
+ snippetId: result[0].id,
+ name,
+ });
+
+ res.status(201).json(result[0]);
+ } catch (err) {
+ authLogger.error("Failed to create snippet", err);
+ res.status(500).json({
+ error: err instanceof Error ? err.message : "Failed to create snippet",
+ });
+ }
+ },
+);
+
+// Update a snippet
+// PUT /snippets/:id
+router.put(
+ "/:id",
+ authenticateJWT,
+ requireDataAccess,
+ async (req: Request, res: Response) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ const { id } = req.params;
+ const updateData = req.body;
+
+ if (!isNonEmptyString(userId) || !id) {
+ authLogger.warn("Invalid request for snippet update");
+ return res.status(400).json({ error: "Invalid request" });
+ }
+
+ try {
+ const existing = await db
+ .select()
+ .from(snippets)
+ .where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
+
+ if (existing.length === 0) {
+ return res.status(404).json({ error: "Snippet not found" });
+ }
+
+ const updateFields: Partial<{
+ updatedAt: ReturnType;
+ name: string;
+ content: string;
+ description: string | null;
+ }> = {
+ updatedAt: sql`CURRENT_TIMESTAMP`,
+ };
+
+ if (updateData.name !== undefined)
+ updateFields.name = updateData.name.trim();
+ if (updateData.content !== undefined)
+ updateFields.content = updateData.content.trim();
+ if (updateData.description !== undefined)
+ updateFields.description = updateData.description?.trim() || null;
+
+ await db
+ .update(snippets)
+ .set(updateFields)
+ .where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
+
+ const updated = await db
+ .select()
+ .from(snippets)
+ .where(eq(snippets.id, parseInt(id)));
+
+ authLogger.success(
+ `Snippet updated: ${updated[0].name} by user ${userId}`,
+ {
+ operation: "snippet_update_success",
+ userId,
+ snippetId: parseInt(id),
+ name: updated[0].name,
+ },
+ );
+
+ res.json(updated[0]);
+ } catch (err) {
+ authLogger.error("Failed to update snippet", err);
+ res.status(500).json({
+ error: err instanceof Error ? err.message : "Failed to update snippet",
+ });
+ }
+ },
+);
+
+// Delete a snippet
+// DELETE /snippets/:id
+router.delete(
+ "/:id",
+ authenticateJWT,
+ requireDataAccess,
+ async (req: Request, res: Response) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ const { id } = req.params;
+
+ if (!isNonEmptyString(userId) || !id) {
+ authLogger.warn("Invalid request for snippet delete");
+ return res.status(400).json({ error: "Invalid request" });
+ }
+
+ try {
+ const existing = await db
+ .select()
+ .from(snippets)
+ .where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
+
+ if (existing.length === 0) {
+ return res.status(404).json({ error: "Snippet not found" });
+ }
+
+ await db
+ .delete(snippets)
+ .where(and(eq(snippets.id, parseInt(id)), eq(snippets.userId, userId)));
+
+ authLogger.success(
+ `Snippet deleted: ${existing[0].name} by user ${userId}`,
+ {
+ operation: "snippet_delete_success",
+ userId,
+ snippetId: parseInt(id),
+ name: existing[0].name,
+ },
+ );
+
+ res.json({ success: true });
+ } catch (err) {
+ authLogger.error("Failed to delete snippet", err);
+ res.status(500).json({
+ error: err instanceof Error ? err.message : "Failed to delete snippet",
+ });
+ }
+ },
+);
+
+export default router;
diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts
index cdaf7668..8e9cf570 100644
--- a/src/backend/database/routes/ssh.ts
+++ b/src/backend/database/routes/ssh.ts
@@ -1,3 +1,4 @@
+import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express";
import { db } from "../db/index.js";
import {
@@ -9,8 +10,7 @@ import {
fileManagerShortcuts,
} from "../db/schema.js";
import { eq, and, desc, isNotNull, or } from "drizzle-orm";
-import type { Request, Response, NextFunction } from "express";
-import jwt from "jsonwebtoken";
+import type { Request, Response } from "express";
import multer from "multer";
import { sshLogger } from "../../utils/logger.js";
import { SimpleDBOps } from "../../utils/simple-db-ops.js";
@@ -23,11 +23,11 @@ const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
-function isNonEmptyString(value: any): value is string {
+function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
-function isValidPort(port: any): port is number {
+function isValidPort(port: unknown): port is number {
return typeof port === "number" && port > 0 && port <= 65535;
}
@@ -75,7 +75,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
: [];
const hasAutoStartTunnels = tunnelConnections.some(
- (tunnel: any) => tunnel.autoStart,
+ (tunnel: Record) => tunnel.autoStart,
);
if (!hasAutoStartTunnels) {
@@ -100,7 +100,7 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
credentialId: host.credentialId,
enableTunnel: true,
tunnelConnections: tunnelConnections.filter(
- (tunnel: any) => tunnel.autoStart,
+ (tunnel: Record) => tunnel.autoStart,
),
pin: !!host.pin,
enableTerminal: !!host.enableTerminal,
@@ -184,8 +184,8 @@ router.post(
requireDataAccess,
upload.single("key"),
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
- let hostData: any;
+ const userId = (req as AuthenticatedRequest).userId;
+ let hostData: Record;
if (req.headers["content-type"]?.includes("multipart/form-data")) {
if (req.body.data) {
@@ -234,6 +234,9 @@ router.post(
enableFileManager,
defaultPath,
tunnelConnections,
+ statsConfig,
+ terminalConfig,
+ forceKeyboardInteractive,
} = hostData;
if (
!isNonEmptyString(userId) ||
@@ -251,7 +254,7 @@ router.post(
}
const effectiveAuthType = authType || authMethod;
- const sshDataObj: any = {
+ const sshDataObj: Record = {
userId: userId,
name,
folder: folder || null,
@@ -269,6 +272,9 @@ router.post(
: null,
enableFileManager: enableFileManager ? 1 : 0,
defaultPath: defaultPath || null,
+ statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
+ terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
+ forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
};
if (effectiveAuthType === "password") {
@@ -320,9 +326,12 @@ router.post(
enableTerminal: !!createdHost.enableTerminal,
enableTunnel: !!createdHost.enableTunnel,
tunnelConnections: createdHost.tunnelConnections
- ? JSON.parse(createdHost.tunnelConnections)
+ ? JSON.parse(createdHost.tunnelConnections as string)
: [],
enableFileManager: !!createdHost.enableFileManager,
+ statsConfig: createdHost.statsConfig
+ ? JSON.parse(createdHost.statsConfig as string)
+ : undefined,
};
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
@@ -332,7 +341,7 @@ router.post(
{
operation: "host_create_success",
userId,
- hostId: createdHost.id,
+ hostId: createdHost.id as number,
name,
ip,
port,
@@ -363,8 +372,8 @@ router.put(
upload.single("key"),
async (req: Request, res: Response) => {
const hostId = req.params.id;
- const userId = (req as any).userId;
- let hostData: any;
+ const userId = (req as AuthenticatedRequest).userId;
+ let hostData: Record;
if (req.headers["content-type"]?.includes("multipart/form-data")) {
if (req.body.data) {
@@ -415,6 +424,9 @@ router.put(
enableFileManager,
defaultPath,
tunnelConnections,
+ statsConfig,
+ terminalConfig,
+ forceKeyboardInteractive,
} = hostData;
if (
!isNonEmptyString(userId) ||
@@ -434,7 +446,7 @@ router.put(
}
const effectiveAuthType = authType || authMethod;
- const sshDataObj: any = {
+ const sshDataObj: Record = {
name,
folder,
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
@@ -451,6 +463,9 @@ router.put(
: null,
enableFileManager: enableFileManager ? 1 : 0,
defaultPath: defaultPath || null,
+ statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
+ terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
+ forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
};
if (effectiveAuthType === "password") {
@@ -520,9 +535,12 @@ router.put(
enableTerminal: !!updatedHost.enableTerminal,
enableTunnel: !!updatedHost.enableTunnel,
tunnelConnections: updatedHost.tunnelConnections
- ? JSON.parse(updatedHost.tunnelConnections)
+ ? JSON.parse(updatedHost.tunnelConnections as string)
: [],
enableFileManager: !!updatedHost.enableFileManager,
+ statsConfig: updatedHost.statsConfig
+ ? JSON.parse(updatedHost.statsConfig as string)
+ : undefined,
};
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
@@ -559,7 +577,7 @@ router.put(
// 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;
+ const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId)) {
sshLogger.warn("Invalid userId for SSH data fetch", {
operation: "host_fetch",
@@ -575,7 +593,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
);
const result = await Promise.all(
- data.map(async (row: any) => {
+ data.map(async (row: Record) => {
const baseHost = {
...row,
tags:
@@ -588,9 +606,16 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
enableTerminal: !!row.enableTerminal,
enableTunnel: !!row.enableTunnel,
tunnelConnections: row.tunnelConnections
- ? JSON.parse(row.tunnelConnections)
+ ? JSON.parse(row.tunnelConnections as string)
: [],
enableFileManager: !!row.enableFileManager,
+ statsConfig: row.statsConfig
+ ? JSON.parse(row.statsConfig as string)
+ : undefined,
+ terminalConfig: row.terminalConfig
+ ? JSON.parse(row.terminalConfig as string)
+ : undefined,
+ forceKeyboardInteractive: row.forceKeyboardInteractive === "true",
};
return (await resolveHostCredentials(baseHost)) || baseHost;
@@ -614,7 +639,7 @@ router.get(
authenticateJWT,
async (req: Request, res: Response) => {
const hostId = req.params.id;
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId) || !hostId) {
sshLogger.warn("Invalid userId or hostId for SSH host fetch by ID", {
@@ -655,6 +680,13 @@ router.get(
? JSON.parse(host.tunnelConnections)
: [],
enableFileManager: !!host.enableFileManager,
+ statsConfig: host.statsConfig
+ ? JSON.parse(host.statsConfig)
+ : undefined,
+ terminalConfig: host.terminalConfig
+ ? JSON.parse(host.terminalConfig)
+ : undefined,
+ forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
};
res.json((await resolveHostCredentials(result)) || result);
@@ -677,7 +709,7 @@ router.get(
requireDataAccess,
async (req: Request, res: Response) => {
const hostId = req.params.id;
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!isNonEmptyString(userId) || !hostId) {
return res.status(400).json({ error: "Invalid userId or hostId" });
@@ -711,7 +743,7 @@ router.get(
authType: resolvedHost.authType,
password: resolvedHost.password || null,
key: resolvedHost.key || null,
- keyPassword: resolvedHost.keyPassword || null,
+ keyPassword: resolvedHost.key_password || null,
keyType: resolvedHost.keyType || null,
folder: resolvedHost.folder,
tags:
@@ -724,7 +756,7 @@ router.get(
enableFileManager: !!resolvedHost.enableFileManager,
defaultPath: resolvedHost.defaultPath,
tunnelConnections: resolvedHost.tunnelConnections
- ? JSON.parse(resolvedHost.tunnelConnections)
+ ? JSON.parse(resolvedHost.tunnelConnections as string)
: [],
};
@@ -752,7 +784,7 @@ router.delete(
"/db/host/:id",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const hostId = req.params.id;
if (!isNonEmptyString(userId) || !hostId) {
@@ -816,7 +848,7 @@ router.delete(
),
);
- const result = await db
+ await db
.delete(sshData)
.where(and(eq(sshData.id, numericHostId), eq(sshData.userId, userId)));
@@ -851,7 +883,7 @@ router.get(
"/file_manager/recent",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId
? parseInt(req.query.hostId as string)
: null;
@@ -893,7 +925,7 @@ router.post(
"/file_manager/recent",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -942,8 +974,8 @@ router.delete(
"/file_manager/recent",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
- const { hostId, path, name } = req.body;
+ const userId = (req as AuthenticatedRequest).userId;
+ const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) {
sshLogger.warn("Invalid data for recent file deletion");
@@ -975,7 +1007,7 @@ router.get(
"/file_manager/pinned",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId
? parseInt(req.query.hostId as string)
: null;
@@ -1016,7 +1048,7 @@ router.post(
"/file_manager/pinned",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1062,8 +1094,8 @@ router.delete(
"/file_manager/pinned",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
- const { hostId, path, name } = req.body;
+ const userId = (req as AuthenticatedRequest).userId;
+ const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) {
sshLogger.warn("Invalid data for pinned file deletion");
@@ -1095,7 +1127,7 @@ router.get(
"/file_manager/shortcuts",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const hostId = req.query.hostId
? parseInt(req.query.hostId as string)
: null;
@@ -1136,7 +1168,7 @@ router.post(
"/file_manager/shortcuts",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { hostId, path, name } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) {
@@ -1182,8 +1214,8 @@ router.delete(
"/file_manager/shortcuts",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
- const { hostId, path, name } = req.body;
+ const userId = (req as AuthenticatedRequest).userId;
+ const { hostId, path } = req.body;
if (!isNonEmptyString(userId) || !hostId || !path) {
sshLogger.warn("Invalid data for shortcut deletion");
@@ -1209,21 +1241,26 @@ router.delete(
},
);
-async function resolveHostCredentials(host: any): Promise {
+async function resolveHostCredentials(
+ host: Record,
+): Promise> {
try {
if (host.credentialId && host.userId) {
+ const credentialId = host.credentialId as number;
+ const userId = host.userId as string;
+
const credentials = await SimpleDBOps.select(
db
.select()
.from(sshCredentials)
.where(
and(
- eq(sshCredentials.id, host.credentialId),
- eq(sshCredentials.userId, host.userId),
+ eq(sshCredentials.id, credentialId),
+ eq(sshCredentials.userId, userId),
),
),
"ssh_credentials",
- host.userId,
+ userId,
);
if (credentials.length > 0) {
@@ -1239,6 +1276,7 @@ async function resolveHostCredentials(host: any): Promise {
};
}
}
+
const result = { ...host };
if (host.key_password !== undefined) {
if (result.keyPassword === undefined) {
@@ -1261,7 +1299,7 @@ router.put(
"/folders/rename",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { oldName, newName } = req.body;
if (!isNonEmptyString(userId) || !oldName || !newName) {
@@ -1326,7 +1364,7 @@ router.post(
"/bulk-import",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { hosts } = req.body;
if (!Array.isArray(hosts) || hosts.length === 0) {
@@ -1398,7 +1436,7 @@ router.post(
continue;
}
- const sshDataObj: any = {
+ const sshDataObj: Record = {
userId: userId,
name: hostData.name || `${hostData.username}@${hostData.ip}`,
folder: hostData.folder || "Default",
@@ -1411,7 +1449,7 @@ router.post(
credentialId:
hostData.authType === "credential" ? hostData.credentialId : null,
key: hostData.authType === "key" ? hostData.key : null,
- key_password:
+ keyPassword:
hostData.authType === "key"
? hostData.keyPassword || hostData.key_password || null
: null,
@@ -1425,6 +1463,9 @@ router.post(
tunnelConnections: hostData.tunnelConnections
? JSON.stringify(hostData.tunnelConnections)
: "[]",
+ statsConfig: hostData.statsConfig
+ ? JSON.stringify(hostData.statsConfig)
+ : null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
@@ -1455,7 +1496,7 @@ router.post(
authenticateJWT,
requireDataAccess,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { sshConfigId } = req.body;
if (!sshConfigId || typeof sshConfigId !== "number") {
@@ -1519,7 +1560,7 @@ router.post(
const tunnelConnections = JSON.parse(config.tunnelConnections);
const resolvedConnections = await Promise.all(
- tunnelConnections.map(async (tunnel: any) => {
+ tunnelConnections.map(async (tunnel: Record) => {
if (
tunnel.autoStart &&
tunnel.endpointHost &&
@@ -1567,7 +1608,7 @@ router.post(
}
}
- const updateResult = await db
+ await db
.update(sshData)
.set({
autostartPassword: decryptedConfig.password || null,
@@ -1608,7 +1649,7 @@ router.delete(
"/autostart/disable",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { sshConfigId } = req.body;
if (!sshConfigId || typeof sshConfigId !== "number") {
@@ -1624,7 +1665,7 @@ router.delete(
}
try {
- const result = await db
+ await db
.update(sshData)
.set({
autostartPassword: null,
@@ -1654,7 +1695,7 @@ router.get(
"/autostart/status",
authenticateJWT,
async (req: Request, res: Response) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
try {
const autostartConfigs = await db
diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts
index 91f761e2..c2b2ac03 100644
--- a/src/backend/database/routes/users.ts
+++ b/src/backend/database/routes/users.ts
@@ -1,14 +1,20 @@
+import type { AuthenticatedRequest } from "../../../types/index.js";
import express from "express";
import crypto from "crypto";
import { db } from "../db/index.js";
import {
users,
+ sessions,
sshData,
+ sshCredentials,
fileManagerRecent,
fileManagerPinned,
fileManagerShortcuts,
dismissedAlerts,
settings,
+ sshCredentialUsage,
+ recentActivity,
+ snippets,
} from "../db/schema.js";
import { eq, and } from "drizzle-orm";
import bcrypt from "bcryptjs";
@@ -20,6 +26,7 @@ import { authLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-manager.js";
import { DataCrypto } from "../../utils/data-crypto.js";
import { LazyFieldEncryption } from "../../utils/lazy-field-encryption.js";
+import { parseUserAgent } from "../../utils/user-agent-parser.js";
const authManager = AuthManager.getInstance();
@@ -27,112 +34,104 @@ async function verifyOIDCToken(
idToken: string,
issuerUrl: string,
clientId: string,
-): Promise {
+): Promise> {
+ 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 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) {
- 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 {
- }
- } catch (error) {
- continue;
+ const discoveryUrl = `${normalizedIssuerUrl}/.well-known/openid-configuration`;
+ const discoveryResponse = await fetch(discoveryUrl);
+ if (discoveryResponse.ok) {
+ const discovery = (await discoveryResponse.json()) as Record<
+ string,
+ unknown
+ >;
+ if (discovery.jwks_uri) {
+ jwksUrls.unshift(discovery.jwks_uri as string);
}
}
-
- 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) {
- throw error;
+ } catch (discoveryError) {
+ authLogger.error(`OIDC discovery failed: ${discoveryError}`);
}
+
+ let jwks: Record | null = null;
+
+ for (const url of jwksUrls) {
+ try {
+ const response = await fetch(url);
+ if (response.ok) {
+ const jwksData = (await response.json()) as Record;
+ if (jwksData && jwksData.keys && Array.isArray(jwksData.keys)) {
+ jwks = jwksData;
+ break;
+ } else {
+ authLogger.error(
+ `Invalid JWKS structure from ${url}: ${JSON.stringify(jwksData)}`,
+ );
+ }
+ } else {
+ }
+ } catch {
+ 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: Record) => key.kid === keyId,
+ );
+ if (!publicKey) {
+ throw new Error(
+ `No matching public key found for key ID: ${keyId}. Available keys: ${jwks.keys.map((k: Record) => 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;
}
const router = express.Router();
-function isNonEmptyString(val: any): val is string {
+function isNonEmptyString(val: unknown): val is string {
return typeof val === "string" && val.trim().length > 0;
}
-interface JWTPayload {
- userId: string;
- iat?: number;
- exp?: number;
-}
-
const authenticateJWT = authManager.createAuthMiddleware();
const requireAdmin = authManager.createAdminMiddleware();
-const requireDataAccess = authManager.createDataAccessMiddleware();
// Route: Create traditional user (username/password)
// POST /users/create
@@ -141,7 +140,7 @@ router.post("/create", async (req, res) => {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
.get();
- if (row && (row as any).value !== "true") {
+ if (row && (row as Record).value !== "true") {
return res
.status(403)
.json({ error: "Registration is currently disabled" });
@@ -186,7 +185,7 @@ router.post("/create", async (req, res) => {
const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users")
.get();
- isFirstUser = ((countResult as any)?.count || 0) === 0;
+ isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
const saltRounds = parseInt(process.env.SALT || "10", 10);
const password_hash = await bcrypt.hash(password, saltRounds);
@@ -250,7 +249,7 @@ router.post("/create", async (req, res) => {
// 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;
+ const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) {
@@ -337,14 +336,10 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
userId,
adminDataKey,
);
- authLogger.info("OIDC configuration encrypted with admin data key", {
- operation: "oidc_config_encrypt",
- userId,
- });
} else {
encryptedConfig = {
...config,
- client_secret: `encrypted:${Buffer.from(client_secret).toString("base64")}`, // Simple base64 encoding
+ client_secret: `encrypted:${Buffer.from(client_secret).toString("base64")}`,
};
authLogger.warn(
"OIDC configuration stored with basic encoding - admin should re-save with password",
@@ -390,7 +385,7 @@ router.post("/oidc-config", authenticateJWT, async (req, res) => {
// Route: Disable OIDC configuration (admin only)
// DELETE /users/oidc-config
router.delete("/oidc-config", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) {
@@ -420,69 +415,76 @@ router.get("/oidc-config", async (req, res) => {
return res.json(null);
}
- let config = JSON.parse((row as any).value);
+ const config = JSON.parse((row as Record).value as string);
- if (config.client_secret) {
- if (config.client_secret.startsWith("encrypted:")) {
- const authHeader = req.headers["authorization"];
- if (authHeader?.startsWith("Bearer ")) {
- const token = authHeader.split(" ")[1];
- const authManager = AuthManager.getInstance();
- const payload = await authManager.verifyJWTToken(token);
+ const publicConfig = {
+ client_id: config.client_id,
+ issuer_url: config.issuer_url,
+ authorization_url: config.authorization_url,
+ scopes: config.scopes,
+ };
- if (payload) {
- const userId = payload.userId;
- const user = await db
- .select()
- .from(users)
- .where(eq(users.id, userId));
+ return res.json(publicConfig);
+ } catch (err) {
+ authLogger.error("Failed to get OIDC config", err);
+ res.status(500).json({ error: "Failed to get OIDC config" });
+ }
+});
- if (user && user.length > 0 && user[0].is_admin) {
- try {
- const adminDataKey = DataCrypto.getUserDataKey(userId);
- if (adminDataKey) {
- config = DataCrypto.decryptRecord(
- "settings",
- config,
- userId,
- adminDataKey,
- );
- } else {
- config.client_secret = "[ENCRYPTED - PASSWORD REQUIRED]";
- }
- } catch (decryptError) {
- authLogger.warn("Failed to decrypt OIDC config for admin", {
- operation: "oidc_config_decrypt_failed",
- userId,
- });
- config.client_secret = "[ENCRYPTED - DECRYPTION FAILED]";
- }
- } else {
- config.client_secret = "[ENCRYPTED - ADMIN ONLY]";
- }
- } else {
- config.client_secret = "[ENCRYPTED - AUTH REQUIRED]";
- }
+// Route: Get OIDC configuration for Admin (admin only)
+// GET /users/oidc-config/admin
+router.get("/oidc-config/admin", requireAdmin, async (req, res) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ try {
+ const row = db.$client
+ .prepare("SELECT value FROM settings WHERE key = 'oidc_config'")
+ .get();
+ if (!row) {
+ return res.json(null);
+ }
+
+ let config = JSON.parse((row as Record).value as string);
+
+ if (config.client_secret?.startsWith("encrypted:")) {
+ try {
+ const adminDataKey = DataCrypto.getUserDataKey(userId);
+ if (adminDataKey) {
+ config = DataCrypto.decryptRecord(
+ "settings",
+ config,
+ userId,
+ adminDataKey,
+ );
} else {
- config.client_secret = "[ENCRYPTED - AUTH REQUIRED]";
- }
- } else if (config.client_secret.startsWith("encoded:")) {
- try {
- const decoded = Buffer.from(
- config.client_secret.substring(8),
- "base64",
- ).toString("utf8");
- config.client_secret = decoded;
- } catch {
- config.client_secret = "[ENCODING ERROR]";
+ config.client_secret = "[ENCRYPTED - PASSWORD REQUIRED]";
}
+ } catch (decryptError) {
+ authLogger.warn("Failed to decrypt OIDC config for admin", {
+ operation: "oidc_config_decrypt_failed",
+ userId,
+ });
+ config.client_secret = "[ENCRYPTED - DECRYPTION FAILED]";
+ }
+ } else if (config.client_secret?.startsWith("encoded:")) {
+ try {
+ const decoded = Buffer.from(
+ config.client_secret.substring(8),
+ "base64",
+ ).toString("utf8");
+ config.client_secret = decoded;
+ } catch (decodeError) {
+ authLogger.warn("Failed to decode OIDC config for admin", {
+ operation: "oidc_config_decode_failed",
+ userId,
+ });
+ config.client_secret = "[ENCODING ERROR]";
}
}
res.json(config);
} catch (err) {
- authLogger.error("Failed to get OIDC config", err);
- res.status(500).json({ error: "Failed to get OIDC config" });
+ authLogger.error("Failed to get OIDC config for admin", err);
+ res.status(500).json({ error: "Failed to get OIDC config for admin" });
}
});
@@ -497,13 +499,13 @@ router.get("/oidc/authorize", async (req, res) => {
return res.status(404).json({ error: "OIDC not configured" });
}
- const config = JSON.parse((row as any).value);
+ const config = JSON.parse((row as Record).value as string);
const state = nanoid();
const nonce = nanoid();
let origin =
req.get("Origin") ||
- req.get("Referer")?.replace(/\/[^\/]*$/, "") ||
+ req.get("Referer")?.replace(/\/[^/]*$/, "") ||
"http://localhost:5173";
if (origin.includes("localhost")) {
@@ -552,7 +554,8 @@ router.get("/oidc/callback", async (req, res) => {
.status(400)
.json({ error: "Invalid state parameter - redirect URI not found" });
}
- const redirectUri = (storedRedirectRow as any).value;
+ const redirectUri = (storedRedirectRow as Record)
+ .value as string;
try {
const storedNonce = db.$client
@@ -576,7 +579,9 @@ router.get("/oidc/callback", async (req, res) => {
return res.status(500).json({ error: "OIDC not configured" });
}
- const config = JSON.parse((configRow as any).value);
+ const config = JSON.parse(
+ (configRow as Record).value as string,
+ );
const tokenResponse = await fetch(config.token_url, {
method: "POST",
@@ -602,26 +607,26 @@ router.get("/oidc/callback", async (req, res) => {
.json({ error: "Failed to exchange authorization code" });
}
- const tokenData = (await tokenResponse.json()) as any;
+ const tokenData = (await tokenResponse.json()) as Record;
- let userInfo: any = null;
- let userInfoUrls: string[] = [];
+ let userInfo: Record = null;
+ const userInfoUrls: string[] = [];
const normalizedIssuerUrl = config.issuer_url.endsWith("/")
? config.issuer_url.slice(0, -1)
: config.issuer_url;
- const baseUrl = normalizedIssuerUrl.replace(
- /\/application\/o\/[^\/]+$/,
- "",
- );
+ 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;
+ const discovery = (await discoveryResponse.json()) as Record<
+ string,
+ unknown
+ >;
if (discovery.userinfo_endpoint) {
- userInfoUrls.push(discovery.userinfo_endpoint);
+ userInfoUrls.push(discovery.userinfo_endpoint as string);
}
}
} catch (discoveryError) {
@@ -646,13 +651,13 @@ router.get("/oidc/callback", async (req, res) => {
if (tokenData.id_token) {
try {
userInfo = await verifyOIDCToken(
- tokenData.id_token,
+ tokenData.id_token as string,
config.issuer_url,
config.client_id,
);
- } catch (error) {
+ } catch {
try {
- const parts = tokenData.id_token.split(".");
+ const parts = (tokenData.id_token as string).split(".");
if (parts.length === 3) {
const payload = JSON.parse(
Buffer.from(parts[1], "base64").toString(),
@@ -675,7 +680,10 @@ router.get("/oidc/callback", async (req, res) => {
});
if (userInfoResponse.ok) {
- userInfo = await userInfoResponse.json();
+ userInfo = (await userInfoResponse.json()) as Record<
+ string,
+ unknown
+ >;
break;
} else {
authLogger.error(
@@ -698,24 +706,25 @@ router.get("/oidc/callback", async (req, res) => {
return res.status(400).json({ error: "Failed to get user information" });
}
- const getNestedValue = (obj: any, path: string): any => {
+ const getNestedValue = (
+ obj: Record,
+ path: string,
+ ): unknown => {
if (!path || !obj) return null;
return path.split(".").reduce((current, key) => current?.[key], obj);
};
- const identifier =
- getNestedValue(userInfo, config.identifier_path) ||
+ const identifier = (getNestedValue(userInfo, config.identifier_path) ||
userInfo[config.identifier_path] ||
userInfo.sub ||
userInfo.email ||
- userInfo.preferred_username;
+ userInfo.preferred_username) as string;
- const name =
- getNestedValue(userInfo, config.name_path) ||
+ const name = (getNestedValue(userInfo, config.name_path) ||
userInfo[config.name_path] ||
userInfo.name ||
userInfo.given_name ||
- identifier;
+ identifier) as string;
if (!identifier) {
authLogger.error(
@@ -739,7 +748,7 @@ router.get("/oidc/callback", async (req, res) => {
const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users")
.get();
- isFirstUser = ((countResult as any)?.count || 0) === 0;
+ isFirstUser = ((countResult as { count?: number })?.count || 0) === 0;
const id = nanoid();
await db.insert(users).values({
@@ -749,14 +758,14 @@ router.get("/oidc/callback", async (req, res) => {
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,
+ client_id: String(config.client_id),
+ client_secret: String(config.client_secret),
+ issuer_url: String(config.issuer_url),
+ authorization_url: String(config.authorization_url),
+ token_url: String(config.token_url),
+ identifier_path: String(config.identifier_path),
+ name_path: String(config.name_path),
+ scopes: String(config.scopes),
});
try {
@@ -797,11 +806,23 @@ router.get("/oidc/callback", async (req, res) => {
});
}
+ const deviceInfo = parseUserAgent(req);
const token = await authManager.generateJWTToken(userRecord.id, {
- expiresIn: "50d",
+ deviceType: deviceInfo.type,
+ deviceInfo: deviceInfo.deviceInfo,
});
- let frontendUrl = redirectUri.replace("/users/oidc/callback", "");
+ authLogger.success("OIDC user authenticated", {
+ operation: "oidc_login_success",
+ userId: userRecord.id,
+ deviceType: deviceInfo.type,
+ deviceInfo: deviceInfo.deviceInfo,
+ });
+
+ let frontendUrl = (redirectUri as string).replace(
+ "/users/oidc/callback",
+ "",
+ );
if (frontendUrl.includes("localhost")) {
frontendUrl = "http://localhost:5173";
@@ -810,17 +831,21 @@ router.get("/oidc/callback", async (req, res) => {
const redirectUrl = new URL(frontendUrl);
redirectUrl.searchParams.set("success", "true");
+ const maxAge =
+ deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
+ ? 30 * 24 * 60 * 60 * 1000
+ : 7 * 24 * 60 * 60 * 1000;
+
return res
- .cookie(
- "jwt",
- token,
- authManager.getSecureCookieOptions(req, 50 * 24 * 60 * 60 * 1000),
- )
+ .cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
.redirect(redirectUrl.toString());
} catch (err) {
authLogger.error("OIDC callback failed", err);
- let frontendUrl = redirectUri.replace("/users/oidc/callback", "");
+ let frontendUrl = (redirectUri as string).replace(
+ "/users/oidc/callback",
+ "",
+ );
if (frontendUrl.includes("localhost")) {
frontendUrl = "http://localhost:5173";
@@ -847,6 +872,23 @@ router.post("/login", async (req, res) => {
return res.status(400).json({ error: "Invalid username or password" });
}
+ try {
+ const row = db.$client
+ .prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
+ .get();
+ if (row && (row as { value: string }).value !== "true") {
+ return res
+ .status(403)
+ .json({ error: "Password authentication is currently disabled" });
+ }
+ } catch (e) {
+ authLogger.error("Failed to check password login status", {
+ operation: "login_check",
+ error: e,
+ });
+ return res.status(500).json({ error: "Failed to check login status" });
+ }
+
try {
const user = await db
.select()
@@ -893,7 +935,7 @@ router.post("/login", async (req, res) => {
if (kekSalt.length === 0) {
await authManager.registerUser(userRecord.id, password);
}
- } catch (setupError) {}
+ } catch {}
const dataUnlocked = await authManager.authenticateUser(
userRecord.id,
@@ -915,8 +957,10 @@ router.post("/login", async (req, res) => {
});
}
+ const deviceInfo = parseUserAgent(req);
const token = await authManager.generateJWTToken(userRecord.id, {
- expiresIn: "24h",
+ deviceType: deviceInfo.type,
+ deviceInfo: deviceInfo.deviceInfo,
});
authLogger.success(`User logged in successfully: ${username}`, {
@@ -924,9 +968,11 @@ router.post("/login", async (req, res) => {
username,
userId: userRecord.id,
dataUnlocked: true,
+ deviceType: deviceInfo.type,
+ deviceInfo: deviceInfo.deviceInfo,
});
- const response: any = {
+ const response: Record = {
success: true,
is_admin: !!userRecord.is_admin,
username: userRecord.username,
@@ -940,12 +986,13 @@ router.post("/login", async (req, res) => {
response.token = token;
}
+ const maxAge =
+ deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
+ ? 30 * 24 * 60 * 60 * 1000
+ : 7 * 24 * 60 * 60 * 1000;
+
return res
- .cookie(
- "jwt",
- token,
- authManager.getSecureCookieOptions(req, 24 * 60 * 60 * 1000),
- )
+ .cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
.json(response);
} catch (err) {
authLogger.error("Failed to log in user", err);
@@ -955,15 +1002,28 @@ router.post("/login", async (req, res) => {
// Route: Logout user
// POST /users/logout
-router.post("/logout", async (req, res) => {
+router.post("/logout", authenticateJWT, async (req, res) => {
try {
- const userId = (req as any).userId;
+ const authReq = req as AuthenticatedRequest;
+ const userId = authReq.userId;
if (userId) {
- authManager.logoutUser(userId);
+ const token =
+ req.cookies?.jwt || req.headers["authorization"]?.split(" ")[1];
+ let sessionId: string | undefined;
+
+ if (token) {
+ try {
+ const payload = await authManager.verifyJWTToken(token);
+ sessionId = payload?.sessionId;
+ } catch (error) {}
+ }
+
+ await authManager.logoutUser(userId, sessionId);
authLogger.info("User logged out", {
operation: "user_logout",
userId,
+ sessionId,
});
}
@@ -979,7 +1039,8 @@ router.post("/logout", async (req, res) => {
// 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;
+ const userId = (req as AuthenticatedRequest).userId;
+
if (!isNonEmptyString(userId)) {
authLogger.warn("Invalid userId in JWT for /users/me");
return res.status(401).json({ error: "Invalid userId" });
@@ -991,15 +1052,12 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
return res.status(401).json({ error: "User not found" });
}
- const isDataUnlocked = authManager.isUserUnlocked(userId);
-
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,
- data_unlocked: isDataUnlocked,
});
} catch (err) {
authLogger.error("Failed to get username", err);
@@ -1014,7 +1072,7 @@ router.get("/setup-required", async (req, res) => {
const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users")
.get();
- const count = (countResult as any)?.count || 0;
+ const count = (countResult as { count?: number })?.count || 0;
res.json({
setup_required: count === 0,
@@ -1028,7 +1086,7 @@ router.get("/setup-required", async (req, res) => {
// Route: Count users (admin only - for dashboard statistics)
// GET /users/count
router.get("/count", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user[0] || !user[0].is_admin) {
@@ -1038,7 +1096,7 @@ router.get("/count", authenticateJWT, async (req, res) => {
const countResult = db.$client
.prepare("SELECT COUNT(*) as count FROM users")
.get();
- const count = (countResult as any)?.count || 0;
+ const count = (countResult as { count?: number })?.count || 0;
res.json({ count });
} catch (err) {
authLogger.error("Failed to count users", err);
@@ -1065,7 +1123,9 @@ router.get("/registration-allowed", async (req, res) => {
const row = db.$client
.prepare("SELECT value FROM settings WHERE key = 'allow_registration'")
.get();
- res.json({ allowed: row ? (row as any).value === "true" : true });
+ res.json({
+ allowed: row ? (row as Record).value === "true" : true,
+ });
} catch (err) {
authLogger.error("Failed to get registration allowed", err);
res.status(500).json({ error: "Failed to get registration allowed" });
@@ -1075,7 +1135,7 @@ router.get("/registration-allowed", async (req, res) => {
// 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;
+ const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) {
@@ -1095,10 +1155,51 @@ router.patch("/registration-allowed", authenticateJWT, async (req, res) => {
}
});
+// Route: Get password login allowed status (public - needed for login page)
+// GET /users/password-login-allowed
+router.get("/password-login-allowed", async (req, res) => {
+ try {
+ const row = db.$client
+ .prepare("SELECT value FROM settings WHERE key = 'allow_password_login'")
+ .get();
+ res.json({
+ allowed: row ? (row as { value: string }).value === "true" : true,
+ });
+ } catch (err) {
+ authLogger.error("Failed to get password login allowed", err);
+ res.status(500).json({ error: "Failed to get password login allowed" });
+ }
+});
+
+// Route: Set password login allowed status (admin only)
+// PATCH /users/password-login-allowed
+router.patch("/password-login-allowed", authenticateJWT, async (req, res) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ try {
+ const user = await db.select().from(users).where(eq(users.id, userId));
+ if (!user || user.length === 0 || !user[0].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(
+ "INSERT OR REPLACE INTO settings (key, value) VALUES ('allow_password_login', ?)",
+ )
+ .run(allowed ? "true" : "false");
+ res.json({ allowed });
+ } catch (err) {
+ authLogger.error("Failed to set password login allowed", err);
+ res.status(500).json({ error: "Failed to set password login allowed" });
+ }
+});
+
// Route: Delete user account
// DELETE /users/delete-account
router.delete("/delete-account", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body;
if (!isNonEmptyString(password)) {
@@ -1134,7 +1235,7 @@ router.delete("/delete-account", authenticateJWT, async (req, res) => {
const adminCount = db.$client
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
.get();
- if ((adminCount as any)?.count <= 1) {
+ if (((adminCount as { count?: number })?.count || 0) <= 1) {
return res
.status(403)
.json({ error: "Cannot delete the last admin user" });
@@ -1224,7 +1325,9 @@ router.post("/verify-reset-code", async (req, res) => {
.json({ error: "No reset code found for this user" });
}
- const resetData = JSON.parse((resetDataRow as any).value);
+ const resetData = JSON.parse(
+ (resetDataRow as Record).value as string,
+ );
const now = new Date();
const expiresAt = new Date(resetData.expiresAt);
@@ -1282,7 +1385,9 @@ router.post("/complete-reset", async (req, res) => {
return res.status(400).json({ error: "No temporary token found" });
}
- const tempTokenData = JSON.parse((tempTokenRow as any).value);
+ const tempTokenData = JSON.parse(
+ (tempTokenRow as Record).value as string,
+ );
const now = new Date();
const expiresAt = new Date(tempTokenData.expiresAt);
@@ -1309,79 +1414,128 @@ router.post("/complete-reset", async (req, res) => {
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));
+ let userIdFromJwt: string | null = null;
+ const cookie = req.cookies?.jwt;
+ let header: string | undefined;
+ if (req.headers?.authorization?.startsWith("Bearer ")) {
+ header = req.headers?.authorization?.split(" ")[1];
+ }
+ const token = cookie || header;
- try {
- const hasActiveSession = authManager.isUserUnlocked(userId);
+ if (token) {
+ const payload = await authManager.verifyJWTToken(token);
+ if (payload) {
+ userIdFromJwt = payload.userId;
+ }
+ }
- if (hasActiveSession) {
+ if (userIdFromJwt === userId) {
+ try {
const success = await authManager.resetUserPasswordWithPreservedDEK(
userId,
newPassword,
);
if (!success) {
- authLogger.warn(
- `Failed to preserve DEK during password reset for ${username}. Creating new DEK - data will be lost.`,
- {
- operation: "password_reset_preserve_failed",
- userId,
- username,
- },
- );
- await authManager.registerUser(userId, newPassword);
- authManager.logoutUser(userId);
- } else {
- authLogger.success(
- `Password reset completed for user: ${username}. Data preserved using existing session.`,
- {
- operation: "password_reset_data_preserved",
- userId,
- username,
- },
- );
+ throw new Error("Failed to re-encrypt user data with new password.");
}
- } else {
- await authManager.registerUser(userId, newPassword);
- authManager.logoutUser(userId);
- authLogger.warn(
- `Password reset completed for user: ${username}. Existing encrypted data is now inaccessible and will need to be re-entered.`,
+ await db
+ .update(users)
+ .set({ password_hash })
+ .where(eq(users.id, userId));
+ authManager.logoutUser(userId);
+ authLogger.success(
+ `Password reset (data preserved) for user: ${username}`,
{
- operation: "password_reset_data_inaccessible",
+ operation: "password_reset_preserved",
userId,
username,
},
);
+ } catch (encryptionError) {
+ authLogger.error(
+ "Failed to setup user data encryption after password reset",
+ encryptionError,
+ {
+ operation: "password_reset_encryption_failed_preserved",
+ userId,
+ username,
+ },
+ );
+ return res.status(500).json({
+ error: "Password reset failed. Please contact administrator.",
+ });
}
-
+ } else {
await db
.update(users)
- .set({
- totp_enabled: false,
- totp_secret: null,
- totp_backup_codes: null,
- })
- .where(eq(users.id, userId));
- } catch (encryptionError) {
- authLogger.error(
- "Failed to re-encrypt user data after password reset",
- encryptionError,
- {
- operation: "password_reset_encryption_failed",
- userId,
- username,
- },
- );
- return res.status(500).json({
- error:
- "Password reset completed but user data encryption failed. Please contact administrator.",
- });
+ .set({ password_hash })
+ .where(eq(users.username, username));
+
+ try {
+ await db
+ .delete(sshCredentialUsage)
+ .where(eq(sshCredentialUsage.userId, userId));
+ await db
+ .delete(fileManagerRecent)
+ .where(eq(fileManagerRecent.userId, userId));
+ await db
+ .delete(fileManagerPinned)
+ .where(eq(fileManagerPinned.userId, userId));
+ await db
+ .delete(fileManagerShortcuts)
+ .where(eq(fileManagerShortcuts.userId, userId));
+ await db
+ .delete(recentActivity)
+ .where(eq(recentActivity.userId, userId));
+ await db
+ .delete(dismissedAlerts)
+ .where(eq(dismissedAlerts.userId, userId));
+ await db.delete(snippets).where(eq(snippets.userId, userId));
+ await db.delete(sshData).where(eq(sshData.userId, userId));
+ await db
+ .delete(sshCredentials)
+ .where(eq(sshCredentials.userId, userId));
+
+ await authManager.registerUser(userId, newPassword);
+ authManager.logoutUser(userId);
+
+ await db
+ .update(users)
+ .set({
+ totp_enabled: false,
+ totp_secret: null,
+ totp_backup_codes: null,
+ })
+ .where(eq(users.id, userId));
+
+ authLogger.warn(
+ `Password reset completed for user: ${username}. All encrypted data has been deleted due to lost encryption key.`,
+ {
+ operation: "password_reset_data_deleted",
+ userId,
+ username,
+ },
+ );
+ } catch (encryptionError) {
+ authLogger.error(
+ "Failed to setup user data encryption after password reset",
+ encryptionError,
+ {
+ operation: "password_reset_encryption_failed",
+ userId,
+ username,
+ },
+ );
+ return res.status(500).json({
+ error: "Password reset failed. Please contact administrator.",
+ });
+ }
}
+ authLogger.success(`Password successfully reset for user: ${username}`);
+
db.$client
.prepare("DELETE FROM settings WHERE key = ?")
.run(`reset_code_${username}`);
@@ -1396,10 +1550,54 @@ router.post("/complete-reset", async (req, res) => {
}
});
+router.post("/change-password", authenticateJWT, async (req, res) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ const { oldPassword, newPassword } = req.body;
+
+ if (!userId) {
+ return res.status(401).json({ error: "User not authenticated" });
+ }
+
+ if (!oldPassword || !newPassword) {
+ return res
+ .status(400)
+ .json({ error: "Old and new passwords are required." });
+ }
+
+ 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 isMatch = await bcrypt.compare(oldPassword, user[0].password_hash);
+ if (!isMatch) {
+ return res.status(401).json({ error: "Incorrect current password" });
+ }
+
+ const success = await authManager.changeUserPassword(
+ userId,
+ oldPassword,
+ newPassword,
+ );
+ if (!success) {
+ return res
+ .status(500)
+ .json({ error: "Failed to update password and re-encrypt data." });
+ }
+
+ 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.id, userId));
+
+ authManager.logoutUser(userId);
+
+ res.json({ message: "Password changed successfully. Please log in again." });
+});
+
// Route: List all users (admin only)
// GET /users/list
router.get("/list", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
if (!user || user.length === 0 || !user[0].is_admin) {
@@ -1425,7 +1623,7 @@ router.get("/list", authenticateJWT, async (req, res) => {
// 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 userId = (req as AuthenticatedRequest).userId;
const { username } = req.body;
if (!isNonEmptyString(username)) {
@@ -1468,7 +1666,7 @@ router.post("/make-admin", authenticateJWT, async (req, res) => {
// 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 userId = (req as AuthenticatedRequest).userId;
const { username } = req.body;
if (!isNonEmptyString(username)) {
@@ -1587,7 +1785,7 @@ router.post("/totp/verify-login", async (req, res) => {
backupCodes = userRecord.totp_backup_codes
? JSON.parse(userRecord.totp_backup_codes)
: [];
- } catch (parseError) {
+ } catch {
backupCodes = [];
}
@@ -1608,43 +1806,43 @@ router.post("/totp/verify-login", async (req, res) => {
.where(eq(users.id, userRecord.id));
}
+ const deviceInfo = parseUserAgent(req);
const token = await authManager.generateJWTToken(userRecord.id, {
- expiresIn: "50d",
+ deviceType: deviceInfo.type,
+ deviceInfo: deviceInfo.deviceInfo,
});
const isElectron =
req.headers["x-electron-app"] === "true" ||
req.headers["X-Electron-App"] === "true";
- const isDataUnlocked = authManager.isUserUnlocked(userRecord.id);
+ authLogger.success("TOTP verification successful", {
+ operation: "totp_verify_success",
+ userId: userRecord.id,
+ deviceType: deviceInfo.type,
+ deviceInfo: deviceInfo.deviceInfo,
+ });
- if (!isDataUnlocked) {
- return res.status(401).json({
- error: "Session expired - please log in again",
- code: "SESSION_EXPIRED",
- });
- }
-
- const response: any = {
+ const response: Record = {
success: true,
is_admin: !!userRecord.is_admin,
username: userRecord.username,
userId: userRecord.id,
is_oidc: !!userRecord.is_oidc,
totp_enabled: !!userRecord.totp_enabled,
- data_unlocked: isDataUnlocked,
};
if (isElectron) {
response.token = token;
}
+ const maxAge =
+ deviceInfo.type === "desktop" || deviceInfo.type === "mobile"
+ ? 30 * 24 * 60 * 60 * 1000
+ : 7 * 24 * 60 * 60 * 1000;
+
return res
- .cookie(
- "jwt",
- token,
- authManager.getSecureCookieOptions(req, 50 * 24 * 60 * 60 * 1000),
- )
+ .cookie("jwt", token, authManager.getSecureCookieOptions(req, maxAge))
.json(response);
} catch (err) {
authLogger.error("TOTP verification failed", err);
@@ -1655,7 +1853,7 @@ router.post("/totp/verify-login", async (req, res) => {
// Route: Setup TOTP
// POST /users/totp/setup
router.post("/totp/setup", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
try {
const user = await db.select().from(users).where(eq(users.id, userId));
@@ -1694,7 +1892,7 @@ router.post("/totp/setup", authenticateJWT, async (req, res) => {
// Route: Enable TOTP
// POST /users/totp/enable
router.post("/totp/enable", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { totp_code } = req.body;
if (!totp_code) {
@@ -1753,7 +1951,7 @@ router.post("/totp/enable", authenticateJWT, async (req, res) => {
// Route: Disable TOTP
// POST /users/totp/disable
router.post("/totp/disable", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { password, totp_code } = req.body;
if (!password && !totp_code) {
@@ -1811,7 +2009,7 @@ router.post("/totp/disable", authenticateJWT, async (req, res) => {
// 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 userId = (req as AuthenticatedRequest).userId;
const { password, totp_code } = req.body;
if (!password && !totp_code) {
@@ -1869,7 +2067,7 @@ router.post("/totp/backup-codes", authenticateJWT, async (req, res) => {
// Route: Delete user (admin only)
// DELETE /users/delete-user
router.delete("/delete-user", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { username } = req.body;
if (!isNonEmptyString(username)) {
@@ -1898,7 +2096,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
const adminCount = db.$client
.prepare("SELECT COUNT(*) as count FROM users WHERE is_admin = 1")
.get();
- if ((adminCount as any)?.count <= 1) {
+ if (((adminCount as { count?: number })?.count || 0) <= 1) {
return res
.status(403)
.json({ error: "Cannot delete the last admin user" });
@@ -1908,6 +2106,10 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
const targetUserId = targetUser[0].id;
try {
+ // Delete all user-related data to avoid foreign key constraints
+ await db
+ .delete(sshCredentialUsage)
+ .where(eq(sshCredentialUsage.userId, targetUserId));
await db
.delete(fileManagerRecent)
.where(eq(fileManagerRecent.userId, targetUserId));
@@ -1917,12 +2119,17 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
await db
.delete(fileManagerShortcuts)
.where(eq(fileManagerShortcuts.userId, targetUserId));
-
+ await db
+ .delete(recentActivity)
+ .where(eq(recentActivity.userId, targetUserId));
await db
.delete(dismissedAlerts)
.where(eq(dismissedAlerts.userId, targetUserId));
-
+ await db.delete(snippets).where(eq(snippets.userId, targetUserId));
await db.delete(sshData).where(eq(sshData.userId, targetUserId));
+ await db
+ .delete(sshCredentials)
+ .where(eq(sshCredentials.userId, targetUserId));
} catch (cleanupError) {
authLogger.error(`Cleanup failed for user ${username}:`, cleanupError);
throw cleanupError;
@@ -1955,7 +2162,7 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => {
// Route: User data unlock - used when session expires
// POST /users/unlock-data
router.post("/unlock-data", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { password } = req.body;
if (!password) {
@@ -1988,15 +2195,12 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => {
// Route: Check user data unlock status
// GET /users/data-status
router.get("/data-status", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
try {
- const isUnlocked = authManager.isUserUnlocked(userId);
res.json({
- unlocked: isUnlocked,
- message: isUnlocked
- ? "Data is unlocked"
- : "Data is locked - re-authenticate with password",
+ unlocked: true,
+ message: "Data is unlocked",
});
} catch (err) {
authLogger.error("Failed to check data status", err, {
@@ -2010,7 +2214,7 @@ router.get("/data-status", authenticateJWT, async (req, res) => {
// Route: Change user password (re-encrypt data keys)
// POST /users/change-password
router.post("/change-password", authenticateJWT, async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
@@ -2068,4 +2272,158 @@ router.post("/change-password", authenticateJWT, async (req, res) => {
}
});
+// Route: Get sessions (all for admin, own for user)
+// GET /users/sessions
+router.get("/sessions", authenticateJWT, async (req, res) => {
+ const userId = (req as AuthenticatedRequest).userId;
+
+ try {
+ const user = await db.select().from(users).where(eq(users.id, userId));
+ if (!user || user.length === 0) {
+ return res.status(404).json({ error: "User not found" });
+ }
+
+ const userRecord = user[0];
+ let sessionList;
+
+ if (userRecord.is_admin) {
+ sessionList = await authManager.getAllSessions();
+
+ const enrichedSessions = await Promise.all(
+ sessionList.map(async (session) => {
+ const sessionUser = await db
+ .select({ username: users.username })
+ .from(users)
+ .where(eq(users.id, session.userId))
+ .limit(1);
+
+ return {
+ ...session,
+ username: sessionUser[0]?.username || "Unknown",
+ };
+ }),
+ );
+
+ return res.json({ sessions: enrichedSessions });
+ } else {
+ sessionList = await authManager.getUserSessions(userId);
+ return res.json({ sessions: sessionList });
+ }
+ } catch (err) {
+ authLogger.error("Failed to get sessions", err);
+ res.status(500).json({ error: "Failed to get sessions" });
+ }
+});
+
+// Route: Revoke a specific session
+// DELETE /users/sessions/:sessionId
+router.delete("/sessions/:sessionId", authenticateJWT, async (req, res) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ const { sessionId } = req.params;
+
+ if (!sessionId) {
+ return res.status(400).json({ error: "Session ID 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" });
+ }
+
+ const userRecord = user[0];
+
+ const sessionRecords = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.id, sessionId))
+ .limit(1);
+
+ if (sessionRecords.length === 0) {
+ return res.status(404).json({ error: "Session not found" });
+ }
+
+ const session = sessionRecords[0];
+
+ if (!userRecord.is_admin && session.userId !== userId) {
+ return res
+ .status(403)
+ .json({ error: "Not authorized to revoke this session" });
+ }
+
+ const success = await authManager.revokeSession(sessionId);
+
+ if (success) {
+ authLogger.success("Session revoked", {
+ operation: "session_revoke",
+ sessionId,
+ revokedBy: userId,
+ sessionUserId: session.userId,
+ });
+ res.json({ success: true, message: "Session revoked successfully" });
+ } else {
+ res.status(500).json({ error: "Failed to revoke session" });
+ }
+ } catch (err) {
+ authLogger.error("Failed to revoke session", err);
+ res.status(500).json({ error: "Failed to revoke session" });
+ }
+});
+
+// Route: Revoke all sessions for a user
+// POST /users/sessions/revoke-all
+router.post("/sessions/revoke-all", authenticateJWT, async (req, res) => {
+ const userId = (req as AuthenticatedRequest).userId;
+ const { targetUserId, exceptCurrent } = req.body;
+
+ 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];
+
+ let revokeUserId = userId;
+ if (targetUserId && userRecord.is_admin) {
+ revokeUserId = targetUserId;
+ } else if (targetUserId && targetUserId !== userId) {
+ return res.status(403).json({
+ error: "Not authorized to revoke sessions for other users",
+ });
+ }
+
+ let currentSessionId: string | undefined;
+ if (exceptCurrent) {
+ const token =
+ req.cookies?.jwt || req.headers?.authorization?.split(" ")[1];
+ if (token) {
+ const payload = await authManager.verifyJWTToken(token);
+ currentSessionId = payload?.sessionId;
+ }
+ }
+
+ const revokedCount = await authManager.revokeAllUserSessions(
+ revokeUserId,
+ currentSessionId,
+ );
+
+ authLogger.success("User sessions revoked", {
+ operation: "user_sessions_revoke_all",
+ revokeUserId,
+ revokedBy: userId,
+ exceptCurrent,
+ revokedCount,
+ });
+
+ res.json({
+ message: `${revokedCount} session(s) revoked successfully`,
+ count: revokedCount,
+ });
+ } catch (err) {
+ authLogger.error("Failed to revoke user sessions", err);
+ res.status(500).json({ error: "Failed to revoke sessions" });
+ }
+});
+
export default router;
diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts
index 99ce1e95..bf30c2de 100644
--- a/src/backend/ssh/file-manager.ts
+++ b/src/backend/ssh/file-manager.ts
@@ -1,13 +1,15 @@
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
+import axios from "axios";
import { Client as SSHClient } from "ssh2";
import { getDb } from "../database/db/index.js";
-import { sshCredentials } from "../database/db/schema.js";
+import { sshCredentials, sshData } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { fileLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js";
+import type { AuthenticatedRequest } from "../../types/index.js";
function isExecutableFile(permissions: string, fileName: string): boolean {
const hasExecutePermission =
@@ -94,7 +96,24 @@ interface SSHSession {
timeout?: NodeJS.Timeout;
}
+interface PendingTOTPSession {
+ client: SSHClient;
+ finish: (responses: string[]) => void;
+ config: import("ssh2").ConnectConfig;
+ createdAt: number;
+ sessionId: string;
+ hostId?: number;
+ ip?: string;
+ port?: number;
+ username?: string;
+ userId?: string;
+ prompts?: Array<{ prompt: string; echo: boolean }>;
+ totpPromptIndex?: number;
+ resolvedPassword?: string;
+}
+
const sshSessions: Record = {};
+const pendingTOTPSessions: Record = {};
function cleanupSession(sessionId: string) {
const session = sshSessions[sessionId];
@@ -153,9 +172,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
keyPassword,
authType,
credentialId,
+ userProvidedPassword,
+ forceKeyboardInteractive,
} = req.body;
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!userId) {
fileLogger.error("SSH connection rejected: no authenticated user", {
@@ -235,40 +256,68 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
);
}
- const config: any = {
+ const config: Record = {
host: ip,
- port: port || 22,
+ port,
username,
- readyTimeout: 60000,
+ tryKeyboard: true,
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: [
+ "curve25519-sha256",
+ "curve25519-sha256@libssh.org",
+ "ecdh-sha2-nistp521",
+ "ecdh-sha2-nistp384",
+ "ecdh-sha2-nistp256",
+ "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
- "diffie-hellman-group1-sha1",
- "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group-exchange-sha1",
- "ecdh-sha2-nistp256",
- "ecdh-sha2-nistp384",
- "ecdh-sha2-nistp521",
+ "diffie-hellman-group1-sha1",
+ ],
+ serverHostKey: [
+ "ssh-ed25519",
+ "ecdsa-sha2-nistp521",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp256",
+ "rsa-sha2-512",
+ "rsa-sha2-256",
+ "ssh-rsa",
+ "ssh-dss",
],
cipher: [
- "aes128-ctr",
- "aes192-ctr",
- "aes256-ctr",
- "aes128-gcm@openssh.com",
+ "chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
- "aes128-cbc",
- "aes192-cbc",
+ "aes128-gcm@openssh.com",
+ "aes256-ctr",
+ "aes192-ctr",
+ "aes128-ctr",
"aes256-cbc",
+ "aes192-cbc",
+ "aes128-cbc",
"3des-cbc",
],
hmac: [
- "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
- "hmac-sha2-256",
+ "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512",
+ "hmac-sha2-256",
"hmac-sha1",
"hmac-md5",
],
@@ -277,12 +326,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
};
if (
- resolvedCredentials.authType === "password" &&
- resolvedCredentials.password &&
- resolvedCredentials.password.trim()
- ) {
- config.password = resolvedCredentials.password;
- } else if (
resolvedCredentials.authType === "key" &&
resolvedCredentials.sshKey &&
resolvedCredentials.sshKey.trim()
@@ -313,6 +356,17 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
});
return res.status(400).json({ error: "Invalid SSH key format" });
}
+ } else if (resolvedCredentials.authType === "password") {
+ if (!resolvedCredentials.password || !resolvedCredentials.password.trim()) {
+ return res
+ .status(400)
+ .json({ error: "Password required for password authentication" });
+ }
+
+ if (!forceKeyboardInteractive) {
+ config.password = resolvedCredentials.password;
+ }
+ } else if (resolvedCredentials.authType === "none") {
} else {
fileLogger.warn(
"No valid authentication method provided for file manager",
@@ -342,6 +396,48 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
};
scheduleSessionCleanup(sessionId);
res.json({ status: "success", message: "SSH connection established" });
+
+ if (hostId && userId) {
+ (async () => {
+ try {
+ const hosts = await SimpleDBOps.select(
+ getDb()
+ .select()
+ .from(sshData)
+ .where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
+ "ssh_data",
+ userId,
+ );
+
+ const hostName =
+ hosts.length > 0 && hosts[0].name
+ ? hosts[0].name
+ : `${username}@${ip}:${port}`;
+
+ const authManager = AuthManager.getInstance();
+ await axios.post(
+ "http://localhost:30006/activity/log",
+ {
+ type: "file_manager",
+ hostId,
+ hostName,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${await authManager.generateJWTToken(userId)}`,
+ },
+ },
+ );
+ } catch (error) {
+ fileLogger.warn("Failed to log file manager activity", {
+ operation: "activity_log_error",
+ userId,
+ hostId,
+ error: error instanceof Error ? error.message : "Unknown error",
+ });
+ }
+ })();
+ }
});
client.on("error", (err) => {
@@ -356,7 +452,19 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
username,
error: err.message,
});
- res.status(500).json({ status: "error", message: err.message });
+
+ if (
+ resolvedCredentials.authType === "none" &&
+ (err.message.includes("authentication") ||
+ err.message.includes("All configured authentication methods failed"))
+ ) {
+ res.json({
+ status: "auth_required",
+ reason: "no_keyboard",
+ });
+ } else {
+ res.status(500).json({ status: "error", message: err.message });
+ }
});
client.on("close", () => {
@@ -364,9 +472,324 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
cleanupSession(sessionId);
});
+ let keyboardInteractiveResponded = false;
+
+ client.on(
+ "keyboard-interactive",
+ (
+ name: string,
+ instructions: string,
+ instructionsLang: string,
+ prompts: Array<{ prompt: string; echo: boolean }>,
+ finish: (responses: string[]) => void,
+ ) => {
+ const promptTexts = prompts.map((p) => p.prompt);
+ const totpPromptIndex = prompts.findIndex((p) =>
+ /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
+ p.prompt,
+ ),
+ );
+
+ if (totpPromptIndex !== -1) {
+ if (responseSent) {
+ const responses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && resolvedCredentials.password) {
+ return resolvedCredentials.password;
+ }
+ return "";
+ });
+ finish(responses);
+ return;
+ }
+ responseSent = true;
+
+ if (pendingTOTPSessions[sessionId]) {
+ const responses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && resolvedCredentials.password) {
+ return resolvedCredentials.password;
+ }
+ return "";
+ });
+ finish(responses);
+ return;
+ }
+
+ keyboardInteractiveResponded = true;
+
+ pendingTOTPSessions[sessionId] = {
+ client,
+ finish,
+ config,
+ createdAt: Date.now(),
+ sessionId,
+ hostId,
+ ip,
+ port,
+ username,
+ userId,
+ prompts,
+ totpPromptIndex,
+ resolvedPassword: resolvedCredentials.password,
+ };
+
+ res.json({
+ requires_totp: true,
+ sessionId,
+ prompt: prompts[totpPromptIndex].prompt,
+ });
+ } else {
+ const hasStoredPassword =
+ resolvedCredentials.password &&
+ resolvedCredentials.authType !== "none";
+
+ const passwordPromptIndex = prompts.findIndex((p) =>
+ /password/i.test(p.prompt),
+ );
+
+ if (
+ resolvedCredentials.authType === "none" &&
+ passwordPromptIndex !== -1
+ ) {
+ if (responseSent) return;
+ responseSent = true;
+
+ client.end();
+
+ res.json({
+ status: "auth_required",
+ reason: "no_keyboard",
+ });
+ return;
+ }
+
+ if (!hasStoredPassword && passwordPromptIndex !== -1) {
+ if (responseSent) {
+ const responses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && resolvedCredentials.password) {
+ return resolvedCredentials.password;
+ }
+ return "";
+ });
+ finish(responses);
+ return;
+ }
+ responseSent = true;
+
+ if (pendingTOTPSessions[sessionId]) {
+ const responses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && resolvedCredentials.password) {
+ return resolvedCredentials.password;
+ }
+ return "";
+ });
+ finish(responses);
+ return;
+ }
+
+ keyboardInteractiveResponded = true;
+
+ pendingTOTPSessions[sessionId] = {
+ client,
+ finish,
+ config,
+ createdAt: Date.now(),
+ sessionId,
+ hostId,
+ ip,
+ port,
+ username,
+ userId,
+ prompts,
+ totpPromptIndex: passwordPromptIndex,
+ resolvedPassword: resolvedCredentials.password,
+ };
+
+ res.json({
+ requires_totp: true,
+ sessionId,
+ prompt: prompts[passwordPromptIndex].prompt,
+ isPassword: true,
+ });
+ return;
+ }
+
+ const responses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && resolvedCredentials.password) {
+ return resolvedCredentials.password;
+ }
+ return "";
+ });
+
+ finish(responses);
+ }
+ },
+ );
+
client.connect(config);
});
+app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
+ const { sessionId, totpCode } = req.body;
+
+ const userId = (req as AuthenticatedRequest).userId;
+
+ if (!userId) {
+ fileLogger.error("TOTP verification rejected: no authenticated user", {
+ operation: "file_totp_auth",
+ sessionId,
+ });
+ return res.status(401).json({ error: "Authentication required" });
+ }
+
+ if (!sessionId || !totpCode) {
+ return res.status(400).json({ error: "Session ID and TOTP code required" });
+ }
+
+ const session = pendingTOTPSessions[sessionId];
+
+ if (!session) {
+ fileLogger.warn("TOTP session not found or expired", {
+ operation: "file_totp_verify",
+ sessionId,
+ userId,
+ availableSessions: Object.keys(pendingTOTPSessions),
+ });
+ return res
+ .status(404)
+ .json({ error: "TOTP session expired. Please reconnect." });
+ }
+
+ if (Date.now() - session.createdAt > 180000) {
+ delete pendingTOTPSessions[sessionId];
+ try {
+ session.client.end();
+ } catch {}
+ fileLogger.warn("TOTP session timeout before code submission", {
+ operation: "file_totp_verify",
+ sessionId,
+ userId,
+ age: Date.now() - session.createdAt,
+ });
+ return res
+ .status(408)
+ .json({ error: "TOTP session timeout. Please reconnect." });
+ }
+
+ const responses = (session.prompts || []).map((p, index) => {
+ if (index === session.totpPromptIndex) {
+ return totpCode;
+ }
+ if (/password/i.test(p.prompt) && session.resolvedPassword) {
+ return session.resolvedPassword;
+ }
+ return "";
+ });
+
+ let responseSent = false;
+ let responseTimeout: NodeJS.Timeout;
+
+ session.client.once("ready", () => {
+ if (responseSent) return;
+ responseSent = true;
+ clearTimeout(responseTimeout);
+
+ delete pendingTOTPSessions[sessionId];
+
+ setTimeout(() => {
+ sshSessions[sessionId] = {
+ client: session.client,
+ isConnected: true,
+ lastActive: Date.now(),
+ };
+ scheduleSessionCleanup(sessionId);
+
+ res.json({
+ status: "success",
+ message: "TOTP verified, SSH connection established",
+ });
+
+ if (session.hostId && session.userId) {
+ (async () => {
+ try {
+ const hosts = await SimpleDBOps.select(
+ getDb()
+ .select()
+ .from(sshData)
+ .where(
+ and(
+ eq(sshData.id, session.hostId!),
+ eq(sshData.userId, session.userId!),
+ ),
+ ),
+ "ssh_data",
+ session.userId!,
+ );
+
+ const hostName =
+ hosts.length > 0 && hosts[0].name
+ ? hosts[0].name
+ : `${session.username}@${session.ip}:${session.port}`;
+
+ const authManager = AuthManager.getInstance();
+ await axios.post(
+ "http://localhost:30006/activity/log",
+ {
+ type: "file_manager",
+ hostId: session.hostId,
+ hostName,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${await authManager.generateJWTToken(session.userId!)}`,
+ },
+ },
+ );
+ } catch (error) {
+ fileLogger.warn("Failed to log file manager activity (TOTP)", {
+ operation: "activity_log_error",
+ userId: session.userId,
+ hostId: session.hostId,
+ error: error instanceof Error ? error.message : "Unknown error",
+ });
+ }
+ })();
+ }
+ }, 200);
+ });
+
+ session.client.once("error", (err) => {
+ if (responseSent) return;
+ responseSent = true;
+ clearTimeout(responseTimeout);
+
+ delete pendingTOTPSessions[sessionId];
+
+ fileLogger.error("TOTP verification failed", {
+ operation: "file_totp_verify",
+ sessionId,
+ userId,
+ error: err.message,
+ });
+
+ res.status(401).json({ status: "error", message: "Invalid TOTP code" });
+ });
+
+ responseTimeout = setTimeout(() => {
+ if (!responseSent) {
+ responseSent = true;
+ delete pendingTOTPSessions[sessionId];
+ fileLogger.warn("TOTP verification timeout", {
+ operation: "file_totp_verify",
+ sessionId,
+ userId,
+ });
+ res.status(408).json({ error: "TOTP verification timeout" });
+ }
+ }, 60000);
+
+ session.finish(responses);
+});
+
app.post("/ssh/file_manager/ssh/disconnect", (req, res) => {
const { sessionId } = req.body;
cleanupSession(sessionId);
@@ -455,13 +878,12 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => {
const parts = line.split(/\s+/);
if (parts.length >= 9) {
const permissions = parts[0];
- const linkCount = parts[1];
const owner = parts[2];
const group = parts[3];
const size = parseInt(parts[4], 10);
let dateStr = "";
- let nameStartIndex = 8;
+ const nameStartIndex = 8;
if (parts[5] && parts[6] && parts[7]) {
dateStr = `${parts[5]} ${parts[6]} ${parts[7]}`;
@@ -694,7 +1116,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
});
app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
- const { sessionId, path: filePath, content, hostId, userId } = req.body;
+ const { sessionId, path: filePath, content } = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -881,14 +1303,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => {
});
app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
- const {
- sessionId,
- path: filePath,
- content,
- fileName,
- hostId,
- userId,
- } = req.body;
+ const { sessionId, path: filePath, content, fileName } = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -1022,8 +1437,6 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
}
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"`;
@@ -1088,13 +1501,11 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
});
});
} 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) => {
+ chunks.forEach((chunk) => {
writeCommand += ` && echo '${chunk}' | base64 -d >> '${escapedPath}'`;
});
@@ -1177,14 +1588,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
});
app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
- const {
- sessionId,
- path: filePath,
- fileName,
- content = "",
- hostId,
- userId,
- } = req.body;
+ const { sessionId, path: filePath, fileName } = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -1285,7 +1689,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => {
});
app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
- const { sessionId, path: folderPath, folderName, hostId, userId } = req.body;
+ const { sessionId, path: folderPath, folderName } = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -1386,7 +1790,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => {
});
app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
- const { sessionId, path: itemPath, isDirectory, hostId, userId } = req.body;
+ const { sessionId, path: itemPath, isDirectory } = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -1488,7 +1892,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => {
});
app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
- const { sessionId, oldPath, newName, hostId, userId } = req.body;
+ const { sessionId, oldPath, newName } = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -1596,7 +2000,7 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => {
});
app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => {
- const { sessionId, oldPath, newPath, hostId, userId } = req.body;
+ const { sessionId, oldPath, newPath } = req.body;
const sshConn = sshSessions[sessionId];
if (!sessionId) {
@@ -1985,7 +2389,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
});
app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
- const { sessionId, filePath, hostId, userId } = req.body;
+ const { sessionId, filePath } = req.body;
const sshConn = sshSessions[sessionId];
if (!sshConn || !sshConn.isConnected) {
@@ -2022,7 +2426,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
checkResult += data.toString();
});
- checkStream.on("close", (code) => {
+ checkStream.on("close", () => {
if (!checkResult.includes("EXECUTABLE")) {
return res.status(400).json({ error: "File is not executable" });
}
diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts
index cc8c7e29..cc155e49 100644
--- a/src/backend/ssh/server-stats.ts
+++ b/src/backend/ssh/server-stats.ts
@@ -9,6 +9,7 @@ import { eq, and } from "drizzle-orm";
import { statsLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js";
+import type { AuthenticatedRequest } from "../../types/index.js";
interface PooledConnection {
client: Client;
@@ -60,7 +61,7 @@ class SSHConnectionPool {
return client;
}
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
const checkAvailable = () => {
const available = connections.find((conn) => !conn.inUse);
if (available) {
@@ -95,6 +96,38 @@ class SSHConnectionPool {
reject(err);
});
+ client.on(
+ "keyboard-interactive",
+ (
+ name: string,
+ instructions: string,
+ instructionsLang: string,
+ prompts: Array<{ prompt: string; echo: boolean }>,
+ finish: (responses: string[]) => void,
+ ) => {
+ const totpPrompt = prompts.find((p) =>
+ /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
+ p.prompt,
+ ),
+ );
+
+ if (totpPrompt) {
+ authFailureTracker.recordFailure(host.id, "TOTP", true);
+ client.end();
+ reject(
+ new Error(
+ "TOTP authentication required but not supported in Server Stats",
+ ),
+ );
+ } else if (host.password) {
+ const responses = prompts.map(() => host.password || "");
+ finish(responses);
+ } else {
+ finish(prompts.map(() => ""));
+ }
+ },
+ );
+
try {
client.connect(buildSshConfig(host));
} catch (err) {
@@ -151,7 +184,7 @@ class SSHConnectionPool {
}
class RequestQueue {
- private queues = new Map Promise>>();
+ private queues = new Map Promise>>();
private processing = new Set();
async queueRequest(hostId: number, request: () => Promise): Promise {
@@ -181,7 +214,7 @@ class RequestQueue {
if (request) {
try {
await request();
- } catch (error) {}
+ } catch {}
}
}
@@ -193,7 +226,7 @@ class RequestQueue {
}
interface CachedMetrics {
- data: any;
+ data: unknown;
timestamp: number;
hostId: number;
}
@@ -202,7 +235,7 @@ class MetricsCache {
private cache = new Map();
private ttl = 30000;
- get(hostId: number): any | null {
+ get(hostId: number): unknown | null {
const cached = this.cache.get(hostId);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
@@ -210,7 +243,7 @@ class MetricsCache {
return null;
}
- set(hostId: number, data: any): void {
+ set(hostId: number, data: unknown): void {
this.cache.set(hostId, {
data,
timestamp: Date.now(),
@@ -227,9 +260,104 @@ class MetricsCache {
}
}
+interface AuthFailureRecord {
+ count: number;
+ lastFailure: number;
+ reason: "TOTP" | "AUTH" | "TIMEOUT";
+ permanent: boolean;
+}
+
+class AuthFailureTracker {
+ private failures = new Map();
+ private maxRetries = 3;
+ private backoffBase = 60000;
+
+ recordFailure(
+ hostId: number,
+ reason: "TOTP" | "AUTH" | "TIMEOUT",
+ permanent = false,
+ ): void {
+ const existing = this.failures.get(hostId);
+ if (existing) {
+ existing.count++;
+ existing.lastFailure = Date.now();
+ existing.reason = reason;
+ if (permanent) existing.permanent = true;
+ } else {
+ this.failures.set(hostId, {
+ count: 1,
+ lastFailure: Date.now(),
+ reason,
+ permanent,
+ });
+ }
+ }
+
+ shouldSkip(hostId: number): boolean {
+ const record = this.failures.get(hostId);
+ if (!record) return false;
+
+ if (record.reason === "TOTP" || record.permanent) {
+ return true;
+ }
+
+ if (record.count >= this.maxRetries) {
+ return true;
+ }
+
+ const backoffTime = this.backoffBase * Math.pow(2, record.count - 1);
+ const timeSinceFailure = Date.now() - record.lastFailure;
+
+ return timeSinceFailure < backoffTime;
+ }
+
+ getSkipReason(hostId: number): string | null {
+ const record = this.failures.get(hostId);
+ if (!record) return null;
+
+ if (record.reason === "TOTP") {
+ return "TOTP authentication required (metrics unavailable)";
+ }
+
+ if (record.permanent) {
+ return "Authentication permanently failed";
+ }
+
+ if (record.count >= this.maxRetries) {
+ return `Too many authentication failures (${record.count} attempts)`;
+ }
+
+ const backoffTime = this.backoffBase * Math.pow(2, record.count - 1);
+ const timeSinceFailure = Date.now() - record.lastFailure;
+ const remainingTime = Math.ceil((backoffTime - timeSinceFailure) / 1000);
+
+ if (timeSinceFailure < backoffTime) {
+ return `Retry in ${remainingTime}s (attempt ${record.count}/${this.maxRetries})`;
+ }
+
+ return null;
+ }
+
+ reset(hostId: number): void {
+ this.failures.delete(hostId);
+ }
+
+ cleanup(): void {
+ const maxAge = 60 * 60 * 1000;
+ const now = Date.now();
+
+ for (const [hostId, record] of this.failures.entries()) {
+ if (!record.permanent && now - record.lastFailure > maxAge) {
+ this.failures.delete(hostId);
+ }
+ }
+ }
+}
+
const connectionPool = new SSHConnectionPool();
const requestQueue = new RequestQueue();
const metricsCache = new MetricsCache();
+const authFailureTracker = new AuthFailureTracker();
const authManager = AuthManager.getInstance();
type HostStatus = "online" | "offline";
@@ -253,7 +381,8 @@ interface SSHHostWithCredentials {
enableTunnel: boolean;
enableFileManager: boolean;
defaultPath: string;
- tunnelConnections: any[];
+ tunnelConnections: unknown[];
+ statsConfig?: string;
createdAt: string;
updatedAt: string;
userId: string;
@@ -264,6 +393,183 @@ type StatusEntry = {
lastChecked: string;
};
+interface StatsConfig {
+ enabledWidgets: string[];
+ statusCheckEnabled: boolean;
+ statusCheckInterval: number;
+ metricsEnabled: boolean;
+ metricsInterval: number;
+}
+
+const DEFAULT_STATS_CONFIG: StatsConfig = {
+ enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"],
+ statusCheckEnabled: true,
+ statusCheckInterval: 30,
+ metricsEnabled: true,
+ metricsInterval: 30,
+};
+
+interface HostPollingConfig {
+ host: SSHHostWithCredentials;
+ statsConfig: StatsConfig;
+ statusTimer?: NodeJS.Timeout;
+ metricsTimer?: NodeJS.Timeout;
+}
+
+class PollingManager {
+ private pollingConfigs = new Map();
+ private statusStore = new Map();
+ private metricsStore = new Map<
+ number,
+ {
+ data: Awaited>;
+ timestamp: number;
+ }
+ >();
+
+ parseStatsConfig(statsConfigStr?: string): StatsConfig {
+ if (!statsConfigStr) {
+ return DEFAULT_STATS_CONFIG;
+ }
+ try {
+ const parsed = JSON.parse(statsConfigStr);
+ return { ...DEFAULT_STATS_CONFIG, ...parsed };
+ } catch (error) {
+ statsLogger.warn(
+ `Failed to parse statsConfig: ${error instanceof Error ? error.message : "Unknown error"}`,
+ );
+ return DEFAULT_STATS_CONFIG;
+ }
+ }
+
+ async startPollingForHost(host: SSHHostWithCredentials): Promise {
+ const statsConfig = this.parseStatsConfig(host.statsConfig);
+ const existingConfig = this.pollingConfigs.get(host.id);
+
+ if (existingConfig) {
+ if (existingConfig.statusTimer) {
+ clearInterval(existingConfig.statusTimer);
+ }
+ if (existingConfig.metricsTimer) {
+ clearInterval(existingConfig.metricsTimer);
+ }
+ }
+
+ const config: HostPollingConfig = {
+ host,
+ statsConfig,
+ };
+
+ if (statsConfig.statusCheckEnabled) {
+ const intervalMs = statsConfig.statusCheckInterval * 1000;
+
+ this.pollHostStatus(host);
+
+ config.statusTimer = setInterval(() => {
+ this.pollHostStatus(host);
+ }, intervalMs);
+ } else {
+ this.statusStore.delete(host.id);
+ }
+
+ if (statsConfig.metricsEnabled) {
+ const intervalMs = statsConfig.metricsInterval * 1000;
+
+ this.pollHostMetrics(host);
+
+ config.metricsTimer = setInterval(() => {
+ this.pollHostMetrics(host);
+ }, intervalMs);
+ } else {
+ this.metricsStore.delete(host.id);
+ }
+
+ this.pollingConfigs.set(host.id, config);
+ }
+
+ private async pollHostStatus(host: SSHHostWithCredentials): Promise {
+ try {
+ const isOnline = await tcpPing(host.ip, host.port, 5000);
+ const statusEntry: StatusEntry = {
+ status: isOnline ? "online" : "offline",
+ lastChecked: new Date().toISOString(),
+ };
+ this.statusStore.set(host.id, statusEntry);
+ } catch (error) {
+ const statusEntry: StatusEntry = {
+ status: "offline",
+ lastChecked: new Date().toISOString(),
+ };
+ this.statusStore.set(host.id, statusEntry);
+ }
+ }
+
+ private async pollHostMetrics(host: SSHHostWithCredentials): Promise {
+ try {
+ const metrics = await collectMetrics(host);
+ this.metricsStore.set(host.id, {
+ data: metrics,
+ timestamp: Date.now(),
+ });
+ } catch (error) {}
+ }
+
+ stopPollingForHost(hostId: number): void {
+ const config = this.pollingConfigs.get(hostId);
+ if (config) {
+ if (config.statusTimer) {
+ clearInterval(config.statusTimer);
+ }
+ if (config.metricsTimer) {
+ clearInterval(config.metricsTimer);
+ }
+ this.pollingConfigs.delete(hostId);
+ this.statusStore.delete(hostId);
+ this.metricsStore.delete(hostId);
+ }
+ }
+
+ getStatus(hostId: number): StatusEntry | undefined {
+ return this.statusStore.get(hostId);
+ }
+
+ getAllStatuses(): Map {
+ return this.statusStore;
+ }
+
+ getMetrics(
+ hostId: number,
+ ):
+ | { data: Awaited>; timestamp: number }
+ | undefined {
+ return this.metricsStore.get(hostId);
+ }
+
+ async initializePolling(userId: string): Promise {
+ const hosts = await fetchAllHosts(userId);
+
+ for (const host of hosts) {
+ await this.startPollingForHost(host);
+ }
+ }
+
+ async refreshHostPolling(userId: string): Promise {
+ for (const hostId of this.pollingConfigs.keys()) {
+ this.stopPollingForHost(hostId);
+ }
+
+ await this.initializePolling(userId);
+ }
+
+ destroy(): void {
+ for (const hostId of this.pollingConfigs.keys()) {
+ this.stopPollingForHost(hostId);
+ }
+ }
+}
+
+const pollingManager = new PollingManager();
+
function validateHostId(
req: express.Request,
res: express.Response,
@@ -289,6 +595,10 @@ app.use(
"http://127.0.0.1:3000",
];
+ if (allowedOrigins.includes(origin)) {
+ return callback(null, true);
+ }
+
if (origin.startsWith("https://")) {
return callback(null, true);
}
@@ -297,10 +607,6 @@ app.use(
return callback(null, true);
}
- if (allowedOrigins.includes(origin)) {
- return callback(null, true);
- }
-
callback(new Error("Not allowed by CORS"));
},
credentials: true,
@@ -318,8 +624,6 @@ app.use(express.json({ limit: "1mb" }));
app.use(authManager.createAuthMiddleware());
-const hostStatuses: Map = new Map();
-
async function fetchAllHosts(
userId: string,
): Promise {
@@ -357,11 +661,6 @@ async function fetchHostById(
): Promise {
try {
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
- statsLogger.debug("User data locked - cannot fetch host", {
- operation: "fetchHostById_data_locked",
- userId,
- hostId: id,
- });
return undefined;
}
@@ -387,11 +686,11 @@ async function fetchHostById(
}
async function resolveHostCredentials(
- host: any,
+ host: Record,
userId: string,
): Promise {
try {
- const baseHost: any = {
+ const baseHost: Record = {
id: host.id,
name: host.name,
ip: host.ip,
@@ -411,8 +710,9 @@ async function resolveHostCredentials(
enableFileManager: !!host.enableFileManager,
defaultPath: host.defaultPath || "/",
tunnelConnections: host.tunnelConnections
- ? JSON.parse(host.tunnelConnections)
+ ? JSON.parse(host.tunnelConnections as string)
: [],
+ statsConfig: host.statsConfig || undefined,
createdAt: host.createdAt,
updatedAt: host.updatedAt,
userId: host.userId,
@@ -426,7 +726,7 @@ async function resolveHostCredentials(
.from(sshCredentials)
.where(
and(
- eq(sshCredentials.id, host.credentialId),
+ eq(sshCredentials.id, host.credentialId as number),
eq(sshCredentials.userId, userId),
),
),
@@ -466,7 +766,7 @@ async function resolveHostCredentials(
addLegacyCredentials(baseHost, host);
}
- return baseHost;
+ return baseHost as unknown as SSHHostWithCredentials;
} catch (error) {
statsLogger.error(
`Failed to resolve host credentials for host ${host.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
@@ -475,7 +775,10 @@ async function resolveHostCredentials(
}
}
-function addLegacyCredentials(baseHost: any, host: any): void {
+function addLegacyCredentials(
+ baseHost: Record,
+ host: Record,
+): void {
baseHost.password = host.password || null;
baseHost.key = host.key || null;
baseHost.keyPassword = host.key_password || host.keyPassword || null;
@@ -485,36 +788,66 @@ function addLegacyCredentials(baseHost: any, host: any): void {
function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
const base: ConnectConfig = {
host: host.ip,
- port: host.port || 22,
- username: host.username || "root",
- readyTimeout: 10_000,
+ port: host.port,
+ username: host.username,
+ tryKeyboard: true,
+ 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: [
+ "curve25519-sha256",
+ "curve25519-sha256@libssh.org",
+ "ecdh-sha2-nistp521",
+ "ecdh-sha2-nistp384",
+ "ecdh-sha2-nistp256",
+ "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
- "diffie-hellman-group1-sha1",
- "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group-exchange-sha1",
- "ecdh-sha2-nistp256",
- "ecdh-sha2-nistp384",
- "ecdh-sha2-nistp521",
+ "diffie-hellman-group1-sha1",
+ ],
+ serverHostKey: [
+ "ssh-ed25519",
+ "ecdsa-sha2-nistp521",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp256",
+ "rsa-sha2-512",
+ "rsa-sha2-256",
+ "ssh-rsa",
+ "ssh-dss",
],
cipher: [
- "aes128-ctr",
- "aes192-ctr",
- "aes256-ctr",
- "aes128-gcm@openssh.com",
+ "chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
- "aes128-cbc",
- "aes192-cbc",
+ "aes128-gcm@openssh.com",
+ "aes256-ctr",
+ "aes192-ctr",
+ "aes128-ctr",
"aes256-cbc",
+ "aes192-cbc",
+ "aes128-cbc",
"3des-cbc",
],
hmac: [
- "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
- "hmac-sha2-256",
+ "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512",
+ "hmac-sha2-256",
"hmac-sha1",
"hmac-md5",
],
@@ -526,7 +859,7 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
if (!host.password) {
throw new Error(`No password available for host ${host.ip}`);
}
- (base as any).password = host.password;
+ base.password = host.password;
} else if (host.authType === "key") {
if (!host.key) {
throw new Error(`No SSH key available for host ${host.ip}`);
@@ -542,10 +875,13 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
- (base as any).privateKey = Buffer.from(cleanKey, "utf8");
+ (base as Record).privateKey = Buffer.from(
+ cleanKey,
+ "utf8",
+ );
if (host.keyPassword) {
- (base as any).passphrase = host.keyPassword;
+ (base as Record).passphrase = host.keyPassword;
}
} catch (keyError) {
statsLogger.error(
@@ -643,175 +979,375 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
percent: number | null;
usedHuman: string | null;
totalHuman: string | null;
+ availableHuman: string | null;
+ };
+ network: {
+ interfaces: Array<{
+ name: string;
+ ip: string;
+ state: string;
+ rxBytes: string | null;
+ txBytes: string | null;
+ }>;
+ };
+ uptime: {
+ seconds: number | null;
+ formatted: string | null;
+ };
+ processes: {
+ total: number | null;
+ running: number | null;
+ top: Array<{
+ pid: string;
+ user: string;
+ cpu: string;
+ mem: string;
+ command: string;
+ }>;
+ };
+ system: {
+ hostname: string | null;
+ kernel: string | null;
+ os: string | null;
};
}> {
+ if (authFailureTracker.shouldSkip(host.id)) {
+ const reason = authFailureTracker.getSkipReason(host.id);
+ throw new Error(reason || "Authentication failed");
+ }
+
const cached = metricsCache.get(host.id);
if (cached) {
- return cached;
+ return cached as ReturnType extends Promise
+ ? T
+ : never;
}
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 {
+ return await withSshConnection(host, async (client) => {
+ let cpuPercent: number | null = null;
+ let cores: number | null = null;
+ let loadTriplet: [number, number, number] | null = null;
- 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",
- ),
- ]);
+ 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",
+ ),
+ ]);
- await new Promise((r) => setTimeout(r, 500));
- const stat2 = await execCommand(client, "cat /proc/stat");
+ await new Promise((r) => setTimeout(r, 500));
+ const stat2 = await execCommand(client, "cat /proc/stat");
- 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));
- }
-
- 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,
- ];
- }
-
- 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;
- }
-
- 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;
- let availableHuman: 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;
- availableHuman = humanParts[3] || 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),
- );
+ 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));
}
+
+ 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,
+ ];
+ }
+
+ const coresNum = Number((coresOut.stdout || "").trim());
+ cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null;
+ } catch (e) {
+ cpuPercent = null;
+ cores = null;
+ loadTriplet = null;
+ }
+
+ 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;
+ }
+
+ let diskPercent: number | null = null;
+ let usedHuman: string | null = null;
+ let totalHuman: string | null = null;
+ let availableHuman: 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;
+ availableHuman = humanParts[3] || 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) {
+ diskPercent = null;
+ usedHuman = null;
+ totalHuman = null;
+ availableHuman = null;
+ }
+
+ const interfaces: Array<{
+ name: string;
+ ip: string;
+ state: string;
+ rxBytes: string | null;
+ txBytes: string | null;
+ }> = [];
+ try {
+ const ifconfigOut = await execCommand(
+ client,
+ "ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'",
+ );
+ const netStatOut = await execCommand(
+ client,
+ "ip -o link show | awk '{gsub(/:/, \"\", $2); print $2,$9}'",
+ );
+
+ const addrs = ifconfigOut.stdout
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
+ const states = netStatOut.stdout
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
+
+ const ifMap = new Map();
+ for (const line of addrs) {
+ const parts = line.split(/\s+/);
+ if (parts.length >= 2) {
+ const name = parts[0];
+ const ip = parts[1].split("/")[0];
+ if (!ifMap.has(name)) ifMap.set(name, { ip, state: "UNKNOWN" });
+ }
+ }
+ for (const line of states) {
+ const parts = line.split(/\s+/);
+ if (parts.length >= 2) {
+ const name = parts[0];
+ const state = parts[1];
+ const existing = ifMap.get(name);
+ if (existing) {
+ existing.state = state;
+ }
+ }
+ }
+
+ for (const [name, data] of ifMap.entries()) {
+ interfaces.push({
+ name,
+ ip: data.ip,
+ state: data.state,
+ rxBytes: null,
+ txBytes: null,
+ });
+ }
+ } catch (e) {}
+
+ let uptimeSeconds: number | null = null;
+ let uptimeFormatted: string | null = null;
+ try {
+ const uptimeOut = await execCommand(client, "cat /proc/uptime");
+ const uptimeParts = uptimeOut.stdout.trim().split(/\s+/);
+ if (uptimeParts.length >= 1) {
+ uptimeSeconds = Number(uptimeParts[0]);
+ if (Number.isFinite(uptimeSeconds)) {
+ const days = Math.floor(uptimeSeconds / 86400);
+ const hours = Math.floor((uptimeSeconds % 86400) / 3600);
+ const minutes = Math.floor((uptimeSeconds % 3600) / 60);
+ uptimeFormatted = `${days}d ${hours}h ${minutes}m`;
+ }
+ }
+ } catch (e) {}
+
+ let totalProcesses: number | null = null;
+ let runningProcesses: number | null = null;
+ const topProcesses: Array<{
+ pid: string;
+ user: string;
+ cpu: string;
+ mem: string;
+ command: string;
+ }> = [];
+ try {
+ const psOut = await execCommand(
+ client,
+ "ps aux --sort=-%cpu | head -n 11",
+ );
+ const psLines = psOut.stdout
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
+ if (psLines.length > 1) {
+ for (let i = 1; i < Math.min(psLines.length, 11); i++) {
+ const parts = psLines[i].split(/\s+/);
+ if (parts.length >= 11) {
+ topProcesses.push({
+ pid: parts[1],
+ user: parts[0],
+ cpu: parts[2],
+ mem: parts[3],
+ command: parts.slice(10).join(" ").substring(0, 50),
+ });
+ }
+ }
+ }
+
+ const procCount = await execCommand(client, "ps aux | wc -l");
+ const runningCount = await execCommand(
+ client,
+ "ps aux | grep -c ' R '",
+ );
+ totalProcesses = Number(procCount.stdout.trim()) - 1;
+ runningProcesses = Number(runningCount.stdout.trim());
+ } catch (e) {}
+
+ let hostname: string | null = null;
+ let kernel: string | null = null;
+ let os: string | null = null;
+ try {
+ const hostnameOut = await execCommand(client, "hostname");
+ const kernelOut = await execCommand(client, "uname -r");
+ const osOut = await execCommand(
+ client,
+ "cat /etc/os-release | grep '^PRETTY_NAME=' | cut -d'\"' -f2",
+ );
+
+ hostname = hostnameOut.stdout.trim() || null;
+ kernel = kernelOut.stdout.trim() || null;
+ os = osOut.stdout.trim() || null;
+ } catch (e) {}
+
+ 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,
+ availableHuman,
+ },
+ network: {
+ interfaces,
+ },
+ uptime: {
+ seconds: uptimeSeconds,
+ formatted: uptimeFormatted,
+ },
+ processes: {
+ total: totalProcesses,
+ running: runningProcesses,
+ top: topProcesses,
+ },
+ system: {
+ hostname,
+ kernel,
+ os,
+ },
+ };
+
+ metricsCache.set(host.id, result);
+ return result;
+ });
+ } catch (error) {
+ if (error instanceof Error) {
+ if (error.message.includes("TOTP authentication required")) {
+ throw error;
+ } else if (
+ error.message.includes("No password available") ||
+ error.message.includes("Unsupported authentication type") ||
+ error.message.includes("No SSH key available")
+ ) {
+ authFailureTracker.recordFailure(host.id, "AUTH", true);
+ } else if (
+ error.message.includes("authentication") ||
+ error.message.includes("Permission denied") ||
+ error.message.includes("All configured authentication methods failed")
+ ) {
+ authFailureTracker.recordFailure(host.id, "AUTH");
+ } else if (
+ error.message.includes("timeout") ||
+ error.message.includes("ETIMEDOUT")
+ ) {
+ authFailureTracker.recordFailure(host.id, "TIMEOUT");
}
- } catch (e) {
- statsLogger.warn(
- `Failed to collect disk metrics for host ${host.id}`,
- e,
- );
- diskPercent = null;
- usedHuman = null;
- totalHuman = null;
- availableHuman = 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,
- availableHuman,
- },
- };
-
- metricsCache.set(host.id, result);
- return result;
- });
+ throw error;
+ }
});
}
@@ -842,51 +1378,8 @@ function tcpPing(
});
}
-async function pollStatusesOnce(userId?: string): Promise {
- if (!userId) {
- statsLogger.warn("Skipping status poll - no authenticated user", {
- operation: "status_poll",
- });
- return;
- }
-
- const hosts = await fetchAllHosts(userId);
- if (hosts.length === 0) {
- statsLogger.warn("No hosts retrieved for status polling", {
- operation: "status_poll",
- userId,
- });
- 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 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) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({
@@ -895,11 +1388,13 @@ app.get("/status", async (req, res) => {
});
}
- if (hostStatuses.size === 0) {
- await pollStatusesOnce(userId);
+ const statuses = pollingManager.getAllStatuses();
+ if (statuses.size === 0) {
+ await pollingManager.initializePolling(userId);
}
+
const result: Record = {};
- for (const [id, entry] of hostStatuses.entries()) {
+ for (const [id, entry] of pollingManager.getAllStatuses().entries()) {
result[id] = entry;
}
res.json(result);
@@ -907,7 +1402,7 @@ app.get("/status", async (req, res) => {
app.get("/status/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({
@@ -916,29 +1411,21 @@ app.get("/status/:id", validateHostId, async (req, res) => {
});
}
- try {
- const host = await fetchHostById(id, userId);
- 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) {
- statsLogger.error("Failed to check host status", err);
- res.status(500).json({ error: "Failed to check host status" });
+ const statuses = pollingManager.getAllStatuses();
+ if (statuses.size === 0) {
+ await pollingManager.initializePolling(userId);
}
+
+ const statusEntry = pollingManager.getStatus(id);
+ if (!statusEntry) {
+ return res.status(404).json({ error: "Status not available" });
+ }
+
+ res.json(statusEntry);
});
app.post("/refresh", async (req, res) => {
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({
@@ -947,13 +1434,13 @@ app.post("/refresh", async (req, res) => {
});
}
- await pollStatusesOnce(userId);
- res.json({ message: "Refreshed" });
+ await pollingManager.refreshHostPolling(userId);
+ res.json({ message: "Polling refreshed" });
});
app.get("/metrics/:id", validateHostId, async (req, res) => {
const id = Number(req.params.id);
- const userId = (req as any).userId;
+ const userId = (req as AuthenticatedRequest).userId;
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
return res.status(401).json({
@@ -962,54 +1449,40 @@ app.get("/metrics/:id", validateHostId, async (req, res) => {
});
}
- try {
- const host = await fetchHostById(id, userId);
- 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",
+ const metricsData = pollingManager.getMetrics(id);
+ if (!metricsData) {
+ return res.status(404).json({
+ error: "Metrics not available",
cpu: { percent: null, cores: null, load: null },
memory: { percent: null, usedGiB: null, totalGiB: null },
- disk: { percent: null, usedHuman: null, totalHuman: null },
+ disk: {
+ percent: null,
+ usedHuman: null,
+ totalHuman: null,
+ availableHuman: null,
+ },
+ network: { interfaces: [] },
+ uptime: { seconds: null, formatted: null },
+ processes: { total: null, running: null, top: [] },
+ system: { hostname: null, kernel: null, os: null },
lastChecked: new Date().toISOString(),
});
}
+
+ res.json({
+ ...metricsData.data,
+ lastChecked: new Date(metricsData.timestamp).toISOString(),
+ });
});
process.on("SIGINT", () => {
+ pollingManager.destroy();
connectionPool.destroy();
process.exit(0);
});
process.on("SIGTERM", () => {
+ pollingManager.destroy();
connectionPool.destroy();
process.exit(0);
});
@@ -1023,4 +1496,11 @@ app.listen(PORT, async () => {
operation: "auth_init_error",
});
}
+
+ setInterval(
+ () => {
+ authFailureTracker.cleanup();
+ },
+ 10 * 60 * 1000,
+ );
});
diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts
index 94650bd6..a040ee91 100644
--- a/src/backend/ssh/terminal.ts
+++ b/src/backend/ssh/terminal.ts
@@ -1,14 +1,57 @@
import { WebSocketServer, WebSocket, type RawData } from "ws";
-import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2";
+import {
+ Client,
+ type ClientChannel,
+ type PseudoTtyOptions,
+ type ConnectConfig,
+} from "ssh2";
import { parse as parseUrl } from "url";
+import axios from "axios";
import { getDb } from "../database/db/index.js";
-import { sshCredentials } from "../database/db/schema.js";
+import { sshCredentials, sshData } from "../database/db/schema.js";
import { eq, and } from "drizzle-orm";
import { sshLogger } from "../utils/logger.js";
import { SimpleDBOps } from "../utils/simple-db-ops.js";
import { AuthManager } from "../utils/auth-manager.js";
import { UserCrypto } from "../utils/user-crypto.js";
+interface ConnectToHostData {
+ 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;
+ forceKeyboardInteractive?: boolean;
+ };
+ initialPath?: string;
+ executeCommand?: string;
+}
+
+interface ResizeData {
+ cols: number;
+ rows: number;
+}
+
+interface TOTPResponseData {
+ code?: string;
+}
+
+interface WebSocketMessage {
+ type: string;
+ data?: ConnectToHostData | ResizeData | TOTPResponseData | string | unknown;
+ code?: string;
+ [key: string]: unknown;
+}
+
const authManager = AuthManager.getInstance();
const userCrypto = UserCrypto.getInstance();
@@ -22,47 +65,21 @@ const wss = new WebSocketServer({
const token = url.query.token as string;
if (!token) {
- sshLogger.warn("WebSocket connection rejected: missing token", {
- operation: "websocket_auth_reject",
- reason: "missing_token",
- ip: info.req.socket.remoteAddress,
- });
return false;
}
const payload = await authManager.verifyJWTToken(token);
if (!payload) {
- sshLogger.warn("WebSocket connection rejected: invalid token", {
- operation: "websocket_auth_reject",
- reason: "invalid_token",
- ip: info.req.socket.remoteAddress,
- });
return false;
}
if (payload.pendingTOTP) {
- sshLogger.warn(
- "WebSocket connection rejected: TOTP verification pending",
- {
- operation: "websocket_auth_reject",
- reason: "totp_pending",
- userId: payload.userId,
- ip: info.req.socket.remoteAddress,
- },
- );
return false;
}
const existingConnections = userConnections.get(payload.userId);
if (existingConnections && existingConnections.size >= 3) {
- sshLogger.warn("WebSocket connection rejected: too many connections", {
- operation: "websocket_auth_reject",
- reason: "connection_limit",
- userId: payload.userId,
- currentConnections: existingConnections.size,
- ip: info.req.socket.remoteAddress,
- });
return false;
}
@@ -79,41 +96,23 @@ const wss = new WebSocketServer({
wss.on("connection", async (ws: WebSocket, req) => {
let userId: string | undefined;
- let userPayload: any;
try {
const url = parseUrl(req.url!, true);
const token = url.query.token as string;
if (!token) {
- sshLogger.warn(
- "WebSocket connection rejected: missing token in connection",
- {
- operation: "websocket_connection_reject",
- reason: "missing_token",
- ip: req.socket.remoteAddress,
- },
- );
ws.close(1008, "Authentication required");
return;
}
const payload = await authManager.verifyJWTToken(token);
if (!payload) {
- sshLogger.warn(
- "WebSocket connection rejected: invalid token in connection",
- {
- operation: "websocket_connection_reject",
- reason: "invalid_token",
- ip: req.socket.remoteAddress,
- },
- );
ws.close(1008, "Authentication required");
return;
}
userId = payload.userId;
- userPayload = payload;
} catch (error) {
sshLogger.error(
"WebSocket JWT verification failed during connection",
@@ -129,11 +128,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
const dataKey = userCrypto.getUserDataKey(userId);
if (!dataKey) {
- sshLogger.warn("WebSocket connection rejected: data locked", {
- operation: "websocket_data_locked",
- userId,
- ip: req.socket.remoteAddress,
- });
ws.send(
JSON.stringify({
type: "error",
@@ -154,6 +148,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null;
let pingInterval: NodeJS.Timeout | null = null;
+ let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
+ let totpPromptSent = false;
+ let isKeyboardInteractive = false;
+ let keyboardInteractiveResponded = false;
ws.on("close", () => {
const userWs = userConnections.get(userId);
@@ -170,11 +168,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws.on("message", (msg: RawData) => {
const currentDataKey = userCrypto.getUserDataKey(userId);
if (!currentDataKey) {
- sshLogger.warn("WebSocket message rejected: data access expired", {
- operation: "websocket_message_rejected",
- userId,
- reason: "data_access_expired",
- });
ws.send(
JSON.stringify({
type: "error",
@@ -186,9 +179,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
return;
}
- let parsed: any;
+ let parsed: WebSocketMessage;
try {
- parsed = JSON.parse(msg.toString());
+ parsed = JSON.parse(msg.toString()) as WebSocketMessage;
} catch (e) {
sshLogger.error("Invalid JSON received", e, {
operation: "websocket_message_invalid_json",
@@ -202,16 +195,17 @@ wss.on("connection", async (ws: WebSocket, req) => {
const { type, data } = parsed;
switch (type) {
- case "connectToHost":
- if (data.hostConfig) {
- data.hostConfig.userId = userId;
+ case "connectToHost": {
+ const connectData = data as ConnectToHostData;
+ if (connectData.hostConfig) {
+ connectData.hostConfig.userId = userId;
}
- handleConnectToHost(data).catch((error) => {
+ handleConnectToHost(connectData).catch((error) => {
sshLogger.error("Failed to connect to host", error, {
operation: "ssh_connect",
userId,
- hostId: data.hostConfig?.id,
- ip: data.hostConfig?.ip,
+ hostId: connectData.hostConfig?.id,
+ ip: connectData.hostConfig?.ip,
});
ws.send(
JSON.stringify({
@@ -223,40 +217,144 @@ wss.on("connection", async (ws: WebSocket, req) => {
);
});
break;
+ }
- case "resize":
- handleResize(data);
+ case "resize": {
+ const resizeData = data as ResizeData;
+ handleResize(resizeData);
break;
+ }
case "disconnect":
cleanupSSH();
break;
- case "input":
+ case "input": {
+ const inputData = data as string;
if (sshStream) {
- if (data === "\t") {
- sshStream.write(data);
- } else if (data.startsWith("\x1b")) {
- sshStream.write(data);
+ if (inputData === "\t") {
+ sshStream.write(inputData);
+ } else if (
+ typeof inputData === "string" &&
+ inputData.startsWith("\x1b")
+ ) {
+ sshStream.write(inputData);
} else {
try {
- sshStream.write(Buffer.from(data, "utf8"));
+ sshStream.write(Buffer.from(inputData, "utf8"));
} catch (error) {
sshLogger.error("Error writing input to SSH stream", error, {
operation: "ssh_input_encoding",
userId,
- dataLength: data.length,
+ dataLength: inputData.length,
});
- sshStream.write(Buffer.from(data, "latin1"));
+ sshStream.write(Buffer.from(inputData, "latin1"));
}
}
}
break;
+ }
case "ping":
ws.send(JSON.stringify({ type: "pong" }));
break;
+ case "totp_response": {
+ const totpData = data as TOTPResponseData;
+ if (keyboardInteractiveFinish && totpData?.code) {
+ const totpCode = totpData.code;
+ keyboardInteractiveFinish([totpCode]);
+ keyboardInteractiveFinish = null;
+ } else {
+ sshLogger.warn("TOTP response received but no callback available", {
+ operation: "totp_response_error",
+ userId,
+ hasCallback: !!keyboardInteractiveFinish,
+ hasCode: !!totpData?.code,
+ });
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message: "TOTP authentication state lost. Please reconnect.",
+ }),
+ );
+ }
+ break;
+ }
+
+ case "password_response": {
+ const passwordData = data as TOTPResponseData;
+ if (keyboardInteractiveFinish && passwordData?.code) {
+ const password = passwordData.code;
+ keyboardInteractiveFinish([password]);
+ keyboardInteractiveFinish = null;
+ } else {
+ sshLogger.warn(
+ "Password response received but no callback available",
+ {
+ operation: "password_response_error",
+ userId,
+ hasCallback: !!keyboardInteractiveFinish,
+ hasCode: !!passwordData?.code,
+ },
+ );
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message: "Password authentication state lost. Please reconnect.",
+ }),
+ );
+ }
+ break;
+ }
+
+ case "reconnect_with_credentials": {
+ const credentialsData = data as {
+ cols: number;
+ rows: number;
+ hostConfig: ConnectToHostData["hostConfig"];
+ password?: string;
+ sshKey?: string;
+ keyPassword?: string;
+ };
+
+ if (credentialsData.password) {
+ credentialsData.hostConfig.password = credentialsData.password;
+ credentialsData.hostConfig.authType = "password";
+ (credentialsData.hostConfig as any).userProvidedPassword = true;
+ } else if (credentialsData.sshKey) {
+ credentialsData.hostConfig.key = credentialsData.sshKey;
+ credentialsData.hostConfig.keyPassword = credentialsData.keyPassword;
+ credentialsData.hostConfig.authType = "key";
+ }
+
+ cleanupSSH();
+
+ const reconnectData: ConnectToHostData = {
+ cols: credentialsData.cols,
+ rows: credentialsData.rows,
+ hostConfig: credentialsData.hostConfig,
+ };
+
+ handleConnectToHost(reconnectData).catch((error) => {
+ sshLogger.error("Failed to reconnect with credentials", error, {
+ operation: "ssh_reconnect_with_credentials",
+ userId,
+ hostId: credentialsData.hostConfig?.id,
+ ip: credentialsData.hostConfig?.ip,
+ });
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message:
+ "Failed to connect with provided credentials: " +
+ (error instanceof Error ? error.message : "Unknown error"),
+ }),
+ );
+ });
+ break;
+ }
+
default:
sshLogger.warn("Unknown message type received", {
operation: "websocket_message_unknown_type",
@@ -266,26 +364,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
});
- 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;
- };
- initialPath?: string;
- executeCommand?: string;
- }) {
- const { cols, rows, hostConfig, initialPath, executeCommand } = data;
+ async function handleConnectToHost(data: ConnectToHostData) {
+ const { hostConfig, initialPath, executeCommand } = data;
const {
id,
ip,
@@ -356,6 +436,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}, 60000);
let resolvedCredentials = { password, key, keyPassword, keyType, authType };
+ let authMethodNotAvailable = false;
if (credentialId && id && hostConfig.userId) {
try {
const credentials = await SimpleDBOps.select(
@@ -375,12 +456,19 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (credentials.length > 0) {
const credential = credentials[0];
resolvedCredentials = {
- password: credential.password,
- key:
- credential.private_key || credential.privateKey || credential.key,
- keyPassword: credential.key_password || credential.keyPassword,
- keyType: credential.key_type || credential.keyType,
- authType: credential.auth_type || credential.authType,
+ password: credential.password as string | undefined,
+ key: (credential.private_key ||
+ credential.privateKey ||
+ credential.key) as string | undefined,
+ keyPassword: (credential.key_password || credential.keyPassword) as
+ | string
+ | undefined,
+ keyType: (credential.key_type || credential.keyType) as
+ | string
+ | undefined,
+ authType: (credential.auth_type || credential.authType) as
+ | string
+ | undefined,
};
} else {
sshLogger.warn(`No credentials found for host ${id}`, {
@@ -410,7 +498,28 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn.on("ready", () => {
clearTimeout(connectionTimeout);
- sshConn!.shell(
+ if (!sshConn) {
+ sshLogger.warn(
+ "SSH connection was cleaned up before shell could be created",
+ {
+ operation: "ssh_shell",
+ hostId: id,
+ ip,
+ port,
+ username,
+ },
+ );
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message:
+ "SSH connection was closed before terminal could be created",
+ }),
+ );
+ return;
+ }
+
+ sshConn.shell(
{
rows: data.rows,
cols: data.cols,
@@ -497,12 +606,76 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws.send(
JSON.stringify({ type: "connected", message: "SSH connected" }),
);
+
+ if (id && hostConfig.userId) {
+ (async () => {
+ try {
+ const hosts = await SimpleDBOps.select(
+ getDb()
+ .select()
+ .from(sshData)
+ .where(
+ and(
+ eq(sshData.id, id),
+ eq(sshData.userId, hostConfig.userId!),
+ ),
+ ),
+ "ssh_data",
+ hostConfig.userId!,
+ );
+
+ const hostName =
+ hosts.length > 0 && hosts[0].name
+ ? hosts[0].name
+ : `${username}@${ip}:${port}`;
+
+ await axios.post(
+ "http://localhost:30006/activity/log",
+ {
+ type: "terminal",
+ hostId: id,
+ hostName,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${await authManager.generateJWTToken(hostConfig.userId!)}`,
+ },
+ },
+ );
+ } catch (error) {
+ sshLogger.warn("Failed to log terminal activity", {
+ operation: "activity_log_error",
+ userId: hostConfig.userId,
+ hostId: id,
+ error:
+ error instanceof Error ? error.message : "Unknown error",
+ });
+ }
+ })();
+ }
},
);
});
sshConn.on("error", (err: Error) => {
clearTimeout(connectionTimeout);
+
+ if (
+ (authMethodNotAvailable && resolvedCredentials.authType === "none") ||
+ (resolvedCredentials.authType === "none" &&
+ err.message.includes("All configured authentication methods failed"))
+ ) {
+ ws.send(
+ JSON.stringify({
+ type: "auth_method_not_available",
+ message:
+ "The server does not support keyboard-interactive authentication. Please provide credentials.",
+ }),
+ );
+ cleanupSSH(connectionTimeout);
+ return;
+ }
+
sshLogger.error("SSH connection error", err, {
operation: "ssh_connect",
hostId: id,
@@ -557,16 +730,115 @@ wss.on("connection", async (ws: WebSocket, req) => {
cleanupSSH(connectionTimeout);
});
+ sshConn.on(
+ "keyboard-interactive",
+ (
+ name: string,
+ instructions: string,
+ instructionsLang: string,
+ prompts: Array<{ prompt: string; echo: boolean }>,
+ finish: (responses: string[]) => void,
+ ) => {
+ isKeyboardInteractive = true;
+ const promptTexts = prompts.map((p) => p.prompt);
+ const totpPromptIndex = prompts.findIndex((p) =>
+ /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
+ p.prompt,
+ ),
+ );
+
+ if (totpPromptIndex !== -1) {
+ if (totpPromptSent) {
+ sshLogger.warn("TOTP prompt asked again - ignoring duplicate", {
+ operation: "ssh_keyboard_interactive_totp_duplicate",
+ hostId: id,
+ prompts: promptTexts,
+ });
+ return;
+ }
+ totpPromptSent = true;
+ keyboardInteractiveResponded = true;
+
+ keyboardInteractiveFinish = (totpResponses: string[]) => {
+ const totpCode = (totpResponses[0] || "").trim();
+
+ const responses = prompts.map((p, index) => {
+ if (index === totpPromptIndex) {
+ return totpCode;
+ }
+ if (/password/i.test(p.prompt) && resolvedCredentials.password) {
+ return resolvedCredentials.password;
+ }
+ return "";
+ });
+
+ finish(responses);
+ };
+ ws.send(
+ JSON.stringify({
+ type: "totp_required",
+ prompt: prompts[totpPromptIndex].prompt,
+ }),
+ );
+ } else {
+ const hasStoredPassword =
+ resolvedCredentials.password &&
+ resolvedCredentials.authType !== "none";
+
+ const passwordPromptIndex = prompts.findIndex((p) =>
+ /password/i.test(p.prompt),
+ );
+
+ if (!hasStoredPassword && passwordPromptIndex !== -1) {
+ if (keyboardInteractiveResponded) {
+ return;
+ }
+ keyboardInteractiveResponded = true;
+
+ keyboardInteractiveFinish = (userResponses: string[]) => {
+ const userInput = (userResponses[0] || "").trim();
+
+ const responses = prompts.map((p, index) => {
+ if (index === passwordPromptIndex) {
+ return userInput;
+ }
+ return "";
+ });
+
+ finish(responses);
+ };
+
+ ws.send(
+ JSON.stringify({
+ type: "password_required",
+ prompt: prompts[passwordPromptIndex].prompt,
+ }),
+ );
+ return;
+ }
+
+ const responses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && resolvedCredentials.password) {
+ return resolvedCredentials.password;
+ }
+ return "";
+ });
+
+ finish(responses);
+ }
+ },
+ );
+
const connectConfig: any = {
host: ip,
port,
username,
+ tryKeyboard: true,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
-
env: {
TERM: "xterm-256color",
LANG: "en_US.UTF-8",
@@ -579,45 +851,72 @@ wss.on("connection", async (ws: WebSocket, req) => {
LC_COLLATE: "en_US.UTF-8",
COLORTERM: "truecolor",
},
-
algorithms: {
kex: [
+ "curve25519-sha256",
+ "curve25519-sha256@libssh.org",
+ "ecdh-sha2-nistp521",
+ "ecdh-sha2-nistp384",
+ "ecdh-sha2-nistp256",
+ "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
- "diffie-hellman-group1-sha1",
- "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group-exchange-sha1",
- "ecdh-sha2-nistp256",
- "ecdh-sha2-nistp384",
- "ecdh-sha2-nistp521",
+ "diffie-hellman-group1-sha1",
+ ],
+ serverHostKey: [
+ "ssh-ed25519",
+ "ecdsa-sha2-nistp521",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp256",
+ "rsa-sha2-512",
+ "rsa-sha2-256",
+ "ssh-rsa",
+ "ssh-dss",
],
cipher: [
- "aes128-ctr",
- "aes192-ctr",
- "aes256-ctr",
- "aes128-gcm@openssh.com",
+ "chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
- "aes128-cbc",
- "aes192-cbc",
+ "aes128-gcm@openssh.com",
+ "aes256-ctr",
+ "aes192-ctr",
+ "aes128-ctr",
"aes256-cbc",
+ "aes192-cbc",
+ "aes128-cbc",
"3des-cbc",
],
hmac: [
- "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
- "hmac-sha2-256",
+ "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512",
+ "hmac-sha2-256",
"hmac-sha1",
"hmac-md5",
],
compress: ["none", "zlib@openssh.com", "zlib"],
},
};
- if (
- resolvedCredentials.authType === "password" &&
- resolvedCredentials.password
- ) {
- connectConfig.password = resolvedCredentials.password;
+
+ if (resolvedCredentials.authType === "none") {
+ } else if (resolvedCredentials.authType === "password") {
+ if (!resolvedCredentials.password) {
+ sshLogger.error(
+ "Password authentication requested but no password provided",
+ );
+ ws.send(
+ JSON.stringify({
+ type: "error",
+ message:
+ "Password authentication requested but no password provided",
+ }),
+ );
+ return;
+ }
+
+ if (!hostConfig.forceKeyboardInteractive) {
+ connectConfig.password = resolvedCredentials.password;
+ }
} else if (
resolvedCredentials.authType === "key" &&
resolvedCredentials.key
@@ -640,13 +939,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
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(
@@ -680,7 +972,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn.connect(connectConfig);
}
- function handleResize(data: { cols: number; rows: number }) {
+ function handleResize(data: ResizeData) {
if (sshStream && sshStream.setWindow) {
sshStream.setWindow(data.rows, data.cols, data.rows, data.cols);
ws.send(
@@ -702,8 +994,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (sshStream) {
try {
sshStream.end();
- } catch (e: any) {
- sshLogger.error("Error closing stream: " + e.message);
+ } catch (e: unknown) {
+ sshLogger.error(
+ "Error closing stream: " +
+ (e instanceof Error ? e.message : "Unknown error"),
+ );
}
sshStream = null;
}
@@ -711,11 +1006,19 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (sshConn) {
try {
sshConn.end();
- } catch (e: any) {
- sshLogger.error("Error closing connection: " + e.message);
+ } catch (e: unknown) {
+ sshLogger.error(
+ "Error closing connection: " +
+ (e instanceof Error ? e.message : "Unknown error"),
+ );
}
sshConn = null;
}
+
+ totpPromptSent = false;
+ isKeyboardInteractive = false;
+ keyboardInteractiveResponded = false;
+ keyboardInteractiveFinish = null;
}
function setupPingInterval() {
@@ -723,8 +1026,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (sshConn && sshStream) {
try {
sshStream.write("\x00");
- } catch (e: any) {
- sshLogger.error("SSH keepalive failed: " + e.message);
+ } catch (e: unknown) {
+ sshLogger.error(
+ "SSH keepalive failed: " +
+ (e instanceof Error ? e.message : "Unknown error"),
+ );
cleanupSSH();
}
}
diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts
index b49606e6..6262af86 100644
--- a/src/backend/ssh/tunnel.ts
+++ b/src/backend/ssh/tunnel.ts
@@ -33,6 +33,10 @@ app.use(
"http://127.0.0.1:3000",
];
+ if (allowedOrigins.includes(origin)) {
+ return callback(null, true);
+ }
+
if (origin.startsWith("https://")) {
return callback(null, true);
}
@@ -41,10 +45,6 @@ app.use(
return callback(null, true);
}
- if (allowedOrigins.includes(origin)) {
- return callback(null, true);
- }
-
callback(new Error("Not allowed by CORS"));
},
credentials: true,
@@ -217,7 +217,7 @@ function cleanupTunnelResources(
if (verification?.timeout) clearTimeout(verification.timeout);
try {
verification?.conn.end();
- } catch (e) {}
+ } catch {}
tunnelVerifications.delete(tunnelName);
}
@@ -282,7 +282,7 @@ function handleDisconnect(
const verification = tunnelVerifications.get(tunnelName);
if (verification?.timeout) clearTimeout(verification.timeout);
verification?.conn.end();
- } catch (e) {}
+ } catch {}
tunnelVerifications.delete(tunnelName);
}
@@ -511,16 +511,19 @@ async function connectSSHTunnel(
if (credentials.length > 0) {
const credential = credentials[0];
resolvedSourceCredentials = {
- password: credential.password,
- sshKey:
- credential.private_key || credential.privateKey || credential.key,
- keyPassword: credential.key_password || credential.keyPassword,
- keyType: credential.key_type || credential.keyType,
- authMethod: credential.auth_type || credential.authType,
+ password: credential.password as string | undefined,
+ sshKey: (credential.private_key ||
+ credential.privateKey ||
+ credential.key) as string | undefined,
+ keyPassword: (credential.key_password || credential.keyPassword) as
+ | string
+ | undefined,
+ keyType: (credential.key_type || credential.keyType) as
+ | string
+ | undefined,
+ authMethod: (credential.auth_type || credential.authType) as string,
};
- } else {
}
- } else {
}
} catch (error) {
tunnelLogger.warn("Failed to resolve source credentials from database", {
@@ -591,12 +594,17 @@ async function connectSSHTunnel(
if (credentials.length > 0) {
const credential = credentials[0];
resolvedEndpointCredentials = {
- password: credential.password,
- sshKey:
- credential.private_key || credential.privateKey || credential.key,
- keyPassword: credential.key_password || credential.keyPassword,
- keyType: credential.key_type || credential.keyType,
- authMethod: credential.auth_type || credential.authType,
+ password: credential.password as string | undefined,
+ sshKey: (credential.private_key ||
+ credential.privateKey ||
+ credential.key) as string | undefined,
+ keyPassword: (credential.key_password || credential.keyPassword) as
+ | string
+ | undefined,
+ keyType: (credential.key_type || credential.keyType) as
+ | string
+ | undefined,
+ authMethod: (credential.auth_type || credential.authType) as string,
};
} else {
tunnelLogger.warn("No endpoint credentials found in database", {
@@ -605,7 +613,6 @@ async function connectSSHTunnel(
credentialId: tunnelConfig.endpointCredentialId,
});
}
- } else {
}
} catch (error) {
tunnelLogger.warn(
@@ -631,7 +638,7 @@ async function connectSSHTunnel(
try {
conn.end();
- } catch (e) {}
+ } catch {}
activeTunnels.delete(tunnelName);
@@ -771,7 +778,7 @@ async function connectSSHTunnel(
const verification = tunnelVerifications.get(tunnelName);
if (verification?.timeout) clearTimeout(verification.timeout);
verification?.conn.end();
- } catch (e) {}
+ } catch {}
tunnelVerifications.delete(tunnelName);
}
@@ -822,13 +829,9 @@ async function connectSSHTunnel(
}
});
- stream.stdout?.on("data", (data: Buffer) => {
- const output = data.toString().trim();
- if (output) {
- }
- });
+ stream.stdout?.on("data", () => {});
- stream.on("error", (err: Error) => {});
+ stream.on("error", () => {});
stream.stderr.on("data", (data) => {
const errorMsg = data.toString().trim();
@@ -888,42 +891,68 @@ async function connectSSHTunnel(
});
});
- const connOptions: any = {
+ const connOptions: Record = {
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
+ tryKeyboard: true,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
- tcpKeepAliveInitialDelay: 15000,
+ 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: [
+ "curve25519-sha256",
+ "curve25519-sha256@libssh.org",
+ "ecdh-sha2-nistp521",
+ "ecdh-sha2-nistp384",
+ "ecdh-sha2-nistp256",
+ "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
- "diffie-hellman-group1-sha1",
- "diffie-hellman-group-exchange-sha256",
"diffie-hellman-group-exchange-sha1",
- "ecdh-sha2-nistp256",
- "ecdh-sha2-nistp384",
- "ecdh-sha2-nistp521",
+ "diffie-hellman-group1-sha1",
+ ],
+ serverHostKey: [
+ "ssh-ed25519",
+ "ecdsa-sha2-nistp521",
+ "ecdsa-sha2-nistp384",
+ "ecdsa-sha2-nistp256",
+ "rsa-sha2-512",
+ "rsa-sha2-256",
+ "ssh-rsa",
+ "ssh-dss",
],
cipher: [
- "aes128-ctr",
- "aes192-ctr",
- "aes256-ctr",
- "aes128-gcm@openssh.com",
+ "chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
- "aes128-cbc",
- "aes192-cbc",
+ "aes128-gcm@openssh.com",
+ "aes256-ctr",
+ "aes192-ctr",
+ "aes128-ctr",
"aes256-cbc",
+ "aes192-cbc",
+ "aes128-cbc",
"3des-cbc",
],
hmac: [
- "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
- "hmac-sha2-256",
+ "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512",
+ "hmac-sha2-256",
"hmac-sha1",
"hmac-md5",
],
@@ -1026,15 +1055,19 @@ async function killRemoteTunnelByMarker(
if (credentials.length > 0) {
const credential = credentials[0];
resolvedSourceCredentials = {
- password: credential.password,
- sshKey:
- credential.private_key || credential.privateKey || credential.key,
- keyPassword: credential.key_password || credential.keyPassword,
- keyType: credential.key_type || credential.keyType,
- authMethod: credential.auth_type || credential.authType,
+ password: credential.password as string | undefined,
+ sshKey: (credential.private_key ||
+ credential.privateKey ||
+ credential.key) as string | undefined,
+ keyPassword: (credential.key_password || credential.keyPassword) as
+ | string
+ | undefined,
+ keyType: (credential.key_type || credential.keyType) as
+ | string
+ | undefined,
+ authMethod: (credential.auth_type || credential.authType) as string,
};
}
- } else {
}
} catch (error) {
tunnelLogger.warn("Failed to resolve source credentials for cleanup", {
@@ -1046,7 +1079,7 @@ async function killRemoteTunnelByMarker(
}
const conn = new Client();
- const connOptions: any = {
+ const connOptions: Record = {
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
@@ -1122,7 +1155,7 @@ async function killRemoteTunnelByMarker(
conn.on("ready", () => {
const checkCmd = `ps aux | grep -E '(${tunnelMarker}|ssh.*-R.*${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}.*${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}|sshpass.*ssh.*-R.*${tunnelConfig.endpointPort})' | grep -v grep`;
- conn.exec(checkCmd, (err, stream) => {
+ conn.exec(checkCmd, (_err, stream) => {
let foundProcesses = false;
stream.on("data", (data) => {
@@ -1150,7 +1183,7 @@ async function killRemoteTunnelByMarker(
function executeNextKillCommand() {
if (commandIndex >= killCmds.length) {
- conn.exec(checkCmd, (err, verifyStream) => {
+ conn.exec(checkCmd, (_err, verifyStream) => {
let stillRunning = false;
verifyStream.on("data", (data) => {
@@ -1183,19 +1216,14 @@ async function killRemoteTunnelByMarker(
tunnelLogger.warn(
`Kill command ${commandIndex + 1} failed for '${tunnelName}': ${err.message}`,
);
- } else {
}
- stream.on("close", (code) => {
+ stream.on("close", () => {
commandIndex++;
executeNextKillCommand();
});
- stream.on("data", (data) => {
- const output = data.toString().trim();
- if (output) {
- }
- });
+ stream.on("data", () => {});
stream.stderr.on("data", (data) => {
const output = data.toString().trim();
@@ -1381,7 +1409,11 @@ async function initializeAutoStartTunnels(): Promise {
if (endpointHost) {
const tunnelConfig: TunnelConfig = {
- name: `${host.name || `${host.username}@${host.ip}`}_${tunnelConnection.sourcePort}_${tunnelConnection.endpointPort}`,
+ name: `${host.name || `${host.username}@${host.ip}`}_${
+ tunnelConnection.sourcePort
+ }_${tunnelConnection.endpointHost}_${
+ tunnelConnection.endpointPort
+ }`,
hostName: host.name || `${host.username}@${host.ip}`,
sourceIP: host.ip,
sourceSSHPort: host.port,
@@ -1423,14 +1455,6 @@ async function initializeAutoStartTunnels(): Promise {
isPinned: host.pin,
};
- const hasSourcePassword = host.autostartPassword;
- const hasSourceKey = host.autostartKey;
- const hasEndpointPassword =
- tunnelConnection.endpointPassword ||
- endpointHost.autostartPassword;
- const hasEndpointKey =
- tunnelConnection.endpointKey || endpointHost.autostartKey;
-
autoStartTunnels.push(tunnelConfig);
} else {
tunnelLogger.error(
@@ -1453,10 +1477,10 @@ async function initializeAutoStartTunnels(): Promise {
});
}, 1000);
}
- } catch (error: any) {
+ } catch (error) {
tunnelLogger.error(
"Failed to initialize auto-start tunnels:",
- error.message,
+ error instanceof Error ? error.message : "Unknown error",
);
}
}
diff --git a/src/backend/starter.ts b/src/backend/starter.ts
index fb0cfc89..4ab019a6 100644
--- a/src/backend/starter.ts
+++ b/src/backend/starter.ts
@@ -73,7 +73,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
version = foundVersion;
break;
}
- } catch (error) {
+ } catch {
continue;
}
}
@@ -102,6 +102,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
await import("./ssh/tunnel.js");
await import("./ssh/file-manager.js");
await import("./ssh/server-stats.js");
+ await import("./dashboard.js");
process.on("SIGINT", () => {
systemLogger.info(
@@ -126,7 +127,7 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
process.exit(1);
});
- process.on("unhandledRejection", (reason, promise) => {
+ process.on("unhandledRejection", (reason) => {
systemLogger.error("Unhandled promise rejection", reason, {
operation: "error_handling",
});
diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts
index a13db6cd..4c936110 100644
--- a/src/backend/utils/auth-manager.ts
+++ b/src/backend/utils/auth-manager.ts
@@ -4,6 +4,11 @@ import { SystemCrypto } from "./system-crypto.js";
import { DataCrypto } from "./data-crypto.js";
import { databaseLogger } from "./logger.js";
import type { Request, Response, NextFunction } from "express";
+import { db } from "../database/db/index.js";
+import { sessions } from "../database/db/schema.js";
+import { eq, and, sql } from "drizzle-orm";
+import { nanoid } from "nanoid";
+import type { DeviceType } from "./user-agent-parser.js";
interface AuthenticationResult {
success: boolean;
@@ -18,16 +23,28 @@ interface AuthenticationResult {
interface JWTPayload {
userId: string;
+ sessionId?: string;
pendingTOTP?: boolean;
iat?: number;
exp?: number;
}
+interface AuthenticatedRequest extends Request {
+ userId?: string;
+ pendingTOTP?: boolean;
+ dataKey?: Buffer;
+}
+
+interface RequestWithHeaders extends Request {
+ headers: Request["headers"] & {
+ "x-forwarded-proto"?: string;
+ };
+}
+
class AuthManager {
private static instance: AuthManager;
private systemCrypto: SystemCrypto;
private userCrypto: UserCrypto;
- private invalidatedTokens: Set = new Set();
private constructor() {
this.systemCrypto = SystemCrypto.getInstance();
@@ -36,6 +53,21 @@ class AuthManager {
this.userCrypto.setSessionExpiredCallback((userId: string) => {
this.invalidateUserTokens(userId);
});
+
+ setInterval(
+ () => {
+ this.cleanupExpiredSessions().catch((error) => {
+ databaseLogger.error(
+ "Failed to run periodic session cleanup",
+ error,
+ {
+ operation: "session_cleanup_periodic",
+ },
+ );
+ });
+ },
+ 5 * 60 * 1000,
+ );
}
static getInstance(): AuthManager {
@@ -108,7 +140,6 @@ class AuthManager {
if (migrationResult.migrated) {
await saveMemoryDatabaseToFile();
- } else {
}
} catch (error) {
databaseLogger.error("Lazy encryption migration failed", error, {
@@ -121,50 +152,323 @@ class AuthManager {
async generateJWTToken(
userId: string,
- options: { expiresIn?: string; pendingTOTP?: boolean } = {},
+ options: {
+ expiresIn?: string;
+ pendingTOTP?: boolean;
+ deviceType?: DeviceType;
+ deviceInfo?: string;
+ } = {},
): Promise {
const jwtSecret = await this.systemCrypto.getJWTSecret();
+ let expiresIn = options.expiresIn;
+ if (!expiresIn && !options.pendingTOTP) {
+ if (options.deviceType === "desktop" || options.deviceType === "mobile") {
+ expiresIn = "30d";
+ } else {
+ expiresIn = "7d";
+ }
+ } else if (!expiresIn) {
+ expiresIn = "7d";
+ }
+
const payload: JWTPayload = { userId };
if (options.pendingTOTP) {
payload.pendingTOTP = true;
}
- return jwt.sign(payload, jwtSecret, {
- expiresIn: options.expiresIn || "24h",
- } as jwt.SignOptions);
+ if (!options.pendingTOTP && options.deviceType && options.deviceInfo) {
+ const sessionId = nanoid();
+ payload.sessionId = sessionId;
+
+ const token = jwt.sign(payload, jwtSecret, {
+ expiresIn,
+ } as jwt.SignOptions);
+
+ const expirationMs = this.parseExpiresIn(expiresIn);
+ const now = new Date();
+ const expiresAt = new Date(now.getTime() + expirationMs).toISOString();
+ const createdAt = now.toISOString();
+
+ try {
+ await db.insert(sessions).values({
+ id: sessionId,
+ userId,
+ jwtToken: token,
+ deviceType: options.deviceType,
+ deviceInfo: options.deviceInfo,
+ createdAt,
+ expiresAt,
+ lastActiveAt: createdAt,
+ });
+
+ try {
+ const { saveMemoryDatabaseToFile } = await import(
+ "../database/db/index.js"
+ );
+ await saveMemoryDatabaseToFile();
+ } catch (saveError) {
+ databaseLogger.error(
+ "Failed to save database after session creation",
+ saveError,
+ {
+ operation: "session_create_db_save_failed",
+ sessionId,
+ },
+ );
+ }
+ } catch (error) {
+ databaseLogger.error("Failed to create session", error, {
+ operation: "session_create_failed",
+ userId,
+ sessionId,
+ });
+ }
+
+ return token;
+ }
+
+ return jwt.sign(payload, jwtSecret, { expiresIn } as jwt.SignOptions);
+ }
+
+ private parseExpiresIn(expiresIn: string): number {
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
+ if (!match) return 7 * 24 * 60 * 60 * 1000;
+
+ const value = parseInt(match[1]);
+ const unit = match[2];
+
+ switch (unit) {
+ case "s":
+ return value * 1000;
+ case "m":
+ return value * 60 * 1000;
+ case "h":
+ return value * 60 * 60 * 1000;
+ case "d":
+ return value * 24 * 60 * 60 * 1000;
+ default:
+ return 7 * 24 * 60 * 60 * 1000;
+ }
}
async verifyJWTToken(token: string): Promise {
try {
- if (this.invalidatedTokens.has(token)) {
- return null;
- }
-
const jwtSecret = await this.systemCrypto.getJWTSecret();
+
const payload = jwt.verify(token, jwtSecret) as JWTPayload;
+
+ if (payload.sessionId) {
+ try {
+ const sessionRecords = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.id, payload.sessionId))
+ .limit(1);
+
+ if (sessionRecords.length === 0) {
+ databaseLogger.warn("Session not found during JWT verification", {
+ operation: "jwt_verify_session_not_found",
+ sessionId: payload.sessionId,
+ userId: payload.userId,
+ });
+ return null;
+ }
+ } catch (dbError) {
+ databaseLogger.error(
+ "Failed to check session in database during JWT verification",
+ dbError,
+ {
+ operation: "jwt_verify_session_check_failed",
+ sessionId: payload.sessionId,
+ },
+ );
+ return null;
+ }
+ }
return payload;
} catch (error) {
databaseLogger.warn("JWT verification failed", {
operation: "jwt_verify_failed",
error: error instanceof Error ? error.message : "Unknown error",
+ errorName: error instanceof Error ? error.name : "Unknown",
});
return null;
}
}
- invalidateJWTToken(token: string): void {
- this.invalidatedTokens.add(token);
+ invalidateJWTToken(token: string): void {}
+
+ invalidateUserTokens(userId: string): void {}
+
+ async revokeSession(sessionId: string): Promise {
+ try {
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
+
+ try {
+ const { saveMemoryDatabaseToFile } = await import(
+ "../database/db/index.js"
+ );
+ await saveMemoryDatabaseToFile();
+ } catch (saveError) {
+ databaseLogger.error(
+ "Failed to save database after session revocation",
+ saveError,
+ {
+ operation: "session_revoke_db_save_failed",
+ sessionId,
+ },
+ );
+ }
+
+ return true;
+ } catch (error) {
+ databaseLogger.error("Failed to delete session", error, {
+ operation: "session_delete_failed",
+ sessionId,
+ });
+ return false;
+ }
}
- invalidateUserTokens(userId: string): void {
- databaseLogger.info("User tokens invalidated due to data lock", {
- operation: "user_tokens_invalidate",
- userId,
- });
+ async revokeAllUserSessions(
+ userId: string,
+ exceptSessionId?: string,
+ ): Promise {
+ try {
+ const userSessions = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.userId, userId));
+
+ const deletedCount = userSessions.filter(
+ (s) => !exceptSessionId || s.id !== exceptSessionId,
+ ).length;
+
+ if (exceptSessionId) {
+ await db
+ .delete(sessions)
+ .where(
+ and(
+ eq(sessions.userId, userId),
+ sql`${sessions.id} != ${exceptSessionId}`,
+ ),
+ );
+ } else {
+ await db.delete(sessions).where(eq(sessions.userId, userId));
+ }
+
+ try {
+ const { saveMemoryDatabaseToFile } = await import(
+ "../database/db/index.js"
+ );
+ await saveMemoryDatabaseToFile();
+ } catch (saveError) {
+ databaseLogger.error(
+ "Failed to save database after revoking all user sessions",
+ saveError,
+ {
+ operation: "user_sessions_revoke_db_save_failed",
+ userId,
+ },
+ );
+ }
+
+ return deletedCount;
+ } catch (error) {
+ databaseLogger.error("Failed to delete user sessions", error, {
+ operation: "user_sessions_delete_failed",
+ userId,
+ });
+ return 0;
+ }
}
- getSecureCookieOptions(req: any, maxAge: number = 24 * 60 * 60 * 1000) {
+ async cleanupExpiredSessions(): Promise {
+ try {
+ const expiredSessions = await db
+ .select()
+ .from(sessions)
+ .where(sql`${sessions.expiresAt} < datetime('now')`);
+
+ const expiredCount = expiredSessions.length;
+
+ if (expiredCount === 0) {
+ return 0;
+ }
+
+ await db
+ .delete(sessions)
+ .where(sql`${sessions.expiresAt} < datetime('now')`);
+
+ try {
+ const { saveMemoryDatabaseToFile } = await import(
+ "../database/db/index.js"
+ );
+ await saveMemoryDatabaseToFile();
+ } catch (saveError) {
+ databaseLogger.error(
+ "Failed to save database after cleaning up expired sessions",
+ saveError,
+ {
+ operation: "sessions_cleanup_db_save_failed",
+ },
+ );
+ }
+
+ const affectedUsers = new Set(expiredSessions.map((s) => s.userId));
+ for (const userId of affectedUsers) {
+ const remainingSessions = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.userId, userId));
+
+ if (remainingSessions.length === 0) {
+ this.userCrypto.logoutUser(userId);
+ }
+ }
+
+ return expiredCount;
+ } catch (error) {
+ databaseLogger.error("Failed to cleanup expired sessions", error, {
+ operation: "sessions_cleanup_failed",
+ });
+ return 0;
+ }
+ }
+
+ async getAllSessions(): Promise {
+ try {
+ const allSessions = await db.select().from(sessions);
+ return allSessions;
+ } catch (error) {
+ databaseLogger.error("Failed to get all sessions", error, {
+ operation: "sessions_get_all_failed",
+ });
+ return [];
+ }
+ }
+
+ async getUserSessions(userId: string): Promise {
+ try {
+ const userSessions = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.userId, userId));
+ return userSessions;
+ } catch (error) {
+ databaseLogger.error("Failed to get user sessions", error, {
+ operation: "sessions_get_user_failed",
+ userId,
+ });
+ return [];
+ }
+ }
+
+ getSecureCookieOptions(
+ req: RequestWithHeaders,
+ maxAge: number = 7 * 24 * 60 * 60 * 1000,
+ ) {
return {
httpOnly: false,
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
@@ -176,10 +480,11 @@ class AuthManager {
createAuthMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => {
- let token = req.cookies?.jwt;
+ const authReq = req as AuthenticatedRequest;
+ let token = authReq.cookies?.jwt;
if (!token) {
- const authHeader = req.headers["authorization"];
+ const authHeader = authReq.headers["authorization"];
if (authHeader?.startsWith("Bearer ")) {
token = authHeader.split(" ")[1];
}
@@ -195,40 +500,142 @@ class AuthManager {
return res.status(401).json({ error: "Invalid token" });
}
- (req as any).userId = payload.userId;
- (req as any).pendingTOTP = payload.pendingTOTP;
+ if (payload.sessionId) {
+ try {
+ const sessionRecords = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.id, payload.sessionId))
+ .limit(1);
+
+ if (sessionRecords.length === 0) {
+ databaseLogger.warn("Session not found in middleware", {
+ operation: "middleware_session_not_found",
+ sessionId: payload.sessionId,
+ userId: payload.userId,
+ });
+ return res.status(401).json({
+ error: "Session not found",
+ code: "SESSION_NOT_FOUND",
+ });
+ }
+
+ const session = sessionRecords[0];
+
+ const sessionExpiryTime = new Date(session.expiresAt).getTime();
+ const currentTime = Date.now();
+ const isExpired = sessionExpiryTime < currentTime;
+
+ if (isExpired) {
+ databaseLogger.warn("Session has expired", {
+ operation: "session_expired",
+ sessionId: payload.sessionId,
+ expiresAt: session.expiresAt,
+ expiryTime: sessionExpiryTime,
+ currentTime: currentTime,
+ difference: currentTime - sessionExpiryTime,
+ });
+
+ db.delete(sessions)
+ .where(eq(sessions.id, payload.sessionId))
+ .then(async () => {
+ try {
+ const { saveMemoryDatabaseToFile } = await import(
+ "../database/db/index.js"
+ );
+ await saveMemoryDatabaseToFile();
+
+ const remainingSessions = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.userId, payload.userId));
+
+ if (remainingSessions.length === 0) {
+ this.userCrypto.logoutUser(payload.userId);
+ }
+ } catch (cleanupError) {
+ databaseLogger.error(
+ "Failed to cleanup after expired session",
+ cleanupError,
+ {
+ operation: "expired_session_cleanup_failed",
+ sessionId: payload.sessionId,
+ },
+ );
+ }
+ })
+ .catch((error) => {
+ databaseLogger.error(
+ "Failed to delete expired session",
+ error,
+ {
+ operation: "expired_session_delete_failed",
+ sessionId: payload.sessionId,
+ },
+ );
+ });
+
+ return res.status(401).json({
+ error: "Session has expired",
+ code: "SESSION_EXPIRED",
+ });
+ }
+
+ db.update(sessions)
+ .set({ lastActiveAt: new Date().toISOString() })
+ .where(eq(sessions.id, payload.sessionId))
+ .then(() => {})
+ .catch((error) => {
+ databaseLogger.warn("Failed to update session lastActiveAt", {
+ operation: "session_update_last_active",
+ sessionId: payload.sessionId,
+ error: error instanceof Error ? error.message : "Unknown error",
+ });
+ });
+ } catch (error) {
+ databaseLogger.error("Session check failed in middleware", error, {
+ operation: "middleware_session_check_failed",
+ sessionId: payload.sessionId,
+ });
+ return res.status(500).json({ error: "Session check failed" });
+ }
+ }
+
+ authReq.userId = payload.userId;
+ authReq.pendingTOTP = payload.pendingTOTP;
next();
};
}
createDataAccessMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => {
- const userId = (req as any).userId;
+ const authReq = req as AuthenticatedRequest;
+ const userId = authReq.userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
}
const dataKey = this.userCrypto.getUserDataKey(userId);
- if (!dataKey) {
- return res.status(401).json({
- error: "Session expired - please log in again",
- code: "SESSION_EXPIRED",
- });
- }
-
- (req as any).dataKey = dataKey;
+ authReq.dataKey = dataKey || undefined;
next();
};
}
createAdminMiddleware() {
return async (req: Request, res: Response, next: NextFunction) => {
- const authHeader = req.headers["authorization"];
- if (!authHeader?.startsWith("Bearer ")) {
- return res.status(401).json({ error: "Missing Authorization header" });
+ let token = req.cookies?.jwt;
+
+ if (!token) {
+ const authHeader = req.headers["authorization"];
+ if (authHeader?.startsWith("Bearer ")) {
+ token = authHeader.split(" ")[1];
+ }
+ }
+
+ if (!token) {
+ return res.status(401).json({ error: "Missing authentication token" });
}
- const token = authHeader.split(" ")[1];
const payload = await this.verifyJWTToken(token);
if (!payload) {
@@ -257,8 +664,9 @@ class AuthManager {
return res.status(403).json({ error: "Admin access required" });
}
- (req as any).userId = payload.userId;
- (req as any).pendingTOTP = payload.pendingTOTP;
+ const authReq = req as AuthenticatedRequest;
+ authReq.userId = payload.userId;
+ authReq.pendingTOTP = payload.pendingTOTP;
next();
} catch (error) {
databaseLogger.error("Failed to verify admin privileges", error, {
@@ -272,8 +680,47 @@ class AuthManager {
};
}
- logoutUser(userId: string): void {
- this.userCrypto.logoutUser(userId);
+ async logoutUser(userId: string, sessionId?: string): Promise {
+ if (sessionId) {
+ try {
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
+
+ try {
+ const { saveMemoryDatabaseToFile } = await import(
+ "../database/db/index.js"
+ );
+ await saveMemoryDatabaseToFile();
+ } catch (saveError) {
+ databaseLogger.error(
+ "Failed to save database after logout",
+ saveError,
+ {
+ operation: "logout_db_save_failed",
+ userId,
+ sessionId,
+ },
+ );
+ }
+
+ const remainingSessions = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.userId, userId));
+
+ if (remainingSessions.length === 0) {
+ this.userCrypto.logoutUser(userId);
+ } else {
+ }
+ } catch (error) {
+ databaseLogger.error("Failed to delete session on logout", error, {
+ operation: "session_delete_logout_failed",
+ userId,
+ sessionId,
+ });
+ }
+ } else {
+ this.userCrypto.logoutUser(userId);
+ }
}
getUserDataKey(userId: string): Buffer | null {
diff --git a/src/backend/utils/auto-ssl-setup.ts b/src/backend/utils/auto-ssl-setup.ts
index e2d1034a..e45ce2ec 100644
--- a/src/backend/utils/auto-ssl-setup.ts
+++ b/src/backend/utils/auto-ssl-setup.ts
@@ -1,7 +1,6 @@
import { execSync } from "child_process";
import { promises as fs } from "fs";
import path from "path";
-import crypto from "crypto";
import { systemLogger } from "./logger.js";
export class AutoSSLSetup {
@@ -102,7 +101,7 @@ export class AutoSSLSetup {
try {
try {
execSync("openssl version", { stdio: "pipe" });
- } catch (error) {
+ } catch {
throw new Error(
"OpenSSL is not installed or not available in PATH. Please install OpenSSL to enable SSL certificate generation.",
);
diff --git a/src/backend/utils/data-crypto.ts b/src/backend/utils/data-crypto.ts
index 870d0d5f..462d2956 100644
--- a/src/backend/utils/data-crypto.ts
+++ b/src/backend/utils/data-crypto.ts
@@ -3,6 +3,19 @@ import { LazyFieldEncryption } from "./lazy-field-encryption.js";
import { UserCrypto } from "./user-crypto.js";
import { databaseLogger } from "./logger.js";
+interface DatabaseInstance {
+ prepare: (sql: string) => {
+ all: (param?: unknown) => DatabaseRecord[];
+ get: (param?: unknown) => DatabaseRecord;
+ run: (...params: unknown[]) => unknown;
+ };
+}
+
+interface DatabaseRecord {
+ id: number | string;
+ [key: string]: unknown;
+}
+
class DataCrypto {
private static userCrypto: UserCrypto;
@@ -10,13 +23,13 @@ class DataCrypto {
this.userCrypto = UserCrypto.getInstance();
}
- static encryptRecord(
+ static encryptRecord>(
tableName: string,
- record: any,
+ record: T,
userId: string,
userDataKey: Buffer,
- ): any {
- const encryptedRecord = { ...record };
+ ): T {
+ const encryptedRecord: Record = { ...record };
const recordId = record.id || "temp-" + Date.now();
for (const [fieldName, value] of Object.entries(record)) {
@@ -24,24 +37,24 @@ class DataCrypto {
encryptedRecord[fieldName] = FieldCrypto.encryptField(
value as string,
userDataKey,
- recordId,
+ recordId as string,
fieldName,
);
}
}
- return encryptedRecord;
+ return encryptedRecord as T;
}
- static decryptRecord(
+ static decryptRecord>(
tableName: string,
- record: any,
+ record: T,
userId: string,
userDataKey: Buffer,
- ): any {
+ ): T {
if (!record) return record;
- const decryptedRecord = { ...record };
+ const decryptedRecord: Record = { ...record };
const recordId = record.id;
for (const [fieldName, value] of Object.entries(record)) {
@@ -49,21 +62,21 @@ class DataCrypto {
decryptedRecord[fieldName] = LazyFieldEncryption.safeGetFieldValue(
value as string,
userDataKey,
- recordId,
+ recordId as string,
fieldName,
);
}
}
- return decryptedRecord;
+ return decryptedRecord as T;
}
- static decryptRecords(
+ static decryptRecords>(
tableName: string,
- records: any[],
+ records: T[],
userId: string,
userDataKey: Buffer,
- ): any[] {
+ ): T[] {
if (!Array.isArray(records)) return records;
return records.map((record) =>
this.decryptRecord(tableName, record, userId, userDataKey),
@@ -73,7 +86,7 @@ class DataCrypto {
static async migrateUserSensitiveFields(
userId: string,
userDataKey: Buffer,
- db: any,
+ db: DatabaseInstance,
): Promise<{
migrated: boolean;
migratedTables: string[];
@@ -84,7 +97,7 @@ class DataCrypto {
let migratedFieldsCount = 0;
try {
- const { needsMigration, plaintextFields } =
+ const { needsMigration } =
await LazyFieldEncryption.checkUserNeedsMigration(
userId,
userDataKey,
@@ -97,7 +110,7 @@ class DataCrypto {
const sshDataRecords = db
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
- .all(userId);
+ .all(userId) as DatabaseRecord[];
for (const record of sshDataRecords) {
const sensitiveFields =
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_data");
@@ -112,13 +125,17 @@ class DataCrypto {
if (needsUpdate) {
const updateQuery = `
UPDATE ssh_data
- SET password = ?, key = ?, key_password = ?, updated_at = CURRENT_TIMESTAMP
+ SET password = ?, key = ?, key_password = ?, key_type = ?, autostart_password = ?, autostart_key = ?, autostart_key_password = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`;
db.prepare(updateQuery).run(
updatedRecord.password || null,
updatedRecord.key || null,
- updatedRecord.key_password || null,
+ updatedRecord.key_password || updatedRecord.keyPassword || null,
+ updatedRecord.keyType || null,
+ updatedRecord.autostartPassword || null,
+ updatedRecord.autostartKey || null,
+ updatedRecord.autostartKeyPassword || null,
record.id,
);
@@ -132,7 +149,7 @@ class DataCrypto {
const sshCredentialsRecords = db
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
- .all(userId);
+ .all(userId) as DatabaseRecord[];
for (const record of sshCredentialsRecords) {
const sensitiveFields =
LazyFieldEncryption.getSensitiveFieldsForTable("ssh_credentials");
@@ -147,15 +164,16 @@ class DataCrypto {
if (needsUpdate) {
const updateQuery = `
UPDATE ssh_credentials
- SET password = ?, key = ?, key_password = ?, private_key = ?, public_key = ?, updated_at = CURRENT_TIMESTAMP
+ SET password = ?, key = ?, key_password = ?, private_key = ?, public_key = ?, key_type = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`;
db.prepare(updateQuery).run(
updatedRecord.password || null,
updatedRecord.key || null,
- updatedRecord.key_password || null,
- updatedRecord.private_key || null,
- updatedRecord.public_key || null,
+ updatedRecord.key_password || updatedRecord.keyPassword || null,
+ updatedRecord.private_key || updatedRecord.privateKey || null,
+ updatedRecord.public_key || updatedRecord.publicKey || null,
+ updatedRecord.keyType || null,
record.id,
);
@@ -169,7 +187,7 @@ class DataCrypto {
const userRecord = db
.prepare("SELECT * FROM users WHERE id = ?")
- .get(userId);
+ .get(userId) as DatabaseRecord | undefined;
if (userRecord) {
const sensitiveFields =
LazyFieldEncryption.getSensitiveFieldsForTable("users");
@@ -184,12 +202,18 @@ class DataCrypto {
if (needsUpdate) {
const updateQuery = `
UPDATE users
- SET totp_secret = ?, totp_backup_codes = ?
+ SET totp_secret = ?, totp_backup_codes = ?, client_secret = ?, oidc_identifier = ?
WHERE id = ?
`;
db.prepare(updateQuery).run(
- updatedRecord.totp_secret || null,
- updatedRecord.totp_backup_codes || null,
+ updatedRecord.totp_secret || updatedRecord.totpSecret || null,
+ updatedRecord.totp_backup_codes ||
+ updatedRecord.totpBackupCodes ||
+ null,
+ updatedRecord.client_secret || updatedRecord.clientSecret || null,
+ updatedRecord.oidc_identifier ||
+ updatedRecord.oidcIdentifier ||
+ null,
userId,
);
@@ -220,7 +244,7 @@ class DataCrypto {
static async reencryptUserDataAfterPasswordReset(
userId: string,
newUserDataKey: Buffer,
- db: any,
+ db: DatabaseInstance,
): Promise<{
success: boolean;
reencryptedTables: string[];
@@ -236,24 +260,44 @@ class DataCrypto {
try {
const tablesToReencrypt = [
- { table: "ssh_data", fields: ["password", "key", "key_password"] },
+ {
+ table: "ssh_data",
+ fields: [
+ "password",
+ "key",
+ "key_password",
+ "keyPassword",
+ "keyType",
+ "autostartPassword",
+ "autostartKey",
+ "autostartKeyPassword",
+ ],
+ },
{
table: "ssh_credentials",
fields: [
"password",
"private_key",
+ "privateKey",
"key_password",
+ "keyPassword",
"key",
"public_key",
+ "publicKey",
+ "keyType",
],
},
{
table: "users",
fields: [
"client_secret",
+ "clientSecret",
"totp_secret",
+ "totpSecret",
"totp_backup_codes",
+ "totpBackupCodes",
"oidc_identifier",
+ "oidcIdentifier",
],
},
];
@@ -262,17 +306,21 @@ class DataCrypto {
try {
const records = db
.prepare(`SELECT * FROM ${table} WHERE user_id = ?`)
- .all(userId);
+ .all(userId) as DatabaseRecord[];
for (const record of records) {
const recordId = record.id.toString();
+ const updatedRecord: DatabaseRecord = { ...record };
let needsUpdate = false;
- const updatedRecord = { ...record };
for (const fieldName of fields) {
const fieldValue = record[fieldName];
- if (fieldValue && fieldValue.trim() !== "") {
+ if (
+ fieldValue &&
+ typeof fieldValue === "string" &&
+ fieldValue.trim() !== ""
+ ) {
try {
const reencryptedValue = FieldCrypto.encryptField(
fieldValue,
@@ -345,18 +393,6 @@ class DataCrypto {
result.success = result.errors.length === 0;
- databaseLogger.info(
- "User data re-encryption completed after password reset",
- {
- operation: "password_reset_reencrypt_completed",
- userId,
- success: result.success,
- reencryptedTables: result.reencryptedTables,
- reencryptedFieldsCount: result.reencryptedFieldsCount,
- errorsCount: result.errors.length,
- },
- );
-
return result;
} catch (error) {
databaseLogger.error(
@@ -384,29 +420,29 @@ class DataCrypto {
return userDataKey;
}
- static encryptRecordForUser(
+ static encryptRecordForUser>(
tableName: string,
- record: any,
+ record: T,
userId: string,
- ): any {
+ ): T {
const userDataKey = this.validateUserAccess(userId);
return this.encryptRecord(tableName, record, userId, userDataKey);
}
- static decryptRecordForUser(
+ static decryptRecordForUser>(
tableName: string,
- record: any,
+ record: T,
userId: string,
- ): any {
+ ): T {
const userDataKey = this.validateUserAccess(userId);
return this.decryptRecord(tableName, record, userId, userDataKey);
}
- static decryptRecordsForUser(
+ static decryptRecordsForUser>(
tableName: string,
- records: any[],
+ records: T[],
userId: string,
- ): any[] {
+ ): T[] {
const userDataKey = this.validateUserAccess(userId);
return this.decryptRecords(tableName, records, userId, userDataKey);
}
@@ -435,7 +471,7 @@ class DataCrypto {
);
return decrypted === testData;
- } catch (error) {
+ } catch {
return false;
}
}
diff --git a/src/backend/utils/database-file-encryption.ts b/src/backend/utils/database-file-encryption.ts
index db302d2e..002464b0 100644
--- a/src/backend/utils/database-file-encryption.ts
+++ b/src/backend/utils/database-file-encryption.ts
@@ -30,7 +30,11 @@ class DatabaseFileEncryption {
const iv = crypto.randomBytes(16);
- const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
+ const cipher = crypto.createCipheriv(
+ this.ALGORITHM,
+ key,
+ iv,
+ ) as crypto.CipherGCM;
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
const tag = cipher.getAuthTag();
@@ -78,7 +82,11 @@ class DatabaseFileEncryption {
const iv = crypto.randomBytes(16);
- const cipher = crypto.createCipheriv(this.ALGORITHM, key, iv) as any;
+ const cipher = crypto.createCipheriv(
+ this.ALGORITHM,
+ key,
+ iv,
+ ) as crypto.CipherGCM;
const encrypted = Buffer.concat([
cipher.update(sourceData),
cipher.final(),
@@ -163,7 +171,7 @@ class DatabaseFileEncryption {
metadata.algorithm,
key,
Buffer.from(metadata.iv, "hex"),
- ) as any;
+ ) as crypto.DecipherGCM;
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
const decryptedBuffer = Buffer.concat([
@@ -233,7 +241,7 @@ class DatabaseFileEncryption {
metadata.algorithm,
key,
Buffer.from(metadata.iv, "hex"),
- ) as any;
+ ) as crypto.DecipherGCM;
decipher.setAuthTag(Buffer.from(metadata.tag, "hex"));
const decrypted = Buffer.concat([
@@ -301,7 +309,6 @@ class DatabaseFileEncryption {
const metadata: EncryptedFileMetadata = JSON.parse(metadataContent);
const fileStats = fs.statSync(encryptedPath);
- const currentFingerprint = "termix-v1-file";
return {
version: metadata.version,
diff --git a/src/backend/utils/database-migration.ts b/src/backend/utils/database-migration.ts
index ebf3192b..7172fc40 100644
--- a/src/backend/utils/database-migration.ts
+++ b/src/backend/utils/database-migration.ts
@@ -55,7 +55,6 @@ export class DatabaseMigration {
if (hasEncryptedDb && hasUnencryptedDb) {
const unencryptedSize = fs.statSync(this.unencryptedDbPath).size;
- const encryptedSize = fs.statSync(this.encryptedDbPath).size;
if (unencryptedSize === 0) {
needsMigration = false;
@@ -63,10 +62,6 @@ export class DatabaseMigration {
"Empty unencrypted database found alongside encrypted database. Removing empty file.";
try {
fs.unlinkSync(this.unencryptedDbPath);
- databaseLogger.info("Removed empty unencrypted database file", {
- operation: "migration_cleanup_empty",
- path: this.unencryptedDbPath,
- });
} catch (error) {
databaseLogger.warn("Failed to remove empty unencrypted database", {
operation: "migration_cleanup_empty_failed",
@@ -168,9 +163,6 @@ export class DatabaseMigration {
return false;
}
- let totalOriginalRows = 0;
- let totalMemoryRows = 0;
-
for (const table of originalTables) {
const originalCount = originalDb
.prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
@@ -179,9 +171,6 @@ export class DatabaseMigration {
.prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
.get() as { count: number };
- totalOriginalRows += originalCount.count;
- totalMemoryRows += memoryCount.count;
-
if (originalCount.count !== memoryCount.count) {
databaseLogger.error(
"Row count mismatch for table during migration verification",
@@ -241,7 +230,9 @@ export class DatabaseMigration {
memoryDb.exec("PRAGMA foreign_keys = OFF");
for (const table of tables) {
- const rows = originalDb.prepare(`SELECT * FROM ${table.name}`).all();
+ const rows = originalDb
+ .prepare(`SELECT * FROM ${table.name}`)
+ .all() as Record[];
if (rows.length > 0) {
const columns = Object.keys(rows[0]);
@@ -251,7 +242,7 @@ export class DatabaseMigration {
);
const insertTransaction = memoryDb.transaction(
- (dataRows: any[]) => {
+ (dataRows: Record[]) => {
for (const row of dataRows) {
const values = columns.map((col) => row[col]);
insertStmt.run(values);
diff --git a/src/backend/utils/database-save-trigger.ts b/src/backend/utils/database-save-trigger.ts
index 15bc05bc..b3c2da21 100644
--- a/src/backend/utils/database-save-trigger.ts
+++ b/src/backend/utils/database-save-trigger.ts
@@ -71,11 +71,6 @@ export class DatabaseSaveTrigger {
this.pendingSave = true;
try {
- databaseLogger.info("Force saving database", {
- operation: "db_save_trigger_force_start",
- reason,
- });
-
await this.saveFunction();
} catch (error) {
databaseLogger.error("Database force save failed", error, {
@@ -110,9 +105,5 @@ export class DatabaseSaveTrigger {
this.pendingSave = false;
this.isInitialized = false;
this.saveFunction = null;
-
- databaseLogger.info("Database save trigger cleaned up", {
- operation: "db_save_trigger_cleanup",
- });
}
}
diff --git a/src/backend/utils/field-crypto.ts b/src/backend/utils/field-crypto.ts
index 098b5b8e..824df007 100644
--- a/src/backend/utils/field-crypto.ts
+++ b/src/backend/utils/field-crypto.ts
@@ -17,18 +17,36 @@ class FieldCrypto {
private static readonly ENCRYPTED_FIELDS = {
users: new Set([
"password_hash",
+ "passwordHash",
"client_secret",
+ "clientSecret",
"totp_secret",
+ "totpSecret",
"totp_backup_codes",
+ "totpBackupCodes",
"oidc_identifier",
+ "oidcIdentifier",
+ ]),
+ ssh_data: new Set([
+ "password",
+ "key",
+ "key_password",
+ "keyPassword",
+ "keyType",
+ "autostartPassword",
+ "autostartKey",
+ "autostartKeyPassword",
]),
- ssh_data: new Set(["password", "key", "key_password"]),
ssh_credentials: new Set([
"password",
"private_key",
+ "privateKey",
"key_password",
+ "keyPassword",
"key",
"public_key",
+ "publicKey",
+ "keyType",
]),
};
@@ -47,7 +65,11 @@ class FieldCrypto {
);
const iv = crypto.randomBytes(this.IV_LENGTH);
- const cipher = crypto.createCipheriv(this.ALGORITHM, fieldKey, iv) as any;
+ const cipher = crypto.createCipheriv(
+ this.ALGORITHM,
+ fieldKey,
+ iv,
+ ) as crypto.CipherGCM;
let encrypted = cipher.update(plaintext, "utf8", "hex");
encrypted += cipher.final("hex");
@@ -89,7 +111,7 @@ class FieldCrypto {
this.ALGORITHM,
fieldKey,
Buffer.from(encrypted.iv, "hex"),
- ) as any;
+ ) as crypto.DecipherGCM;
decipher.setAuthTag(Buffer.from(encrypted.tag, "hex"));
let decrypted = decipher.update(encrypted.data, "hex", "utf8");
diff --git a/src/backend/utils/lazy-field-encryption.ts b/src/backend/utils/lazy-field-encryption.ts
index efe5ea75..6be7b44d 100644
--- a/src/backend/utils/lazy-field-encryption.ts
+++ b/src/backend/utils/lazy-field-encryption.ts
@@ -1,6 +1,14 @@
import { FieldCrypto } from "./field-crypto.js";
import { databaseLogger } from "./logger.js";
+interface DatabaseInstance {
+ prepare: (sql: string) => {
+ all: (param?: unknown) => unknown[];
+ get: (param?: unknown) => unknown;
+ run: (...params: unknown[]) => unknown;
+ };
+}
+
export class LazyFieldEncryption {
private static readonly LEGACY_FIELD_NAME_MAP: Record = {
key_password: "keyPassword",
@@ -39,7 +47,7 @@ export class LazyFieldEncryption {
return false;
}
return true;
- } catch (jsonError) {
+ } catch {
return true;
}
}
@@ -74,7 +82,7 @@ export class LazyFieldEncryption {
legacyFieldName,
);
return decrypted;
- } catch (legacyError) {}
+ } catch {}
}
const sensitiveFields = [
@@ -145,7 +153,7 @@ export class LazyFieldEncryption {
wasPlaintext: false,
wasLegacyEncryption: false,
};
- } catch (error) {
+ } catch {
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
if (legacyFieldName) {
try {
@@ -166,7 +174,7 @@ export class LazyFieldEncryption {
wasPlaintext: false,
wasLegacyEncryption: true,
};
- } catch (legacyError) {}
+ } catch {}
}
return {
encrypted: fieldValue,
@@ -178,12 +186,12 @@ export class LazyFieldEncryption {
}
static migrateRecordSensitiveFields(
- record: any,
+ record: Record,
sensitiveFields: string[],
userKEK: Buffer,
recordId: string,
): {
- updatedRecord: any;
+ updatedRecord: Record;
migratedFields: string[];
needsUpdate: boolean;
} {
@@ -198,7 +206,7 @@ export class LazyFieldEncryption {
try {
const { encrypted, wasPlaintext, wasLegacyEncryption } =
this.migrateFieldToEncrypted(
- fieldValue,
+ fieldValue as string,
userKEK,
recordId,
fieldName,
@@ -253,7 +261,7 @@ export class LazyFieldEncryption {
try {
FieldCrypto.decryptField(fieldValue, userKEK, recordId, fieldName);
return false;
- } catch (error) {
+ } catch {
const legacyFieldName = this.LEGACY_FIELD_NAME_MAP[fieldName];
if (legacyFieldName) {
try {
@@ -264,7 +272,7 @@ export class LazyFieldEncryption {
legacyFieldName,
);
return true;
- } catch (legacyError) {
+ } catch {
return false;
}
}
@@ -275,7 +283,7 @@ export class LazyFieldEncryption {
static async checkUserNeedsMigration(
userId: string,
userKEK: Buffer,
- db: any,
+ db: DatabaseInstance,
): Promise<{
needsMigration: boolean;
plaintextFields: Array<{
@@ -294,7 +302,9 @@ export class LazyFieldEncryption {
try {
const sshHosts = db
.prepare("SELECT * FROM ssh_data WHERE user_id = ?")
- .all(userId);
+ .all(userId) as Array<
+ Record & { id: string | number }
+ >;
for (const host of sshHosts) {
const sensitiveFields = this.getSensitiveFieldsForTable("ssh_data");
const hostPlaintextFields: string[] = [];
@@ -303,7 +313,7 @@ export class LazyFieldEncryption {
if (
host[field] &&
this.fieldNeedsMigration(
- host[field],
+ host[field] as string,
userKEK,
host.id.toString(),
field,
@@ -325,7 +335,9 @@ export class LazyFieldEncryption {
const sshCredentials = db
.prepare("SELECT * FROM ssh_credentials WHERE user_id = ?")
- .all(userId);
+ .all(userId) as Array<
+ Record & { id: string | number }
+ >;
for (const credential of sshCredentials) {
const sensitiveFields =
this.getSensitiveFieldsForTable("ssh_credentials");
@@ -335,7 +347,7 @@ export class LazyFieldEncryption {
if (
credential[field] &&
this.fieldNeedsMigration(
- credential[field],
+ credential[field] as string,
userKEK,
credential.id.toString(),
field,
diff --git a/src/backend/utils/logger.ts b/src/backend/utils/logger.ts
index f020047a..41f44982 100644
--- a/src/backend/utils/logger.ts
+++ b/src/backend/utils/logger.ts
@@ -11,7 +11,7 @@ export interface LogContext {
sessionId?: string;
requestId?: string;
duration?: number;
- [key: string]: any;
+ [key: string]: unknown;
}
const SENSITIVE_FIELDS = [
@@ -253,5 +253,6 @@ 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 dashboardLogger = new Logger("DASHBOARD", "📊", "#ec4899");
export const logger = systemLogger;
diff --git a/src/backend/utils/simple-db-ops.ts b/src/backend/utils/simple-db-ops.ts
index c324e0a2..6fbd7a63 100644
--- a/src/backend/utils/simple-db-ops.ts
+++ b/src/backend/utils/simple-db-ops.ts
@@ -2,10 +2,10 @@ import { getDb, DatabaseSaveTrigger } from "../database/db/index.js";
import { DataCrypto } from "./data-crypto.js";
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
-type TableName = "users" | "ssh_data" | "ssh_credentials";
+type TableName = "users" | "ssh_data" | "ssh_credentials" | "recent_activity";
class SimpleDBOps {
- static async insert>(
+ static async insert>(
table: SQLiteTable,
tableName: TableName,
data: T,
@@ -44,8 +44,8 @@ class SimpleDBOps {
return decryptedResult as T;
}
- static async select>(
- query: any,
+ static async select>(
+ query: unknown,
tableName: TableName,
userId: string,
): Promise {
@@ -56,9 +56,9 @@ class SimpleDBOps {
const results = await query;
- const decryptedResults = DataCrypto.decryptRecords(
+ const decryptedResults = DataCrypto.decryptRecords(
tableName,
- results,
+ results as T[],
userId,
userDataKey,
);
@@ -66,8 +66,8 @@ class SimpleDBOps {
return decryptedResults;
}
- static async selectOne>(
- query: any,
+ static async selectOne>(
+ query: unknown,
tableName: TableName,
userId: string,
): Promise {
@@ -79,9 +79,9 @@ class SimpleDBOps {
const result = await query;
if (!result) return undefined;
- const decryptedResult = DataCrypto.decryptRecord(
+ const decryptedResult = DataCrypto.decryptRecord(
tableName,
- result,
+ result as T,
userId,
userDataKey,
);
@@ -89,10 +89,10 @@ class SimpleDBOps {
return decryptedResult;
}
- static async update>(
+ static async update>(
table: SQLiteTable,
tableName: TableName,
- where: any,
+ where: unknown,
data: Partial,
userId: string,
): Promise {
@@ -108,7 +108,7 @@ class SimpleDBOps {
const result = await getDb()
.update(table)
.set(encryptedData)
- .where(where)
+ .where(where as any)
.returning();
DatabaseSaveTrigger.triggerSave(`update_${tableName}`);
@@ -126,10 +126,12 @@ class SimpleDBOps {
static async delete(
table: SQLiteTable,
tableName: TableName,
- where: any,
- userId: string,
- ): Promise {
- const result = await getDb().delete(table).where(where).returning();
+ where: unknown,
+ ): Promise {
+ const result = await getDb()
+ .delete(table)
+ .where(where as any)
+ .returning();
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
@@ -144,13 +146,10 @@ class SimpleDBOps {
return DataCrypto.getUserDataKey(userId) !== null;
}
- static async selectEncrypted(
- query: any,
- tableName: TableName,
- ): Promise {
+ static async selectEncrypted(query: unknown): Promise {
const results = await query;
- return results;
+ return results as unknown[];
}
}
diff --git a/src/backend/utils/ssh-key-utils.ts b/src/backend/utils/ssh-key-utils.ts
index b19f95c9..8cd3d3d3 100644
--- a/src/backend/utils/ssh-key-utils.ts
+++ b/src/backend/utils/ssh-key-utils.ts
@@ -49,7 +49,7 @@ function detectKeyTypeFromContent(keyContent: string): string {
}
return "ssh-rsa";
- } catch (error) {
+ } catch {
return "ssh-rsa";
}
}
@@ -84,7 +84,7 @@ function detectKeyTypeFromContent(keyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519";
}
- } catch (error) {}
+ } catch {}
if (content.length < 800) {
return "ssh-ed25519";
@@ -140,7 +140,7 @@ function detectPublicKeyTypeFromContent(publicKeyContent: string): string {
} else if (decodedString.includes("1.3.101.112")) {
return "ssh-ed25519";
}
- } catch (error) {}
+ } catch {}
if (content.length < 400) {
return "ssh-ed25519";
@@ -236,13 +236,13 @@ export function parseSSHKey(
} else {
publicKey = "";
}
- } catch (error) {
+ } catch {
publicKey = "";
}
useSSH2 = true;
}
- } catch (error) {}
+ } catch {}
}
if (!useSSH2) {
@@ -268,7 +268,7 @@ export function parseSSHKey(
success: true,
};
}
- } catch (fallbackError) {}
+ } catch {}
return {
privateKey: privateKeyData,
@@ -310,7 +310,7 @@ export function detectKeyType(privateKeyData: string): string {
return "unknown";
}
return parsedKey.type || "unknown";
- } catch (error) {
+ } catch {
return "unknown";
}
}
diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts
index cd805bfc..bd6aa727 100644
--- a/src/backend/utils/system-crypto.ts
+++ b/src/backend/utils/system-crypto.ts
@@ -35,10 +35,33 @@ class SystemCrypto {
if (jwtMatch && jwtMatch[1] && jwtMatch[1].length >= 64) {
this.jwtSecret = jwtMatch[1];
process.env.JWT_SECRET = jwtMatch[1];
+ databaseLogger.success("JWT secret loaded from .env file", {
+ operation: "jwt_init_from_file_success",
+ secretLength: jwtMatch[1].length,
+ secretPrefix: jwtMatch[1].substring(0, 8) + "...",
+ });
return;
+ } else {
+ databaseLogger.warn(
+ "JWT_SECRET in .env file is invalid or too short",
+ {
+ operation: "jwt_init_invalid_secret",
+ hasMatch: !!jwtMatch,
+ secretLength: jwtMatch?.[1]?.length || 0,
+ },
+ );
}
- } catch {}
+ } catch (fileError) {
+ databaseLogger.warn("Failed to read .env file for JWT secret", {
+ operation: "jwt_init_file_read_failed",
+ error:
+ fileError instanceof Error ? fileError.message : "Unknown error",
+ });
+ }
+ databaseLogger.warn("Generating new JWT secret", {
+ operation: "jwt_generating_new_secret",
+ });
await this.generateAndGuideUser();
} catch (error) {
databaseLogger.error("Failed to initialize JWT secret", error, {
diff --git a/src/backend/utils/user-agent-parser.ts b/src/backend/utils/user-agent-parser.ts
new file mode 100644
index 00000000..fb1a3563
--- /dev/null
+++ b/src/backend/utils/user-agent-parser.ts
@@ -0,0 +1,239 @@
+import type { Request } from "express";
+
+export type DeviceType = "web" | "desktop" | "mobile";
+
+export interface DeviceInfo {
+ type: DeviceType;
+ browser: string;
+ version: string;
+ os: string;
+ deviceInfo: string;
+}
+
+export function detectPlatform(req: Request): DeviceType {
+ const userAgent = req.headers["user-agent"] || "";
+ const electronHeader = req.headers["x-electron-app"];
+
+ if (electronHeader === "true" || userAgent.includes("Termix-Desktop")) {
+ return "desktop";
+ }
+
+ if (userAgent.includes("Termix-Mobile")) {
+ return "mobile";
+ }
+
+ if (userAgent.includes("Android")) {
+ return "mobile";
+ }
+
+ return "web";
+}
+
+export function parseUserAgent(req: Request): DeviceInfo {
+ const userAgent = req.headers["user-agent"] || "Unknown";
+ const platform = detectPlatform(req);
+
+ if (platform === "desktop") {
+ return parseElectronUserAgent(userAgent);
+ }
+
+ if (platform === "mobile") {
+ return parseMobileUserAgent(userAgent);
+ }
+
+ return parseWebUserAgent(userAgent);
+}
+
+function parseElectronUserAgent(userAgent: string): DeviceInfo {
+ let os = "Unknown OS";
+ let version = "Unknown";
+
+ const termixMatch = userAgent.match(/Termix-Desktop\/([\d.]+)\s*\(([^;)]+)/);
+ if (termixMatch) {
+ version = termixMatch[1];
+ os = termixMatch[2].trim();
+ } else {
+ if (userAgent.includes("Windows")) {
+ os = parseWindowsVersion(userAgent);
+ } else if (userAgent.includes("Mac OS X")) {
+ os = parseMacVersion(userAgent);
+ } else if (userAgent.includes("macOS")) {
+ os = "macOS";
+ } else if (userAgent.includes("Linux")) {
+ os = "Linux";
+ }
+
+ const electronMatch = userAgent.match(/Electron\/([\d.]+)/);
+ if (electronMatch) {
+ version = electronMatch[1];
+ }
+ }
+
+ return {
+ type: "desktop",
+ browser: "Termix Desktop",
+ version,
+ os,
+ deviceInfo: `Termix Desktop on ${os}`,
+ };
+}
+
+function parseMobileUserAgent(userAgent: string): DeviceInfo {
+ let os = "Unknown OS";
+ let version = "Unknown";
+
+ const termixPlatformMatch = userAgent.match(/Termix-Mobile\/(Android|iOS)/i);
+ if (termixPlatformMatch) {
+ const platform = termixPlatformMatch[1];
+ if (platform.toLowerCase() === "android") {
+ const androidMatch = userAgent.match(/Android ([\d.]+)/);
+ os = androidMatch ? `Android ${androidMatch[1]}` : "Android";
+ } else if (platform.toLowerCase() === "ios") {
+ const iosMatch = userAgent.match(/OS ([\d_]+)/);
+ if (iosMatch) {
+ const iosVersion = iosMatch[1].replace(/_/g, ".");
+ os = `iOS ${iosVersion}`;
+ } else {
+ os = "iOS";
+ }
+ }
+ } else {
+ if (userAgent.includes("Android")) {
+ const androidMatch = userAgent.match(/Android ([\d.]+)/);
+ os = androidMatch ? `Android ${androidMatch[1]}` : "Android";
+ } else if (
+ userAgent.includes("iOS") ||
+ userAgent.includes("iPhone") ||
+ userAgent.includes("iPad")
+ ) {
+ const iosMatch = userAgent.match(/OS ([\d_]+)/);
+ if (iosMatch) {
+ const iosVersion = iosMatch[1].replace(/_/g, ".");
+ os = `iOS ${iosVersion}`;
+ } else {
+ os = "iOS";
+ }
+ }
+ }
+
+ const versionMatch = userAgent.match(
+ /Termix-Mobile\/(?:Android|iOS|)([\d.]+)/i,
+ );
+ if (versionMatch) {
+ version = versionMatch[1];
+ }
+
+ return {
+ type: "mobile",
+ browser: "Termix Mobile",
+ version,
+ os,
+ deviceInfo: `Termix Mobile on ${os}`,
+ };
+}
+
+function parseWebUserAgent(userAgent: string): DeviceInfo {
+ let browser = "Unknown Browser";
+ let version = "Unknown";
+ let os = "Unknown OS";
+
+ if (userAgent.includes("Edg/")) {
+ const match = userAgent.match(/Edg\/([\d.]+)/);
+ browser = "Edge";
+ version = match ? match[1] : "Unknown";
+ } else if (userAgent.includes("Chrome/") && !userAgent.includes("Edg")) {
+ const match = userAgent.match(/Chrome\/([\d.]+)/);
+ browser = "Chrome";
+ version = match ? match[1] : "Unknown";
+ } else if (userAgent.includes("Firefox/")) {
+ const match = userAgent.match(/Firefox\/([\d.]+)/);
+ browser = "Firefox";
+ version = match ? match[1] : "Unknown";
+ } else if (userAgent.includes("Safari/") && !userAgent.includes("Chrome")) {
+ const match = userAgent.match(/Version\/([\d.]+)/);
+ browser = "Safari";
+ version = match ? match[1] : "Unknown";
+ } else if (userAgent.includes("Opera/") || userAgent.includes("OPR/")) {
+ const match = userAgent.match(/(?:Opera|OPR)\/([\d.]+)/);
+ browser = "Opera";
+ version = match ? match[1] : "Unknown";
+ }
+
+ if (userAgent.includes("Windows")) {
+ os = parseWindowsVersion(userAgent);
+ } else if (userAgent.includes("Android")) {
+ const match = userAgent.match(/Android ([\d.]+)/);
+ os = match ? `Android ${match[1]}` : "Android";
+ } else if (
+ userAgent.includes("iOS") ||
+ userAgent.includes("iPhone") ||
+ userAgent.includes("iPad")
+ ) {
+ const match = userAgent.match(/OS ([\d_]+)/);
+ if (match) {
+ const iosVersion = match[1].replace(/_/g, ".");
+ os = `iOS ${iosVersion}`;
+ } else {
+ os = "iOS";
+ }
+ } else if (userAgent.includes("Mac OS X")) {
+ os = parseMacVersion(userAgent);
+ } else if (userAgent.includes("Linux")) {
+ os = "Linux";
+ }
+
+ if (version !== "Unknown") {
+ const versionParts = version.split(".");
+ version = versionParts.slice(0, 2).join(".");
+ }
+
+ return {
+ type: "web",
+ browser,
+ version,
+ os,
+ deviceInfo: `${browser} ${version} on ${os}`,
+ };
+}
+
+function parseWindowsVersion(userAgent: string): string {
+ if (userAgent.includes("Windows NT 10.0")) {
+ return "Windows 10/11";
+ } else if (userAgent.includes("Windows NT 6.3")) {
+ return "Windows 8.1";
+ } else if (userAgent.includes("Windows NT 6.2")) {
+ return "Windows 8";
+ } else if (userAgent.includes("Windows NT 6.1")) {
+ return "Windows 7";
+ } else if (userAgent.includes("Windows NT 6.0")) {
+ return "Windows Vista";
+ } else if (
+ userAgent.includes("Windows NT 5.1") ||
+ userAgent.includes("Windows NT 5.2")
+ ) {
+ return "Windows XP";
+ }
+ return "Windows";
+}
+
+function parseMacVersion(userAgent: string): string {
+ const match = userAgent.match(/Mac OS X ([\d_]+)/);
+ if (match) {
+ const version = match[1].replace(/_/g, ".");
+ const parts = version.split(".");
+ const major = parseInt(parts[0]);
+ const minor = parseInt(parts[1]);
+
+ if (major === 10) {
+ if (minor >= 15) return `macOS ${major}.${minor}`;
+ if (minor === 14) return "macOS Mojave";
+ if (minor === 13) return "macOS High Sierra";
+ if (minor === 12) return "macOS Sierra";
+ } else if (major >= 11) {
+ return `macOS ${major}`;
+ }
+
+ return `macOS ${version}`;
+ }
+ return "macOS";
+}
diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts
index 4bf40338..607066b6 100644
--- a/src/backend/utils/user-crypto.ts
+++ b/src/backend/utils/user-crypto.ts
@@ -163,9 +163,10 @@ class UserCrypto {
async authenticateOIDCUser(userId: string): Promise {
try {
+ const kekSalt = await this.getKEKSalt(userId);
const encryptedDEK = await this.getEncryptedDEK(userId);
- if (!encryptedDEK) {
+ if (!kekSalt || !encryptedDEK) {
await this.setupOIDCUserEncryption(userId);
return true;
}
@@ -195,7 +196,7 @@ class UserCrypto {
DEK.fill(0);
return true;
- } catch (error) {
+ } catch {
await this.setupOIDCUserEncryption(userId);
return true;
}
@@ -276,21 +277,6 @@ class UserCrypto {
oldKEK.fill(0);
newKEK.fill(0);
-
- const dekCopy = Buffer.from(DEK);
-
- const now = Date.now();
- const oldSession = this.userSessions.get(userId);
- if (oldSession) {
- oldSession.dataKey.fill(0);
- }
-
- this.userSessions.set(userId, {
- dataKey: dekCopy,
- lastActivity: now,
- expiresAt: now + UserCrypto.SESSION_DURATION,
- });
-
DEK.fill(0);
return true;
@@ -363,7 +349,7 @@ class UserCrypto {
DEK.fill(0);
return true;
- } catch (error) {
+ } catch {
return false;
}
}
@@ -482,7 +468,7 @@ class UserCrypto {
}
return JSON.parse(result[0].value);
- } catch (error) {
+ } catch {
return null;
}
}
@@ -522,7 +508,7 @@ class UserCrypto {
}
return JSON.parse(result[0].value);
- } catch (error) {
+ } catch {
return null;
}
}
diff --git a/src/backend/utils/user-data-export.ts b/src/backend/utils/user-data-export.ts
index 82d3fde3..03c3fff3 100644
--- a/src/backend/utils/user-data-export.ts
+++ b/src/backend/utils/user-data-export.ts
@@ -18,14 +18,14 @@ interface UserExportData {
userId: string;
username: string;
userData: {
- sshHosts: any[];
- sshCredentials: any[];
+ sshHosts: unknown[];
+ sshCredentials: unknown[];
fileManagerData: {
- recent: any[];
- pinned: any[];
- shortcuts: any[];
+ recent: unknown[];
+ pinned: unknown[];
+ shortcuts: unknown[];
};
- dismissedAlerts: any[];
+ dismissedAlerts: unknown[];
};
metadata: {
totalRecords: number;
@@ -83,7 +83,7 @@ class UserDataExport {
)
: sshHosts;
- let sshCredentialsData: any[] = [];
+ let sshCredentialsData: unknown[] = [];
if (includeCredentials) {
const credentials = await getDb()
.select()
@@ -185,7 +185,10 @@ class UserDataExport {
return JSON.stringify(exportData, null, pretty ? 2 : 0);
}
- static validateExportData(data: any): { valid: boolean; errors: string[] } {
+ static validateExportData(data: unknown): {
+ valid: boolean;
+ errors: string[];
+ } {
const errors: string[] = [];
if (!data || typeof data !== "object") {
@@ -193,23 +196,26 @@ class UserDataExport {
return { valid: false, errors };
}
- if (!data.version) {
+ const dataObj = data as Record;
+
+ if (!dataObj.version) {
errors.push("Missing version field");
}
- if (!data.userId) {
+ if (!dataObj.userId) {
errors.push("Missing userId field");
}
- if (!data.userData || typeof data.userData !== "object") {
+ if (!dataObj.userData || typeof dataObj.userData !== "object") {
errors.push("Missing or invalid userData field");
}
- if (!data.metadata || typeof data.metadata !== "object") {
+ if (!dataObj.metadata || typeof dataObj.metadata !== "object") {
errors.push("Missing or invalid metadata field");
}
- if (data.userData) {
+ if (dataObj.userData) {
+ const userData = dataObj.userData as Record;
const requiredFields = [
"sshHosts",
"sshCredentials",
@@ -218,23 +224,24 @@ class UserDataExport {
];
for (const field of requiredFields) {
if (
- !Array.isArray(data.userData[field]) &&
- !(
- field === "fileManagerData" &&
- typeof data.userData[field] === "object"
- )
+ !Array.isArray(userData[field]) &&
+ !(field === "fileManagerData" && typeof userData[field] === "object")
) {
errors.push(`Missing or invalid userData.${field} field`);
}
}
if (
- data.userData.fileManagerData &&
- typeof data.userData.fileManagerData === "object"
+ userData.fileManagerData &&
+ typeof userData.fileManagerData === "object"
) {
+ const fileManagerData = userData.fileManagerData as Record<
+ string,
+ unknown
+ >;
const fmFields = ["recent", "pinned", "shortcuts"];
for (const field of fmFields) {
- if (!Array.isArray(data.userData.fileManagerData[field])) {
+ if (!Array.isArray(fileManagerData[field])) {
errors.push(
`Missing or invalid userData.fileManagerData.${field} field`,
);
diff --git a/src/backend/utils/user-data-import.ts b/src/backend/utils/user-data-import.ts
index 448d3c00..da776893 100644
--- a/src/backend/utils/user-data-import.ts
+++ b/src/backend/utils/user-data-import.ts
@@ -12,7 +12,6 @@ import { eq, and } from "drizzle-orm";
import { DataCrypto } from "./data-crypto.js";
import { UserDataExport, type UserExportData } from "./user-data-export.js";
import { databaseLogger } from "./logger.js";
-import { nanoid } from "nanoid";
interface ImportOptions {
replaceExisting?: boolean;
@@ -90,7 +89,7 @@ class UserDataImport {
) {
const importStats = await this.importSshHosts(
targetUserId,
- exportData.userData.sshHosts,
+ exportData.userData.sshHosts as Record[],
{ replaceExisting, dryRun, userDataKey },
);
result.summary.sshHostsImported = importStats.imported;
@@ -105,7 +104,7 @@ class UserDataImport {
) {
const importStats = await this.importSshCredentials(
targetUserId,
- exportData.userData.sshCredentials,
+ exportData.userData.sshCredentials as Record[],
{ replaceExisting, dryRun, userDataKey },
);
result.summary.sshCredentialsImported = importStats.imported;
@@ -130,7 +129,7 @@ class UserDataImport {
) {
const importStats = await this.importDismissedAlerts(
targetUserId,
- exportData.userData.dismissedAlerts,
+ exportData.userData.dismissedAlerts as Record[],
{ replaceExisting, dryRun },
);
result.summary.dismissedAlertsImported = importStats.imported;
@@ -160,7 +159,7 @@ class UserDataImport {
private static async importSshHosts(
targetUserId: string,
- sshHosts: any[],
+ sshHosts: Record[],
options: {
replaceExisting: boolean;
dryRun: boolean;
@@ -199,7 +198,9 @@ class UserDataImport {
delete processedHostData.id;
- await getDb().insert(sshData).values(processedHostData);
+ await getDb()
+ .insert(sshData)
+ .values(processedHostData as unknown as typeof sshData.$inferInsert);
imported++;
} catch (error) {
errors.push(
@@ -214,7 +215,7 @@ class UserDataImport {
private static async importSshCredentials(
targetUserId: string,
- credentials: any[],
+ credentials: Record[],
options: {
replaceExisting: boolean;
dryRun: boolean;
@@ -255,7 +256,11 @@ class UserDataImport {
delete processedCredentialData.id;
- await getDb().insert(sshCredentials).values(processedCredentialData);
+ await getDb()
+ .insert(sshCredentials)
+ .values(
+ processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
+ );
imported++;
} catch (error) {
errors.push(
@@ -270,7 +275,7 @@ class UserDataImport {
private static async importFileManagerData(
targetUserId: string,
- fileManagerData: any,
+ fileManagerData: Record,
options: { replaceExisting: boolean; dryRun: boolean },
) {
let imported = 0;
@@ -357,7 +362,7 @@ class UserDataImport {
private static async importDismissedAlerts(
targetUserId: string,
- alerts: any[],
+ alerts: Record[],
options: { replaceExisting: boolean; dryRun: boolean },
) {
let imported = 0;
@@ -377,7 +382,7 @@ class UserDataImport {
.where(
and(
eq(dismissedAlerts.userId, targetUserId),
- eq(dismissedAlerts.alertId, alert.alertId),
+ eq(dismissedAlerts.alertId, alert.alertId as string),
),
);
@@ -396,10 +401,12 @@ class UserDataImport {
if (existing.length > 0 && options.replaceExisting) {
await getDb()
.update(dismissedAlerts)
- .set(newAlert)
+ .set(newAlert as typeof dismissedAlerts.$inferInsert)
.where(eq(dismissedAlerts.id, existing[0].id));
} else {
- await getDb().insert(dismissedAlerts).values(newAlert);
+ await getDb()
+ .insert(dismissedAlerts)
+ .values(newAlert as typeof dismissedAlerts.$inferInsert);
}
imported++;
diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx
index e18440d7..93e2f18c 100644
--- a/src/components/theme-provider.tsx
+++ b/src/components/theme-provider.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
index 46f988c2..b99be47d 100644
--- a/src/components/ui/badge.tsx
+++ b/src/components/ui/badge.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 8b2e9e72..26ee717b 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx
new file mode 100644
index 00000000..c0479b5a
--- /dev/null
+++ b/src/components/ui/chart.tsx
@@ -0,0 +1,24 @@
+import * as React from "react";
+import * as RechartsPrimitive from "recharts";
+
+import { cn } from "@/lib/utils";
+
+// Chart Container
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ return (
+
+ );
+});
+ChartContainer.displayName = "ChartContainer";
+
+export { ChartContainer, RechartsPrimitive };
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx
index 4ebbfe9c..50ee37c3 100644
--- a/src/components/ui/form.tsx
+++ b/src/components/ui/form.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
diff --git a/src/components/ui/password-input.tsx b/src/components/ui/password-input.tsx
index f1cd0066..5eac52b5 100644
--- a/src/components/ui/password-input.tsx
+++ b/src/components/ui/password-input.tsx
@@ -5,8 +5,7 @@ import { Eye, EyeOff } from "lucide-react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
-interface PasswordInputProps
- extends React.InputHTMLAttributes {}
+type PasswordInputProps = React.InputHTMLAttributes;
export const PasswordInput = React.forwardRef<
HTMLInputElement,
diff --git a/src/components/ui/shadcn-io/status/index.tsx b/src/components/ui/shadcn-io/status/index.tsx
index 7139942b..5131e84c 100644
--- a/src/components/ui/shadcn-io/status/index.tsx
+++ b/src/components/ui/shadcn-io/status/index.tsx
@@ -17,10 +17,7 @@ export const Status = ({ className, status, ...props }: StatusProps) => (
export type StatusIndicatorProps = HTMLAttributes;
-export const StatusIndicator = ({
- className,
- ...props
-}: StatusIndicatorProps) => (
+export const StatusIndicator = ({ ...props }: StatusIndicatorProps) => (
- {/* This is what handles the sidebar gap on desktop */}
) {
+ const _values = React.useMemo(
+ () =>
+ Array.isArray(value)
+ ? value
+ : Array.isArray(defaultValue)
+ ? defaultValue
+ : [min, max],
+ [value, defaultValue, min, max],
+ );
+
+ return (
+
+
+
+
+ {Array.from({ length: _values.length }, (_, index) => (
+
+ ))}
+
+ );
+}
+
+export { Slider };
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index 557077ac..04e0013c 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -8,7 +8,10 @@ const Toaster = ({ ...props }: ToasterProps) => {
const originalToast = toast;
- const rateLimitedToast = (message: string, options?: any) => {
+ const rateLimitedToast = (
+ message: string,
+ options?: Record
,
+ ) => {
const now = Date.now();
const lastToast = lastToastRef.current;
@@ -25,13 +28,13 @@ const Toaster = ({ ...props }: ToasterProps) => {
};
Object.assign(toast, {
- success: (message: string, options?: any) =>
+ success: (message: string, options?: Record) =>
rateLimitedToast(message, { ...options, type: "success" }),
- error: (message: string, options?: any) =>
+ error: (message: string, options?: Record) =>
rateLimitedToast(message, { ...options, type: "error" }),
- warning: (message: string, options?: any) =>
+ warning: (message: string, options?: Record) =>
rateLimitedToast(message, { ...options, type: "warning" }),
- info: (message: string, options?: any) =>
+ info: (message: string, options?: Record) =>
rateLimitedToast(message, { ...options, type: "info" }),
message: rateLimitedToast,
});
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
index e306ca0a..6c816b16 100644
--- a/src/components/ui/textarea.tsx
+++ b/src/components/ui/textarea.tsx
@@ -2,8 +2,7 @@ import * as React from "react";
import { cn } from "../../lib/utils";
-export interface TextareaProps
- extends React.TextareaHTMLAttributes {}
+export type TextareaProps = React.TextareaHTMLAttributes;
const Textarea = React.forwardRef(
({ className, ...props }, ref) => {
diff --git a/src/constants/terminal-themes.ts b/src/constants/terminal-themes.ts
new file mode 100644
index 00000000..0786a952
--- /dev/null
+++ b/src/constants/terminal-themes.ts
@@ -0,0 +1,711 @@
+export interface TerminalTheme {
+ name: string;
+ category: "dark" | "light" | "colorful";
+ colors: {
+ background: string;
+ foreground: string;
+ cursor?: string;
+ cursorAccent?: string;
+ selectionBackground?: string;
+ selectionForeground?: string;
+ black: string;
+ red: string;
+ green: string;
+ yellow: string;
+ blue: string;
+ magenta: string;
+ cyan: string;
+ white: string;
+ brightBlack: string;
+ brightRed: string;
+ brightGreen: string;
+ brightYellow: string;
+ brightBlue: string;
+ brightMagenta: string;
+ brightCyan: string;
+ brightWhite: string;
+ };
+}
+
+export const TERMINAL_THEMES: Record = {
+ termix: {
+ name: "Termix Default",
+ category: "dark",
+ colors: {
+ background: "#18181b",
+ foreground: "#f7f7f7",
+ cursor: "#f7f7f7",
+ cursorAccent: "#18181b",
+ selectionBackground: "#3a3a3d",
+ black: "#2e3436",
+ red: "#cc0000",
+ green: "#4e9a06",
+ yellow: "#c4a000",
+ blue: "#3465a4",
+ magenta: "#75507b",
+ cyan: "#06989a",
+ white: "#d3d7cf",
+ brightBlack: "#555753",
+ brightRed: "#ef2929",
+ brightGreen: "#8ae234",
+ brightYellow: "#fce94f",
+ brightBlue: "#729fcf",
+ brightMagenta: "#ad7fa8",
+ brightCyan: "#34e2e2",
+ brightWhite: "#eeeeec",
+ },
+ },
+
+ dracula: {
+ name: "Dracula",
+ category: "dark",
+ colors: {
+ background: "#282a36",
+ foreground: "#f8f8f2",
+ cursor: "#f8f8f2",
+ cursorAccent: "#282a36",
+ selectionBackground: "#44475a",
+ black: "#21222c",
+ red: "#ff5555",
+ green: "#50fa7b",
+ yellow: "#f1fa8c",
+ blue: "#bd93f9",
+ magenta: "#ff79c6",
+ cyan: "#8be9fd",
+ white: "#f8f8f2",
+ brightBlack: "#6272a4",
+ brightRed: "#ff6e6e",
+ brightGreen: "#69ff94",
+ brightYellow: "#ffffa5",
+ brightBlue: "#d6acff",
+ brightMagenta: "#ff92df",
+ brightCyan: "#a4ffff",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ monokai: {
+ name: "Monokai",
+ category: "dark",
+ colors: {
+ background: "#272822",
+ foreground: "#f8f8f2",
+ cursor: "#f8f8f0",
+ cursorAccent: "#272822",
+ selectionBackground: "#49483e",
+ black: "#272822",
+ red: "#f92672",
+ green: "#a6e22e",
+ yellow: "#f4bf75",
+ blue: "#66d9ef",
+ magenta: "#ae81ff",
+ cyan: "#a1efe4",
+ white: "#f8f8f2",
+ brightBlack: "#75715e",
+ brightRed: "#f92672",
+ brightGreen: "#a6e22e",
+ brightYellow: "#f4bf75",
+ brightBlue: "#66d9ef",
+ brightMagenta: "#ae81ff",
+ brightCyan: "#a1efe4",
+ brightWhite: "#f9f8f5",
+ },
+ },
+
+ nord: {
+ name: "Nord",
+ category: "dark",
+ colors: {
+ background: "#2e3440",
+ foreground: "#d8dee9",
+ cursor: "#d8dee9",
+ cursorAccent: "#2e3440",
+ selectionBackground: "#434c5e",
+ black: "#3b4252",
+ red: "#bf616a",
+ green: "#a3be8c",
+ yellow: "#ebcb8b",
+ blue: "#81a1c1",
+ magenta: "#b48ead",
+ cyan: "#88c0d0",
+ white: "#e5e9f0",
+ brightBlack: "#4c566a",
+ brightRed: "#bf616a",
+ brightGreen: "#a3be8c",
+ brightYellow: "#ebcb8b",
+ brightBlue: "#81a1c1",
+ brightMagenta: "#b48ead",
+ brightCyan: "#8fbcbb",
+ brightWhite: "#eceff4",
+ },
+ },
+
+ gruvboxDark: {
+ name: "Gruvbox Dark",
+ category: "dark",
+ colors: {
+ background: "#282828",
+ foreground: "#ebdbb2",
+ cursor: "#ebdbb2",
+ cursorAccent: "#282828",
+ selectionBackground: "#504945",
+ black: "#282828",
+ red: "#cc241d",
+ green: "#98971a",
+ yellow: "#d79921",
+ blue: "#458588",
+ magenta: "#b16286",
+ cyan: "#689d6a",
+ white: "#a89984",
+ brightBlack: "#928374",
+ brightRed: "#fb4934",
+ brightGreen: "#b8bb26",
+ brightYellow: "#fabd2f",
+ brightBlue: "#83a598",
+ brightMagenta: "#d3869b",
+ brightCyan: "#8ec07c",
+ brightWhite: "#ebdbb2",
+ },
+ },
+
+ gruvboxLight: {
+ name: "Gruvbox Light",
+ category: "light",
+ colors: {
+ background: "#fbf1c7",
+ foreground: "#3c3836",
+ cursor: "#3c3836",
+ cursorAccent: "#fbf1c7",
+ selectionBackground: "#d5c4a1",
+ black: "#fbf1c7",
+ red: "#cc241d",
+ green: "#98971a",
+ yellow: "#d79921",
+ blue: "#458588",
+ magenta: "#b16286",
+ cyan: "#689d6a",
+ white: "#7c6f64",
+ brightBlack: "#928374",
+ brightRed: "#9d0006",
+ brightGreen: "#79740e",
+ brightYellow: "#b57614",
+ brightBlue: "#076678",
+ brightMagenta: "#8f3f71",
+ brightCyan: "#427b58",
+ brightWhite: "#3c3836",
+ },
+ },
+
+ solarizedDark: {
+ name: "Solarized Dark",
+ category: "dark",
+ colors: {
+ background: "#002b36",
+ foreground: "#839496",
+ cursor: "#839496",
+ cursorAccent: "#002b36",
+ selectionBackground: "#073642",
+ black: "#073642",
+ red: "#dc322f",
+ green: "#859900",
+ yellow: "#b58900",
+ blue: "#268bd2",
+ magenta: "#d33682",
+ cyan: "#2aa198",
+ white: "#eee8d5",
+ brightBlack: "#002b36",
+ brightRed: "#cb4b16",
+ brightGreen: "#586e75",
+ brightYellow: "#657b83",
+ brightBlue: "#839496",
+ brightMagenta: "#6c71c4",
+ brightCyan: "#93a1a1",
+ brightWhite: "#fdf6e3",
+ },
+ },
+
+ solarizedLight: {
+ name: "Solarized Light",
+ category: "light",
+ colors: {
+ background: "#fdf6e3",
+ foreground: "#657b83",
+ cursor: "#657b83",
+ cursorAccent: "#fdf6e3",
+ selectionBackground: "#eee8d5",
+ black: "#073642",
+ red: "#dc322f",
+ green: "#859900",
+ yellow: "#b58900",
+ blue: "#268bd2",
+ magenta: "#d33682",
+ cyan: "#2aa198",
+ white: "#eee8d5",
+ brightBlack: "#002b36",
+ brightRed: "#cb4b16",
+ brightGreen: "#586e75",
+ brightYellow: "#657b83",
+ brightBlue: "#839496",
+ brightMagenta: "#6c71c4",
+ brightCyan: "#93a1a1",
+ brightWhite: "#fdf6e3",
+ },
+ },
+
+ oneDark: {
+ name: "One Dark",
+ category: "dark",
+ colors: {
+ background: "#282c34",
+ foreground: "#abb2bf",
+ cursor: "#528bff",
+ cursorAccent: "#282c34",
+ selectionBackground: "#3e4451",
+ black: "#282c34",
+ red: "#e06c75",
+ green: "#98c379",
+ yellow: "#e5c07b",
+ blue: "#61afef",
+ magenta: "#c678dd",
+ cyan: "#56b6c2",
+ white: "#abb2bf",
+ brightBlack: "#5c6370",
+ brightRed: "#e06c75",
+ brightGreen: "#98c379",
+ brightYellow: "#e5c07b",
+ brightBlue: "#61afef",
+ brightMagenta: "#c678dd",
+ brightCyan: "#56b6c2",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ tokyoNight: {
+ name: "Tokyo Night",
+ category: "dark",
+ colors: {
+ background: "#1a1b26",
+ foreground: "#a9b1d6",
+ cursor: "#a9b1d6",
+ cursorAccent: "#1a1b26",
+ selectionBackground: "#283457",
+ black: "#15161e",
+ red: "#f7768e",
+ green: "#9ece6a",
+ yellow: "#e0af68",
+ blue: "#7aa2f7",
+ magenta: "#bb9af7",
+ cyan: "#7dcfff",
+ white: "#a9b1d6",
+ brightBlack: "#414868",
+ brightRed: "#f7768e",
+ brightGreen: "#9ece6a",
+ brightYellow: "#e0af68",
+ brightBlue: "#7aa2f7",
+ brightMagenta: "#bb9af7",
+ brightCyan: "#7dcfff",
+ brightWhite: "#c0caf5",
+ },
+ },
+
+ ayuDark: {
+ name: "Ayu Dark",
+ category: "dark",
+ colors: {
+ background: "#0a0e14",
+ foreground: "#b3b1ad",
+ cursor: "#e6b450",
+ cursorAccent: "#0a0e14",
+ selectionBackground: "#253340",
+ black: "#01060e",
+ red: "#ea6c73",
+ green: "#91b362",
+ yellow: "#f9af4f",
+ blue: "#53bdfa",
+ magenta: "#fae994",
+ cyan: "#90e1c6",
+ white: "#c7c7c7",
+ brightBlack: "#686868",
+ brightRed: "#f07178",
+ brightGreen: "#c2d94c",
+ brightYellow: "#ffb454",
+ brightBlue: "#59c2ff",
+ brightMagenta: "#ffee99",
+ brightCyan: "#95e6cb",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ ayuLight: {
+ name: "Ayu Light",
+ category: "light",
+ colors: {
+ background: "#fafafa",
+ foreground: "#5c6166",
+ cursor: "#ff9940",
+ cursorAccent: "#fafafa",
+ selectionBackground: "#d1e4f4",
+ black: "#000000",
+ red: "#f51818",
+ green: "#86b300",
+ yellow: "#f2ae49",
+ blue: "#399ee6",
+ magenta: "#a37acc",
+ cyan: "#4cbf99",
+ white: "#c7c7c7",
+ brightBlack: "#686868",
+ brightRed: "#ff3333",
+ brightGreen: "#b8e532",
+ brightYellow: "#ffc849",
+ brightBlue: "#59c2ff",
+ brightMagenta: "#bf7ce0",
+ brightCyan: "#5cf7a0",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ materialTheme: {
+ name: "Material Theme",
+ category: "dark",
+ colors: {
+ background: "#263238",
+ foreground: "#eeffff",
+ cursor: "#ffcc00",
+ cursorAccent: "#263238",
+ selectionBackground: "#546e7a",
+ black: "#000000",
+ red: "#e53935",
+ green: "#91b859",
+ yellow: "#ffb62c",
+ blue: "#6182b8",
+ magenta: "#7c4dff",
+ cyan: "#39adb5",
+ white: "#ffffff",
+ brightBlack: "#546e7a",
+ brightRed: "#ff5370",
+ brightGreen: "#c3e88d",
+ brightYellow: "#ffcb6b",
+ brightBlue: "#82aaff",
+ brightMagenta: "#c792ea",
+ brightCyan: "#89ddff",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ palenight: {
+ name: "Palenight",
+ category: "dark",
+ colors: {
+ background: "#292d3e",
+ foreground: "#a6accd",
+ cursor: "#ffcc00",
+ cursorAccent: "#292d3e",
+ selectionBackground: "#676e95",
+ black: "#292d3e",
+ red: "#f07178",
+ green: "#c3e88d",
+ yellow: "#ffcb6b",
+ blue: "#82aaff",
+ magenta: "#c792ea",
+ cyan: "#89ddff",
+ white: "#d0d0d0",
+ brightBlack: "#434758",
+ brightRed: "#ff8b92",
+ brightGreen: "#ddffa7",
+ brightYellow: "#ffe585",
+ brightBlue: "#9cc4ff",
+ brightMagenta: "#e1acff",
+ brightCyan: "#a3f7ff",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ oceanicNext: {
+ name: "Oceanic Next",
+ category: "dark",
+ colors: {
+ background: "#1b2b34",
+ foreground: "#cdd3de",
+ cursor: "#c0c5ce",
+ cursorAccent: "#1b2b34",
+ selectionBackground: "#343d46",
+ black: "#343d46",
+ red: "#ec5f67",
+ green: "#99c794",
+ yellow: "#fac863",
+ blue: "#6699cc",
+ magenta: "#c594c5",
+ cyan: "#5fb3b3",
+ white: "#cdd3de",
+ brightBlack: "#65737e",
+ brightRed: "#ec5f67",
+ brightGreen: "#99c794",
+ brightYellow: "#fac863",
+ brightBlue: "#6699cc",
+ brightMagenta: "#c594c5",
+ brightCyan: "#5fb3b3",
+ brightWhite: "#d8dee9",
+ },
+ },
+
+ nightOwl: {
+ name: "Night Owl",
+ category: "dark",
+ colors: {
+ background: "#011627",
+ foreground: "#d6deeb",
+ cursor: "#80a4c2",
+ cursorAccent: "#011627",
+ selectionBackground: "#1d3b53",
+ black: "#011627",
+ red: "#ef5350",
+ green: "#22da6e",
+ yellow: "#c5e478",
+ blue: "#82aaff",
+ magenta: "#c792ea",
+ cyan: "#21c7a8",
+ white: "#ffffff",
+ brightBlack: "#575656",
+ brightRed: "#ef5350",
+ brightGreen: "#22da6e",
+ brightYellow: "#ffeb95",
+ brightBlue: "#82aaff",
+ brightMagenta: "#c792ea",
+ brightCyan: "#7fdbca",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ synthwave84: {
+ name: "Synthwave '84",
+ category: "colorful",
+ colors: {
+ background: "#241b2f",
+ foreground: "#f92aad",
+ cursor: "#f92aad",
+ cursorAccent: "#241b2f",
+ selectionBackground: "#495495",
+ black: "#000000",
+ red: "#f6188f",
+ green: "#1eff8e",
+ yellow: "#ffe261",
+ blue: "#03edf9",
+ magenta: "#f10596",
+ cyan: "#03edf9",
+ white: "#ffffff",
+ brightBlack: "#5a5a5a",
+ brightRed: "#ff1a8e",
+ brightGreen: "#1eff8e",
+ brightYellow: "#ffff00",
+ brightBlue: "#00d8ff",
+ brightMagenta: "#ff00d4",
+ brightCyan: "#00ffff",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ cobalt2: {
+ name: "Cobalt2",
+ category: "dark",
+ colors: {
+ background: "#193549",
+ foreground: "#ffffff",
+ cursor: "#f0cc09",
+ cursorAccent: "#193549",
+ selectionBackground: "#0050a4",
+ black: "#000000",
+ red: "#ff0000",
+ green: "#38de21",
+ yellow: "#ffe50a",
+ blue: "#1460d2",
+ magenta: "#ff005d",
+ cyan: "#00bbbb",
+ white: "#bbbbbb",
+ brightBlack: "#555555",
+ brightRed: "#f40e17",
+ brightGreen: "#3bd01d",
+ brightYellow: "#edc809",
+ brightBlue: "#5555ff",
+ brightMagenta: "#ff55ff",
+ brightCyan: "#6ae3fa",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ snazzy: {
+ name: "Snazzy",
+ category: "dark",
+ colors: {
+ background: "#282a36",
+ foreground: "#eff0eb",
+ cursor: "#97979b",
+ cursorAccent: "#282a36",
+ selectionBackground: "#97979b",
+ black: "#282a36",
+ red: "#ff5c57",
+ green: "#5af78e",
+ yellow: "#f3f99d",
+ blue: "#57c7ff",
+ magenta: "#ff6ac1",
+ cyan: "#9aedfe",
+ white: "#f1f1f0",
+ brightBlack: "#686868",
+ brightRed: "#ff5c57",
+ brightGreen: "#5af78e",
+ brightYellow: "#f3f99d",
+ brightBlue: "#57c7ff",
+ brightMagenta: "#ff6ac1",
+ brightCyan: "#9aedfe",
+ brightWhite: "#eff0eb",
+ },
+ },
+
+ atomOneDark: {
+ name: "Atom One Dark",
+ category: "dark",
+ colors: {
+ background: "#1e2127",
+ foreground: "#abb2bf",
+ cursor: "#528bff",
+ cursorAccent: "#1e2127",
+ selectionBackground: "#3e4451",
+ black: "#000000",
+ red: "#e06c75",
+ green: "#98c379",
+ yellow: "#d19a66",
+ blue: "#61afef",
+ magenta: "#c678dd",
+ cyan: "#56b6c2",
+ white: "#abb2bf",
+ brightBlack: "#5c6370",
+ brightRed: "#e06c75",
+ brightGreen: "#98c379",
+ brightYellow: "#d19a66",
+ brightBlue: "#61afef",
+ brightMagenta: "#c678dd",
+ brightCyan: "#56b6c2",
+ brightWhite: "#ffffff",
+ },
+ },
+
+ catppuccinMocha: {
+ name: "Catppuccin Mocha",
+ category: "dark",
+ colors: {
+ background: "#1e1e2e",
+ foreground: "#cdd6f4",
+ cursor: "#f5e0dc",
+ cursorAccent: "#1e1e2e",
+ selectionBackground: "#585b70",
+ black: "#45475a",
+ red: "#f38ba8",
+ green: "#a6e3a1",
+ yellow: "#f9e2af",
+ blue: "#89b4fa",
+ magenta: "#f5c2e7",
+ cyan: "#94e2d5",
+ white: "#bac2de",
+ brightBlack: "#585b70",
+ brightRed: "#f38ba8",
+ brightGreen: "#a6e3a1",
+ brightYellow: "#f9e2af",
+ brightBlue: "#89b4fa",
+ brightMagenta: "#f5c2e7",
+ brightCyan: "#94e2d5",
+ brightWhite: "#a6adc8",
+ },
+ },
+};
+
+// Font families available for terminal
+export const TERMINAL_FONTS = [
+ {
+ value: "Caskaydia Cove Nerd Font Mono",
+ label: "Caskaydia Cove Nerd Font Mono",
+ fallback:
+ '"Caskaydia Cove Nerd Font Mono", "SF Mono", Consolas, "Liberation Mono", monospace',
+ },
+ {
+ value: "JetBrains Mono",
+ label: "JetBrains Mono",
+ fallback:
+ '"JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", monospace',
+ },
+ {
+ value: "Fira Code",
+ label: "Fira Code",
+ fallback: '"Fira Code", "SF Mono", Consolas, "Liberation Mono", monospace',
+ },
+ {
+ value: "Cascadia Code",
+ label: "Cascadia Code",
+ fallback:
+ '"Cascadia Code", "SF Mono", Consolas, "Liberation Mono", monospace',
+ },
+ {
+ value: "Source Code Pro",
+ label: "Source Code Pro",
+ fallback:
+ '"Source Code Pro", "SF Mono", Consolas, "Liberation Mono", monospace',
+ },
+ {
+ value: "SF Mono",
+ label: "SF Mono",
+ fallback: '"SF Mono", Consolas, "Liberation Mono", monospace',
+ },
+ {
+ value: "Consolas",
+ label: "Consolas",
+ fallback: 'Consolas, "Liberation Mono", monospace',
+ },
+ {
+ value: "Monaco",
+ label: "Monaco",
+ fallback: 'Monaco, "Liberation Mono", monospace',
+ },
+];
+
+export const CURSOR_STYLES = [
+ { value: "block", label: "Block" },
+ { value: "underline", label: "Underline" },
+ { value: "bar", label: "Bar" },
+] as const;
+
+export const BELL_STYLES = [
+ { value: "none", label: "None" },
+ { value: "sound", label: "Sound" },
+ { value: "visual", label: "Visual" },
+ { value: "both", label: "Both" },
+] as const;
+
+export const FAST_SCROLL_MODIFIERS = [
+ { value: "alt", label: "Alt" },
+ { value: "ctrl", label: "Ctrl" },
+ { value: "shift", label: "Shift" },
+] as const;
+
+export const DEFAULT_TERMINAL_CONFIG = {
+ cursorBlink: true,
+ cursorStyle: "bar" as const,
+ fontSize: 14,
+ fontFamily: "Caskaydia Cove Nerd Font Mono",
+ letterSpacing: 0,
+ lineHeight: 1.2,
+ theme: "termix",
+
+ scrollback: 10000,
+ bellStyle: "none" as const,
+ rightClickSelectsWord: false,
+ fastScrollModifier: "alt" as const,
+ fastScrollSensitivity: 5,
+ minimumContrastRatio: 1,
+
+ backspaceMode: "normal" as const,
+ agentForwarding: false,
+ environmentVariables: [] as Array<{ key: string; value: string }>,
+ startupSnippetId: null as number | null,
+ autoMosh: false,
+ moshCommand: "mosh-server new -s -l LANG=en_US.UTF-8",
+};
+
+export type TerminalConfigType = typeof DEFAULT_TERMINAL_CONFIG;
diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts
index cdd414e3..9fc69250 100644
--- a/src/i18n/i18n.ts
+++ b/src/i18n/i18n.ts
@@ -5,12 +5,14 @@ import LanguageDetector from "i18next-browser-languagedetector";
import enTranslation from "../locales/en/translation.json";
import zhTranslation from "../locales/zh/translation.json";
import deTranslation from "../locales/de/translation.json";
+import ptbrTranslation from "../locales/pt-BR/translation.json";
+import ruTranslation from "../locales/ru/translation.json";
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
- supportedLngs: ["en", "zh", "de"],
+ supportedLngs: ["en", "zh", "de", "ptbr", "ru"],
fallbackLng: "en",
debug: false,
@@ -32,6 +34,12 @@ i18n
de: {
translation: deTranslation,
},
+ ptbr: {
+ translation: ptbrTranslation,
+ },
+ ru: {
+ translation: ruTranslation,
+ },
},
interpolation: {
diff --git a/src/lib/frontend-logger.ts b/src/lib/frontend-logger.ts
index b0558de7..2a314430 100644
--- a/src/lib/frontend-logger.ts
+++ b/src/lib/frontend-logger.ts
@@ -17,7 +17,7 @@ export interface LogContext {
errorCode?: string;
errorMessage?: string;
- [key: string]: any;
+ [key: string]: unknown;
}
class FrontendLogger {
@@ -218,7 +218,6 @@ class FrontendLogger {
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
- const shortUrl = this.getShortUrl(cleanUrl);
const statusIcon = this.getStatusIcon(status);
const performanceIcon = this.getPerformanceIcon(responseTime);
@@ -244,7 +243,6 @@ class FrontendLogger {
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
- const shortUrl = this.getShortUrl(cleanUrl);
const statusIcon = this.getStatusIcon(status);
this.error(`← ${statusIcon} ${status} ${errorMessage}`, undefined, {
@@ -265,7 +263,6 @@ class FrontendLogger {
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
- const shortUrl = this.getShortUrl(cleanUrl);
this.error(`🌐 Network Error: ${errorMessage}`, undefined, {
...context,
@@ -279,7 +276,6 @@ class FrontendLogger {
authError(method: string, url: string, context?: LogContext): void {
const cleanUrl = this.sanitizeUrl(url);
- const shortUrl = this.getShortUrl(cleanUrl);
this.security(`🔐 Authentication Required`, {
...context,
@@ -298,7 +294,6 @@ class FrontendLogger {
context?: LogContext,
): void {
const cleanUrl = this.sanitizeUrl(url);
- const shortUrl = this.getShortUrl(cleanUrl);
this.retry(`🔄 Retry ${attempt}/${maxAttempts}`, {
...context,
@@ -384,5 +379,6 @@ export const tunnelLogger = new FrontendLogger("TUNNEL", "📡", "#1e3a8a");
export const fileLogger = new FrontendLogger("FILE", "📁", "#1e3a8a");
export const statsLogger = new FrontendLogger("STATS", "📊", "#22c55e");
export const systemLogger = new FrontendLogger("SYSTEM", "🚀", "#1e3a8a");
+export const dashboardLogger = new FrontendLogger("DASHBOARD", "📊", "#ec4899");
export const logger = systemLogger;
diff --git a/src/locales/de/translation.json b/src/locales/de/translation.json
index 3ab3dd62..c407191b 100644
--- a/src/locales/de/translation.json
+++ b/src/locales/de/translation.json
@@ -311,6 +311,8 @@
"next": "Weiter",
"previous": "Vorherige",
"refresh": "Aktualisieren",
+ "connect": "Verbinden",
+ "connecting": "Verbinde...",
"settings": "Einstellungen",
"profile": "Profil",
"help": "Hilfe",
@@ -318,6 +320,8 @@
"language": "Sprache",
"autoDetect": "Automatische Erkennung",
"changeAccountPassword": "Passwort für Ihr Konto ändern",
+ "passwordResetTitle": "Passwort zurücksetzen",
+ "passwordResetDescription": "Sie sind dabei, Ihr Passwort zurückzusetzen. Dadurch werden Sie von allen aktiven Sitzungen abgemeldet.",
"enterSixDigitCode": "Geben Sie den 6-stelligen Code aus den Docker-Container-Protokollen \/ logs für den Benutzer ein:",
"enterNewPassword": "Geben Sie Ihr neues Passwort für den Benutzer ein:",
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
@@ -529,7 +533,19 @@
"passwordRequired": "Passwort erforderlich",
"confirmExport": "Export bestätigen",
"exportDescription": "SSH-Hosts und Anmeldedaten als SQLite-Datei exportieren",
- "importDescription": "SQLite-Datei mit inkrementellem Zusammenführen importieren (überspringt Duplikate)"
+ "importDescription": "SQLite-Datei mit inkrementellem Zusammenführen importieren (überspringt Duplikate)",
+ "criticalWarning": "Kritische Warnung",
+ "cannotDisablePasswordLoginWithoutOIDC": "Passwort-Login kann nicht ohne konfiguriertes OIDC deaktiviert werden! Sie müssen die OIDC-Authentifizierung konfigurieren, bevor Sie die Passwort-Anmeldung deaktivieren, sonst verlieren Sie den Zugriff auf Termix.",
+ "confirmDisablePasswordLogin": "Sind Sie sicher, dass Sie die Passwort-Anmeldung deaktivieren möchten? Stellen Sie sicher, dass OIDC ordnungsgemäß konfiguriert ist und funktioniert, bevor Sie fortfahren, sonst verlieren Sie den Zugriff auf Ihre Termix-Instanz.",
+ "passwordLoginDisabled": "Passwort-Login erfolgreich deaktiviert",
+ "passwordLoginAndRegistrationDisabled": "Passwort-Login und Registrierung neuer Konten erfolgreich deaktiviert",
+ "requiresPasswordLogin": "Erfordert aktivierte Passwort-Anmeldung",
+ "passwordLoginDisabledWarning": "Passwort-Login ist deaktiviert. Stellen Sie sicher, dass OIDC ordnungsgemäß konfiguriert ist, sonst können Sie sich nicht bei Termix anmelden.",
+ "oidcRequiredWarning": "KRITISCH: Passwort-Login ist deaktiviert. Wenn Sie OIDC zurücksetzen oder falsch konfigurieren, verlieren Sie den gesamten Zugriff auf Termix und Ihre Instanz wird unbrauchbar. Fahren Sie nur fort, wenn Sie absolut sicher sind.",
+ "confirmDisableOIDCWarning": "WARNUNG: Sie sind dabei, OIDC zu deaktivieren, während auch die Passwort-Anmeldung deaktiviert ist. Dies macht Ihre Termix-Instanz unbrauchbar und Sie verlieren den gesamten Zugriff. Sind Sie absolut sicher, dass Sie fortfahren möchten?",
+ "allowPasswordLogin": "Benutzername/Passwort-Anmeldung zulassen",
+ "failedToFetchPasswordLoginStatus": "Abrufen des Passwort-Login-Status fehlgeschlagen",
+ "failedToUpdatePasswordLoginStatus": "Aktualisierung des Passwort-Login-Status fehlgeschlagen"
},
"hosts": {
"title": "Host-Manager",
@@ -623,6 +639,7 @@
"password": "Passwort",
"key": "Schlüssel",
"credential": "Anmeldedaten",
+ "none": "Keine",
"selectCredential": "Anmeldeinformationen auswählen",
"selectCredentialPlaceholder": "Wähle eine Anmeldedaten aus...",
"credentialRequired": "Für die Anmeldeauthentifizierung ist eine Anmeldeinformation erforderlich",
@@ -659,7 +676,34 @@
"folderRenamed": "Ordner „ {{oldName}} “ erfolgreich in „ {{newName}} “ umbenannt",
"failedToRenameFolder": "Ordner konnte nicht umbenannt werden",
"movedToFolder": "Host \"{{name}}\" wurde erfolgreich nach \"{{folder}}\" verschoben",
- "failedToMoveToFolder": "Host konnte nicht in den Ordner verschoben werden"
+ "failedToMoveToFolder": "Host konnte nicht in den Ordner verschoben werden",
+ "statistics": "Statistiken",
+ "enabledWidgets": "Aktivierte Widgets",
+ "enabledWidgetsDesc": "Wählen Sie aus, welche Statistik-Widgets für diesen Host angezeigt werden sollen",
+ "monitoringConfiguration": "Überwachungskonfiguration",
+ "monitoringConfigurationDesc": "Konfigurieren Sie, wie oft Serverstatistiken und Status überprüft werden",
+ "statusCheckEnabled": "Statusüberwachung aktivieren",
+ "statusCheckEnabledDesc": "Prüfen Sie, ob der Server online oder offline ist",
+ "statusCheckInterval": "Statusprüfintervall",
+ "statusCheckIntervalDesc": "Wie oft überprüft werden soll, ob der Host online ist (5s - 1h)",
+ "metricsEnabled": "Metriküberwachung aktivieren",
+ "metricsEnabledDesc": "CPU-, RAM-, Festplatten- und andere Systemstatistiken erfassen",
+ "metricsInterval": "Metriken-Erfassungsintervall",
+ "metricsIntervalDesc": "Wie oft Serverstatistiken erfasst werden sollen (5s - 1h)",
+ "intervalSeconds": "Sekunden",
+ "intervalMinutes": "Minuten",
+ "intervalValidation": "Überwachungsintervalle müssen zwischen 5 Sekunden und 1 Stunde (3600 Sekunden) liegen",
+ "monitoringDisabled": "Die Serverüberwachung ist für diesen Host deaktiviert",
+ "enableMonitoring": "Überwachung aktivieren in Host-Manager → Statistiken-Tab",
+ "monitoringDisabledBadge": "Überwachung Aus",
+ "statusMonitoring": "Status",
+ "metricsMonitoring": "Metriken",
+ "terminalCustomizationNotice": "Hinweis: Terminal-Anpassungen funktionieren nur in der Desktop-Website-Version. Mobile und Electron-Apps verwenden die Standard-Terminaleinstellungen des Systems.",
+ "noneAuthTitle": "Keyboard-Interactive-Authentifizierung",
+ "noneAuthDescription": "Diese Authentifizierungsmethode verwendet beim Herstellen der Verbindung zum SSH-Server die Keyboard-Interactive-Authentifizierung.",
+ "noneAuthDetails": "Keyboard-Interactive-Authentifizierung ermöglicht dem Server, Sie während der Verbindung zur Eingabe von Anmeldeinformationen aufzufordern. Dies ist nützlich für Server, die eine Multi-Faktor-Authentifizierung oder eine dynamische Passworteingabe erfordern.",
+ "forceKeyboardInteractive": "Tastatur-Interaktiv erzwingen",
+ "forceKeyboardInteractiveDesc": "Erzwingt die Verwendung der tastatur-interaktiven Authentifizierung. Dies ist oft für Server erforderlich, die eine Zwei-Faktor-Authentifizierung (TOTP/2FA) verwenden."
},
"terminal": {
"title": "Terminal",
@@ -1132,7 +1176,18 @@
"enterNewPassword": "Geben Sie Ihr neues Passwort für den Benutzer ein:",
"passwordResetSuccess": "Erfolgreich!",
"passwordResetSuccessDesc": "Ihr Passwort wurde erfolgreich zurückgesetzt! Sie können sich jetzt mit Ihrem neuen Passwort anmelden.",
- "signUp": "Registrierung"
+ "signUp": "Registrierung",
+ "dataLossWarning": "Wenn Sie Ihr Passwort auf diese Weise zurücksetzen, werden alle Ihre gespeicherten SSH-Hosts, Anmeldeinformationen und andere verschlüsselte Daten gelöscht. Diese Aktion kann nicht rückgängig gemacht werden. Verwenden Sie diese Option nur, wenn Sie Ihr Passwort vergessen haben und nicht angemeldet sind.",
+ "sshAuthenticationRequired": "SSH-Authentifizierung erforderlich",
+ "sshNoKeyboardInteractive": "Keyboard-Interactive-Authentifizierung nicht verfügbar",
+ "sshAuthenticationFailed": "Authentifizierung fehlgeschlagen",
+ "sshAuthenticationTimeout": "Authentifizierungs-Timeout",
+ "sshNoKeyboardInteractiveDescription": "Der Server unterstützt keine Keyboard-Interactive-Authentifizierung. Bitte geben Sie Ihr Passwort oder Ihren SSH-Schlüssel ein.",
+ "sshAuthFailedDescription": "Die angegebenen Anmeldeinformationen waren falsch. Bitte versuchen Sie es erneut mit gültigen Anmeldeinformationen.",
+ "sshTimeoutDescription": "Der Authentifizierungsversuch ist abgelaufen. Bitte versuchen Sie es erneut.",
+ "sshProvideCredentialsDescription": "Bitte geben Sie Ihre SSH-Anmeldeinformationen ein, um eine Verbindung zu diesem Server herzustellen.",
+ "sshPasswordDescription": "Geben Sie das Passwort für diese SSH-Verbindung ein.",
+ "sshKeyPasswordDescription": "Wenn Ihr SSH-Schlüssel verschlüsselt ist, geben Sie hier die Passphrase ein."
},
"errors": {
"notFound": "Seite nicht gefunden",
@@ -1158,6 +1213,7 @@
"maxLength": "Die maximale Länge beträgt {{max}}",
"invalidEmail": "Ungültige E-Mail-Adresse",
"passwordMismatch": "Passwörter stimmen nicht überein",
+ "passwordLoginDisabled": "Benutzername/Passwort-Anmeldung ist derzeit deaktiviert",
"weakPassword": "Das Passwort ist zu schwach",
"usernameExists": "Benutzername existiert bereits",
"emailExists": "E-Mail existiert bereits",
@@ -1203,7 +1259,10 @@
"authMethod": "Authentifizierungsmethode",
"local": "Lokal",
"external": "Extern (OIDC)",
- "selectPreferredLanguage": "Wählen Sie Ihre bevorzugte Sprache für die Benutzeroberfläche"
+ "selectPreferredLanguage": "Wählen Sie Ihre bevorzugte Sprache für die Benutzeroberfläche",
+ "currentPassword": "Aktuelles Passwort",
+ "passwordChangedSuccess": "Passwort erfolgreich geändert! Bitte melden Sie sich erneut an.",
+ "failedToChangePassword": "Passwort konnte nicht geändert werden. Bitte überprüfen Sie Ihr aktuelles Passwort und versuchen Sie es erneut."
},
"user": {
"failedToLoadVersionInfo": "Fehler beim Laden der Versionsinformationen"
@@ -1261,7 +1320,8 @@
"deleteAccount": "Konto löschen",
"closeDeleteAccount": "Schließen Konto löschen",
"deleteAccountWarning": "Diese Aktion kann nicht rückgängig gemacht werden. Dadurch werden Ihr Konto und alle damit verbundenen Daten dauerhaft gelöscht.",
- "deleteAccountWarningDetails": "Wenn Sie Ihr Konto löschen, werden alle Ihre Daten entfernt, einschließlich SSH-Hosts, Konfigurationen und Einstellungen. Diese Aktion kann nicht rückgängig gemacht werden.",
+ "deleteAccountWarningDetails": "Wenn Sie Ihr Konto löschen, werden alle Ihre Daten entfernt, einschließlich SSH-Hosts, Konfigurationen und Einstellungen. Diese Aktion ist nicht rückgängig zu machen.",
+ "deleteAccountWarningShort": "Diese Aktion kann nicht rückgängig gemacht werden und löscht Ihr Konto dauerhaft.",
"cannotDeleteAccount": "Konto kann nicht gelöscht werden",
"lastAdminWarning": "Sie sind der letzte Administrator. Sie können Ihr Konto nicht löschen, da das System dann ohne Administratoren wäre. Bitte benennen Sie zunächst einen anderen Benutzer als Administrator oder wenden Sie sich an den Systemsupport.",
"confirmPassword": "Passwort bestätigen",
@@ -1381,5 +1441,38 @@
"mobileAppInProgressDesc": "Wir arbeiten an einer speziellen mobilen App, um ein besseres Erlebnis auf Mobilgeräten zu bieten.",
"viewMobileAppDocs": "Mobile App installieren",
"mobileAppDocumentation": "Mobile App-Dokumentation"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "github": "GitHub",
+ "support": "Support",
+ "discord": "Discord",
+ "donate": "Spenden",
+ "serverOverview": "Serverübersicht",
+ "version": "Version",
+ "upToDate": "Auf dem neuesten Stand",
+ "updateAvailable": "Update verfügbar",
+ "uptime": "Betriebszeit",
+ "database": "Datenbank",
+ "healthy": "Gesund",
+ "error": "Fehler",
+ "totalServers": "Server gesamt",
+ "totalTunnels": "Tunnel gesamt",
+ "totalCredentials": "Anmeldedaten gesamt",
+ "recentActivity": "Kürzliche Aktivität",
+ "reset": "Zurücksetzen",
+ "loadingRecentActivity": "Kürzliche Aktivität wird geladen...",
+ "noRecentActivity": "Keine kürzliche Aktivität",
+ "quickActions": "Schnellaktionen",
+ "addHost": "Host hinzufügen",
+ "addCredential": "Anmeldedaten hinzufügen",
+ "adminSettings": "Admin-Einstellungen",
+ "userProfile": "Benutzerprofil",
+ "serverStats": "Serverstatistiken",
+ "loadingServerStats": "Serverstatistiken werden geladen...",
+ "noServerData": "Keine Serverdaten verfügbar",
+ "cpu": "CPU",
+ "ram": "RAM",
+ "notAvailable": "Nicht verfügbar"
}
}
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index e24e7baa..3de6628c 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -54,7 +54,7 @@
"sshPrivateKey": "SSH Private Key",
"upload": "Upload",
"updateKey": "Update Key",
- "keyPassword": "Key Password (optional)",
+ "keyPassword": "Key Password",
"keyType": "Key Type",
"keyTypeRSA": "RSA",
"keyTypeECDSA": "ECDSA",
@@ -191,6 +191,40 @@
"enableRightClickCopyPaste": "Enable right‑click copy/paste",
"shareIdeas": "Have ideas for what should come next for ssh tools? Share them on"
},
+ "snippets": {
+ "title": "Snippets",
+ "new": "New Snippet",
+ "create": "Create Snippet",
+ "edit": "Edit Snippet",
+ "run": "Run",
+ "empty": "No snippets yet",
+ "emptyHint": "Create a snippet to save commonly used commands",
+ "name": "Name",
+ "description": "Description",
+ "content": "Command",
+ "namePlaceholder": "e.g., Restart Nginx",
+ "descriptionPlaceholder": "Optional description",
+ "contentPlaceholder": "e.g., sudo systemctl restart nginx",
+ "nameRequired": "Name is required",
+ "contentRequired": "Command is required",
+ "createDescription": "Create a new command snippet for quick execution",
+ "editDescription": "Edit this command snippet",
+ "deleteConfirmTitle": "Delete Snippet",
+ "deleteConfirmDescription": "Are you sure you want to delete \"{{name}}\"?",
+ "createSuccess": "Snippet created successfully",
+ "updateSuccess": "Snippet updated successfully",
+ "deleteSuccess": "Snippet deleted successfully",
+ "createFailed": "Failed to create snippet",
+ "updateFailed": "Failed to update snippet",
+ "deleteFailed": "Failed to delete snippet",
+ "failedToFetch": "Failed to fetch snippets",
+ "executeSuccess": "Executing: {{name}}",
+ "copySuccess": "Copied \"{{name}}\" to clipboard",
+ "runTooltip": "Execute this snippet in the terminal",
+ "copyTooltip": "Copy snippet to clipboard",
+ "editTooltip": "Edit this snippet",
+ "deleteTooltip": "Delete this snippet"
+ },
"homepage": {
"loggedInTitle": "Logged in!",
"loggedInMessage": "You are logged in! Use the sidebar to access all available tools. To get started, create an SSH Host in the SSH Manager tab. Once created, you can connect to that host using the other apps in the sidebar.",
@@ -213,7 +247,11 @@
"saveError": "Error saving configuration",
"saving": "Saving...",
"saveConfig": "Save Configuration",
- "helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:30001 or https://your-server.com)"
+ "helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:30001 or https://your-server.com)",
+ "warning": "Warning",
+ "notValidatedWarning": "URL not validated - ensure it's correct",
+ "changeServer": "Change Server",
+ "mustIncludeProtocol": "Server URL must start with http:// or https://"
},
"versionCheck": {
"error": "Version Check Error",
@@ -249,6 +287,8 @@
"loading": "Loading",
"required": "Required",
"optional": "Optional",
+ "connect": "Connect",
+ "connecting": "Connecting...",
"clear": "Clear",
"toggleSidebar": "Toggle Sidebar",
"sidebar": "Sidebar",
@@ -333,6 +373,8 @@
"language": "Language",
"autoDetect": "Auto-detect",
"changeAccountPassword": "Change your account password",
+ "passwordResetTitle": "Password Reset",
+ "passwordResetDescription": "You are about to reset your password. This will log you out of all active sessions.",
"enterSixDigitCode": "Enter the 6-digit code from the docker container logs for user:",
"enterNewPassword": "Enter your new password for user:",
"passwordsDoNotMatch": "Passwords do not match",
@@ -357,6 +399,7 @@
"admin": "Admin",
"userProfile": "User Profile",
"tools": "Tools",
+ "snippets": "Snippets",
"newTab": "New Tab",
"splitScreen": "Split Screen",
"closeTab": "Close Tab",
@@ -410,10 +453,12 @@
"general": "General",
"userRegistration": "User Registration",
"allowNewAccountRegistration": "Allow new account registration",
+ "allowPasswordLogin": "Allow username/password login",
"missingRequiredFields": "Missing required fields: {{fields}}",
"oidcConfigurationUpdated": "OIDC configuration updated successfully!",
"failedToFetchOidcConfig": "Failed to fetch OIDC configuration",
"failedToFetchRegistrationStatus": "Failed to fetch registration status",
+ "failedToFetchPasswordLoginStatus": "Failed to fetch password login status",
"failedToFetchUsers": "Failed to fetch users",
"oidcConfigurationDisabled": "OIDC configuration disabled successfully!",
"failedToUpdateOidcConfig": "Failed to update OIDC configuration",
@@ -428,6 +473,13 @@
"userDeletedSuccessfully": "User {{username}} deleted successfully",
"failedToDeleteUser": "Failed to delete user",
"overrideUserInfoUrl": "Override User Info URL (not required)",
+ "failedToFetchSessions": "Failed to fetch sessions",
+ "sessionRevokedSuccessfully": "Session revoked successfully",
+ "failedToRevokeSession": "Failed to revoke session",
+ "confirmRevokeSession": "Are you sure you want to revoke this session?",
+ "confirmRevokeAllSessions": "Are you sure you want to revoke all sessions for this user?",
+ "failedToRevokeSessions": "Failed to revoke sessions",
+ "sessionsRevokedSuccessfully": "Sessions revoked successfully",
"databaseSecurity": "Database Security",
"encryptionStatus": "Encryption Status",
"encryptionEnabled": "Encryption Enabled",
@@ -546,7 +598,16 @@
"passwordRequired": "Password required",
"confirmExport": "Confirm Export",
"exportDescription": "Export SSH hosts and credentials as SQLite file",
- "importDescription": "Import SQLite file with incremental merge (skips duplicates)"
+ "importDescription": "Import SQLite file with incremental merge (skips duplicates)",
+ "criticalWarning": "Critical Warning",
+ "cannotDisablePasswordLoginWithoutOIDC": "Cannot disable password login without OIDC configured! You must configure OIDC authentication before disabling password login, or you will lose access to Termix.",
+ "confirmDisablePasswordLogin": "Are you sure you want to disable password login? Make sure OIDC is properly configured and working before proceeding, or you will lose access to your Termix instance.",
+ "passwordLoginDisabled": "Password login disabled successfully",
+ "passwordLoginAndRegistrationDisabled": "Password login and new account registration disabled successfully",
+ "requiresPasswordLogin": "Requires password login enabled",
+ "passwordLoginDisabledWarning": "Password login is disabled. Ensure OIDC is properly configured or you will not be able to log in to Termix.",
+ "oidcRequiredWarning": "CRITICAL: Password login is disabled. If you reset or misconfigure OIDC, you will lose all access to Termix and brick your instance. Only proceed if you are absolutely certain.",
+ "confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?"
},
"hosts": {
"title": "Host Manager",
@@ -640,6 +701,7 @@
"password": "Password",
"key": "Key",
"credential": "Credential",
+ "none": "None",
"selectCredential": "Select Credential",
"selectCredentialPlaceholder": "Choose a credential...",
"credentialRequired": "Credential is required when using credential authentication",
@@ -669,14 +731,58 @@
"terminal": "Terminal",
"tunnel": "Tunnel",
"fileManager": "File Manager",
+ "serverStats": "Server Stats",
"hostViewer": "Host Viewer",
+ "enableServerStats": "Enable Server Stats",
+ "enableServerStatsDesc": "Enable/disable server statistics collection for this host",
+ "displayItems": "Display Items",
+ "displayItemsDesc": "Choose which metrics to display on the server stats page",
+ "enableCpu": "CPU Usage",
+ "enableMemory": "Memory Usage",
+ "enableDisk": "Disk Usage",
+ "enableNetwork": "Network Statistics (Coming Soon)",
+ "enableProcesses": "Process Count (Coming Soon)",
+ "enableUptime": "Uptime (Coming Soon)",
+ "enableHostname": "Hostname (Coming Soon)",
+ "enableOs": "Operating System (Coming Soon)",
+ "customCommands": "Custom Commands (Coming Soon)",
+ "customCommandsDesc": "Define custom shutdown and reboot commands for this server",
+ "shutdownCommand": "Shutdown Command",
+ "rebootCommand": "Reboot Command",
"confirmRemoveFromFolder": "Are you sure you want to remove \"{{name}}\" from folder \"{{folder}}\"? The host will be moved to \"No Folder\".",
"removedFromFolder": "Host \"{{name}}\" removed from folder successfully",
"failedToRemoveFromFolder": "Failed to remove host from folder",
"folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully",
"failedToRenameFolder": "Failed to rename folder",
"movedToFolder": "Host \"{{name}}\" moved to \"{{folder}}\" successfully",
- "failedToMoveToFolder": "Failed to move host to folder"
+ "failedToMoveToFolder": "Failed to move host to folder",
+ "statistics": "Statistics",
+ "enabledWidgets": "Enabled Widgets",
+ "enabledWidgetsDesc": "Select which statistics widgets to display for this host",
+ "monitoringConfiguration": "Monitoring Configuration",
+ "monitoringConfigurationDesc": "Configure how often server statistics and status are checked",
+ "statusCheckEnabled": "Enable Status Monitoring",
+ "statusCheckEnabledDesc": "Check if the server is online or offline",
+ "statusCheckInterval": "Status Check Interval",
+ "statusCheckIntervalDesc": "How often to check if host is online (5s - 1h)",
+ "metricsEnabled": "Enable Metrics Monitoring",
+ "metricsEnabledDesc": "Collect CPU, RAM, disk, and other system statistics",
+ "metricsInterval": "Metrics Collection Interval",
+ "metricsIntervalDesc": "How often to collect server statistics (5s - 1h)",
+ "intervalSeconds": "seconds",
+ "intervalMinutes": "minutes",
+ "intervalValidation": "Monitoring intervals must be between 5 seconds and 1 hour (3600 seconds)",
+ "monitoringDisabled": "Server monitoring is disabled for this host",
+ "enableMonitoring": "Enable monitoring in Host Manager → Statistics tab",
+ "monitoringDisabledBadge": "Monitoring Off",
+ "statusMonitoring": "Status",
+ "metricsMonitoring": "Metrics",
+ "terminalCustomizationNotice": "Note: Terminal customizations only work on desktop (website and Electron app). Mobile apps and mobile website use system default terminal settings.",
+ "noneAuthTitle": "Keyboard-Interactive Authentication",
+ "noneAuthDescription": "This authentication method will use keyboard-interactive authentication when connecting to the SSH server.",
+ "noneAuthDetails": "Keyboard-interactive authentication allows the server to prompt you for credentials during connection. This is useful for servers that require multi-factor authentication or if you do not want to save credentials locally.",
+ "forceKeyboardInteractive": "Force Keyboard-Interactive",
+ "forceKeyboardInteractiveDesc": "Forces the use of keyboard-interactive authentication. This is often required for servers that use Two-Factor Authentication (TOTP/2FA)."
},
"terminal": {
"title": "Terminal",
@@ -710,7 +816,11 @@
"connectionTimeout": "Connection timeout",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
- "runTitle": "Running {{command}} - {{host}}"
+ "runTitle": "Running {{command}} - {{host}}",
+ "totpRequired": "Two-Factor Authentication Required",
+ "totpCodeLabel": "Verification Code",
+ "totpPlaceholder": "000000",
+ "totpVerify": "Verify"
},
"fileManager": {
"title": "File Manager",
@@ -994,7 +1104,9 @@
"fileComparison": "File Comparison: {{file1}} vs {{file2}}",
"fileTooLarge": "File too large: {{error}}",
"sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})",
- "loadFileFailed": "Failed to load file: {{error}}"
+ "loadFileFailed": "Failed to load file: {{error}}",
+ "connectedSuccessfully": "Connected successfully",
+ "totpVerificationFailed": "TOTP verification failed"
},
"tunnels": {
"title": "SSH Tunnels",
@@ -1083,6 +1195,7 @@
"loadAverageNA": "Avg: N/A",
"cpuUsage": "CPU Usage",
"memoryUsage": "Memory Usage",
+ "diskUsage": "Disk Usage",
"rootStorageSpace": "Root Storage Space",
"of": "of",
"feedbackMessage": "Have ideas for what should come next for server management? Share them on",
@@ -1094,9 +1207,29 @@
"refreshing": "Refreshing...",
"serverOffline": "Server Offline",
"cannotFetchMetrics": "Cannot fetch metrics from offline server",
+ "totpRequired": "TOTP Authentication Required",
+ "totpUnavailable": "Server Stats unavailable for TOTP-enabled servers",
"load": "Load",
"free": "Free",
- "available": "Available"
+ "available": "Available",
+ "editLayout": "Edit Layout",
+ "cancelEdit": "Cancel",
+ "addWidget": "Add Widget",
+ "saveLayout": "Save Layout",
+ "unsavedChanges": "Unsaved changes",
+ "layoutSaved": "Layout saved successfully",
+ "failedToSaveLayout": "Failed to save layout",
+ "systemInfo": "System Information",
+ "hostname": "Hostname",
+ "operatingSystem": "Operating System",
+ "kernel": "Kernel",
+ "totalUptime": "Total Uptime",
+ "seconds": "seconds",
+ "networkInterfaces": "Network Interfaces",
+ "noInterfacesFound": "No network interfaces found",
+ "totalProcesses": "Total Processes",
+ "running": "Running",
+ "noProcessesFound": "No processes found"
},
"auth": {
"loginTitle": "Login to Termix",
@@ -1119,6 +1252,7 @@
"enterCode": "Enter verification code",
"backupCode": "Or use backup code",
"verifyCode": "Verify Code",
+ "redirectingToApp": "Redirecting to app...",
"enableTwoFactor": "Enable Two-Factor Authentication",
"disableTwoFactor": "Disable Two-Factor Authentication",
"scanQRCode": "Scan this QR code with your authenticator app",
@@ -1146,6 +1280,16 @@
"yourBackupCodes": "Your Backup Codes",
"download": "Download",
"setupTwoFactorTitle": "Set Up Two-Factor Authentication",
+ "sshAuthenticationRequired": "SSH Authentication Required",
+ "sshNoKeyboardInteractive": "Keyboard-Interactive Authentication Unavailable",
+ "sshAuthenticationFailed": "Authentication Failed",
+ "sshAuthenticationTimeout": "Authentication Timeout",
+ "sshNoKeyboardInteractiveDescription": "The server does not support keyboard-interactive authentication. Please provide your password or SSH key.",
+ "sshAuthFailedDescription": "The provided credentials were incorrect. Please try again with valid credentials.",
+ "sshTimeoutDescription": "The authentication attempt timed out. Please try again.",
+ "sshProvideCredentialsDescription": "Please provide your SSH credentials to connect to this server.",
+ "sshPasswordDescription": "Enter the password for this SSH connection.",
+ "sshKeyPasswordDescription": "If your SSH key is encrypted, enter the passphrase here.",
"step1ScanQR": "Step 1: Scan the QR code with your authenticator app",
"manualEntryCode": "Manual Entry Code",
"cannotScanQRText": "If you can't scan the QR code, enter this code manually in your authenticator app",
@@ -1178,9 +1322,17 @@
"newPassword": "New Password",
"confirmNewPassword": "Confirm Password",
"enterNewPassword": "Enter your new password for user:",
- "passwordResetSuccess": "Success!",
- "passwordResetSuccessDesc": "Your password has been successfully reset! You can now log in with your new password.",
- "signUp": "Sign Up"
+ "signUp": "Sign Up",
+ "mobileApp": "Mobile App",
+ "loggingInToMobileApp": "Logging in to the mobile app",
+ "desktopApp": "Desktop App",
+ "loggingInToDesktopApp": "Logging in to the desktop app",
+ "loggingInToDesktopAppViaWeb": "Logging in to the desktop app via web interface",
+ "loadingServer": "Loading server...",
+ "authenticating": "Authenticating...",
+ "dataLossWarning": "Resetting your password this way will delete all your saved SSH hosts, credentials, and other encrypted data. This action cannot be undone. Only use this if you have forgotten your password and are not logged in.",
+ "authenticationDisabled": "Authentication Disabled",
+ "authenticationDisabledDesc": "All authentication methods are currently disabled. Please contact your administrator."
},
"errors": {
"notFound": "Page not found",
@@ -1188,7 +1340,7 @@
"forbidden": "Access forbidden",
"serverError": "Server error",
"networkError": "Network error",
- "databaseConnection": "Could not connect to the database.",
+ "databaseConnection": "Could not connect to the database",
"unknownError": "Unknown error",
"loginFailed": "Login failed",
"failedPasswordReset": "Failed to initiate password reset",
@@ -1206,6 +1358,7 @@
"maxLength": "Maximum length is {{max}}",
"invalidEmail": "Invalid email address",
"passwordMismatch": "Passwords do not match",
+ "passwordLoginDisabled": "Username/password login is currently disabled",
"weakPassword": "Password is too weak",
"usernameExists": "Username already exists",
"emailExists": "Email already exists",
@@ -1251,7 +1404,10 @@
"authMethod": "Authentication Method",
"local": "Local",
"external": "External (OIDC)",
- "selectPreferredLanguage": "Select your preferred language for the interface"
+ "selectPreferredLanguage": "Select your preferred language for the interface",
+ "currentPassword": "Current Password",
+ "passwordChangedSuccess": "Password changed successfully! Please log in again.",
+ "failedToChangePassword": "Failed to change password. Please check your current password and try again."
},
"user": {
"failedToLoadVersionInfo": "Failed to load version information"
@@ -1310,6 +1466,7 @@
"closeDeleteAccount": "Close Delete Account",
"deleteAccountWarning": "This action cannot be undone. This will permanently delete your account and all associated data.",
"deleteAccountWarningDetails": "Deleting your account will remove all your data including SSH hosts, configurations, and settings. This action is irreversible.",
+ "deleteAccountWarningShort": "This action is not reversible and will permanently delete your account.",
"cannotDeleteAccount": "Cannot Delete Account",
"lastAdminWarning": "You are the last admin user. You cannot delete your account as this would leave the system without any administrators. Please make another user an admin first, or contact system support.",
"confirmPassword": "Confirm Password",
@@ -1430,5 +1587,38 @@
"mobileAppInProgressDesc": "We're working on a dedicated mobile app to provide a better experience on mobile devices.",
"viewMobileAppDocs": "Install Mobile App",
"mobileAppDocumentation": "Mobile App Documentation"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "github": "GitHub",
+ "support": "Support",
+ "discord": "Discord",
+ "donate": "Donate",
+ "serverOverview": "Server Overview",
+ "version": "Version",
+ "upToDate": "Up to Date",
+ "updateAvailable": "Update Available",
+ "uptime": "Uptime",
+ "database": "Database",
+ "healthy": "Healthy",
+ "error": "Error",
+ "totalServers": "Total Servers",
+ "totalTunnels": "Total Tunnels",
+ "totalCredentials": "Total Credentials",
+ "recentActivity": "Recent Activity",
+ "reset": "Reset",
+ "loadingRecentActivity": "Loading recent activity...",
+ "noRecentActivity": "No recent activity",
+ "quickActions": "Quick Actions",
+ "addHost": "Add Host",
+ "addCredential": "Add Credential",
+ "adminSettings": "Admin Settings",
+ "userProfile": "User Profile",
+ "serverStats": "Server Stats",
+ "loadingServerStats": "Loading server stats...",
+ "noServerData": "No server data available",
+ "cpu": "CPU",
+ "ram": "RAM",
+ "notAvailable": "N/A"
}
}
diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json
new file mode 100644
index 00000000..c656e9cb
--- /dev/null
+++ b/src/locales/pt-BR/translation.json
@@ -0,0 +1,1492 @@
+{
+ "credentials": {
+ "credentialsViewer": "Visualizador de Credenciais",
+ "manageYourSSHCredentials": "Gerencie suas credenciais SSH com segurança",
+ "addCredential": "Adicionar Credencial",
+ "createCredential": "Criar Credencial",
+ "editCredential": "Editar Credencial",
+ "viewCredential": "Ver Credencial",
+ "duplicateCredential": "Duplicar Credencial",
+ "deleteCredential": "Excluir Credencial",
+ "updateCredential": "Atualizar Credencial",
+ "credentialName": "Nome da Credencial",
+ "credentialDescription": "Descrição",
+ "username": "Nome de Usuário",
+ "searchCredentials": "Pesquisar credenciais...",
+ "selectFolder": "Selecionar Pasta",
+ "selectAuthType": "Selecionar Tipo de Autenticação",
+ "allFolders": "Todas as Pastas",
+ "allAuthTypes": "Todos os Tipos de Autenticação",
+ "uncategorized": "Sem categoria",
+ "totalCredentials": "Total",
+ "keyBased": "Baseado em chave",
+ "passwordBased": "Baseado em senha",
+ "folders": "Pastas",
+ "noCredentialsMatchFilters": "Nenhuma credencial corresponde aos seus filtros",
+ "noCredentialsYet": "Nenhuma credencial criada ainda",
+ "createFirstCredential": "Crie sua primeira credencial",
+ "failedToFetchCredentials": "Falha ao buscar credenciais",
+ "credentialDeletedSuccessfully": "Credencial excluída com sucesso",
+ "failedToDeleteCredential": "Falha ao excluir credencial",
+ "confirmDeleteCredential": "Tem certeza que deseja excluir a credencial \"{{name}}\"?",
+ "credentialCreatedSuccessfully": "Credencial criada com sucesso",
+ "credentialUpdatedSuccessfully": "Credencial atualizada com sucesso",
+ "failedToSaveCredential": "Falha ao salvar credencial",
+ "failedToFetchCredentialDetails": "Falha ao buscar detalhes da credencial",
+ "failedToFetchHostsUsing": "Falha ao buscar hosts que usam esta credencial",
+ "loadingCredentials": "Carregando credenciais...",
+ "retry": "Tentar novamente",
+ "noCredentials": "Nenhuma Credencial",
+ "noCredentialsMessage": "Você ainda não adicionou nenhuma credencial. Clique em \"Adicionar Credencial\" para começar.",
+ "sshCredentials": "Credenciais SSH",
+ "credentialsCount": "{{count}} credenciais",
+ "refresh": "Atualizar",
+ "passwordRequired": "Senha é obrigatória",
+ "sshKeyRequired": "Chave SSH é obrigatória",
+ "credentialAddedSuccessfully": "Credencial \"{{name}}\" adicionada com sucesso",
+ "general": "Geral",
+ "description": "Descrição",
+ "folder": "Pasta",
+ "tags": "Tags",
+ "addTagsSpaceToAdd": "Adicionar tags (pressione espaço para adicionar)",
+ "password": "Senha",
+ "key": "Chave",
+ "sshPrivateKey": "Chave Privada SSH",
+ "upload": "Enviar",
+ "updateKey": "Atualizar Chave",
+ "keyPassword": "Senha da Chave (opcional)",
+ "keyType": "Tipo de Chave",
+ "keyTypeRSA": "RSA",
+ "keyTypeECDSA": "ECDSA",
+ "keyTypeEd25519": "Ed25519",
+ "updateCredential": "Atualizar Credencial",
+ "basicInfo": "Informações básicas",
+ "authentication": "Autenticação",
+ "organization": "Organização",
+ "basicInformation": "Informações Básicas",
+ "basicInformationDescription": "Insira as informações básicas para esta credencial",
+ "authenticationMethod": "Método de Autenticação",
+ "authenticationMethodDescription": "Escolha como deseja se autenticar em servidores SSH",
+ "organizationDescription": "Organize suas credenciais com pastas e tags",
+ "enterCredentialName": "Digite o nome da credencial",
+ "enterCredentialDescription": "Digite a descrição (opcional)",
+ "enterUsername": "Digite o nome de usuário",
+ "nameIsRequired": "O nome da credencial é obrigatório",
+ "usernameIsRequired": "Nome de usuário é obrigatório",
+ "authenticationType": "Tipo de Autenticação",
+ "passwordAuthDescription": "Usar autenticação por senha",
+ "sshKeyAuthDescription": "Usar autenticação por chave SSH",
+ "passwordIsRequired": "Senha é obrigatória",
+ "sshKeyIsRequired": "Chave SSH é obrigatória",
+ "sshKeyType": "Tipo de Chave SSH",
+ "privateKey": "Chave Privada",
+ "enterPassword": "Digite a senha",
+ "enterPrivateKey": "Digite a chave privada",
+ "keyPassphrase": "Frase de senha da chave",
+ "enterKeyPassphrase": "Digite a frase de senha da chave (opcional)",
+ "keyPassphraseOptional": "Opcional: deixe vazio se sua chave não tiver frase de senha",
+ "leaveEmptyToKeepCurrent": "Deixe vazio para manter o valor atual",
+ "uploadKeyFile": "Enviar Arquivo de Chave",
+ "generateKeyPairButton": "Gerar Par de Chaves",
+ "generateKeyPair": "Gerar Par de Chaves",
+ "generateKeyPairDescription": "Gerar um novo par de chaves SSH. Se você quiser proteger a chave com uma frase de senha, digite-a no campo Senha da Chave abaixo primeiro.",
+ "deploySSHKey": "Implantar Chave SSH",
+ "deploySSHKeyDescription": "Implantar chave pública no servidor de destino",
+ "sourceCredential": "Credencial de Origem",
+ "targetHost": "Host de Destino",
+ "deploymentProcess": "Processo de Implantação",
+ "deploymentProcessDescription": "Isso adicionará com segurança a chave pública ao arquivo ~/.ssh/authorized_keys do host de destino sem sobrescrever chaves existentes. A operação é reversível.",
+ "chooseHostToDeploy": "Escolha um host para implantar...",
+ "deploying": "Implantando...",
+ "name": "Nome",
+ "noHostsAvailable": "Nenhum host disponível",
+ "noHostsMatchSearch": "Nenhum host corresponde à sua pesquisa",
+ "sshKeyGenerationNotImplemented": "Recurso de geração de chaves SSH em breve",
+ "connectionTestingNotImplemented": "Recurso de teste de conexão em breve",
+ "testConnection": "Testar Conexão",
+ "selectOrCreateFolder": "Selecione ou crie pasta",
+ "noFolder": "Sem pasta",
+ "orCreateNewFolder": "Ou crie nova pasta",
+ "addTag": "Adicionar tag",
+ "saving": "Salvando...",
+ "overview": "Visão geral",
+ "security": "Segurança",
+ "usage": "Uso",
+ "securityDetails": "Detalhes de segurança",
+ "securityDetailsDescription": "Ver informações criptografadas da credencial",
+ "credentialSecured": "Credencial segura",
+ "credentialSecuredDescription": "Todos os dados sensíveis são criptografados com AES-256",
+ "passwordAuthentication": "Autenticação por senha",
+ "keyAuthentication": "Autenticação por chave",
+ "keyType": "Tipo de chave",
+ "securityReminder": "Lembrete de segurança",
+ "securityReminderText": "Nunca compartilhe suas credenciais. Todos os dados são criptografados em repouso.",
+ "hostsUsingCredential": "Hosts usando esta credencial",
+ "noHostsUsingCredential": "Nenhum host está usando esta credencial atualmente",
+ "timesUsed": "Vezes usado",
+ "lastUsed": "Último uso",
+ "connectedHosts": "Hosts conectados",
+ "created": "Criado",
+ "lastModified": "Última modificação",
+ "usageStatistics": "Estatísticas de uso",
+ "copiedToClipboard": "{{field}} copiado para a área de transferência",
+ "failedToCopy": "Falha ao copiar para a área de transferência",
+ "sshKey": "Chave SSH",
+ "createCredentialDescription": "Criar uma nova credencial SSH para acesso seguro",
+ "editCredentialDescription": "Atualizar as informações da credencial",
+ "listView": "Lista",
+ "folderView": "Pastas",
+ "unknownCredential": "Desconhecido",
+ "confirmRemoveFromFolder": "Tem certeza que deseja remover \"{{name}}\" da pasta \"{{folder}}\"? A credencial será movida para \"Sem categoria\".",
+ "removedFromFolder": "Credencial \"{{name}}\" removida da pasta com sucesso",
+ "failedToRemoveFromFolder": "Falha ao remover credencial da pasta",
+ "folderRenamed": "Pasta \"{{oldName}}\" renomeada para \"{{newName}}\" com sucesso",
+ "failedToRenameFolder": "Falha ao renomear pasta",
+ "movedToFolder": "Credencial \"{{name}}\" movida para \"{{folder}}\" com sucesso",
+ "failedToMoveToFolder": "Falha ao mover credencial para pasta",
+ "sshPublicKey": "Chave pública SSH",
+ "publicKeyNote": "A chave pública é opcional, mas recomendada para validação",
+ "publicKeyUploaded": "Chave pública enviada",
+ "uploadPublicKey": "Enviar chave pública",
+ "uploadPrivateKeyFile": "Enviar arquivo de chave privada",
+ "uploadPublicKeyFile": "Enviar arquivo de chave pública",
+ "privateKeyRequiredForGeneration": "A chave privada é necessária para gerar a chave pública",
+ "failedToGeneratePublicKey": "Falha ao gerar chave pública",
+ "generatePublicKey": "Gerar a partir da chave privada",
+ "publicKeyGeneratedSuccessfully": "Chave pública gerada com sucesso",
+ "detectedKeyType": "Tipo de chave detectado",
+ "detectingKeyType": "detectando...",
+ "optional": "Opcional",
+ "generateKeyPairNew": "Gerar novo par de chaves",
+ "generateEd25519": "Gerar Ed25519",
+ "generateECDSA": "Gerar ECDSA",
+ "generateRSA": "Gerar RSA",
+ "keyPairGeneratedSuccessfully": "Par de chaves {{keyType}} gerado com sucesso",
+ "failedToGenerateKeyPair": "Falha ao gerar par de chaves",
+ "generateKeyPairNote": "Gere um novo par de chaves SSH diretamente. Isso substituirá quaisquer chaves existentes no formulário.",
+ "invalidKey": "Chave inválida",
+ "detectionError": "Erro de detecção",
+ "unknown": "Desconhecido"
+ },
+ "dragIndicator": {
+ "error": "Erro: {{error}}",
+ "dragging": "Arrastando {{fileName}}",
+ "preparing": "Preparando {{fileName}}",
+ "readySingle": "Pronto para baixar {{fileName}}",
+ "readyMultiple": "Pronto para baixar {{count}} arquivos",
+ "batchDrag": "Arraste {{count}} arquivos para a área de trabalho",
+ "dragToDesktop": "Arraste para a área de trabalho",
+ "canDragAnywhere": "Você pode arrastar arquivos para qualquer lugar na sua área de trabalho"
+ },
+ "sshTools": {
+ "title": "Ferramentas SSH",
+ "closeTools": "Fechar Ferramentas SSH",
+ "keyRecording": "Gravação de Teclas",
+ "startKeyRecording": "Iniciar Gravação de Teclas",
+ "stopKeyRecording": "Parar Gravação de Teclas",
+ "selectTerminals": "Selecionar terminais:",
+ "typeCommands": "Digite comandos (todas as teclas suportadas):",
+ "commandsWillBeSent": "Os comandos serão enviados para {{count}} terminal(is) selecionado(s).",
+ "settings": "Configurações",
+ "enableRightClickCopyPaste": "Habilitar copiar/colar com botão direito",
+ "shareIdeas": "Tem ideias sobre o que deve vir a seguir nas ferramentas SSH? Compartilhe em"
+ },
+ "homepage": {
+ "loggedInTitle": "Conectado!",
+ "loggedInMessage": "Você está conectado! Use a barra lateral para acessar todas as ferramentas disponíveis. Para começar, crie um Host SSH na aba Gerenciador SSH. Depois de criado, você pode se conectar a esse host usando os outros apps na barra lateral.",
+ "failedToLoadAlerts": "Falha ao carregar alertas",
+ "failedToDismissAlert": "Falha ao dispensar alerta"
+ },
+ "serverConfig": {
+ "title": "Configuração do Servidor",
+ "description": "Configure a URL do servidor Termix para conectar aos serviços de backend",
+ "serverUrl": "URL do Servidor",
+ "enterServerUrl": "Por favor, insira uma URL do servidor",
+ "testConnectionFirst": "Por favor, teste a conexão primeiro",
+ "connectionSuccess": "Conexão bem-sucedida!",
+ "connectionFailed": "Conexão falhou",
+ "connectionError": "Ocorreu um erro de conexão",
+ "connected": "Conectado",
+ "disconnected": "Desconectado",
+ "configSaved": "Configuração salva com sucesso",
+ "saveFailed": "Falha ao salvar configuração",
+ "saveError": "Erro ao salvar configuração",
+ "saving": "Salvando...",
+ "saveConfig": "Salvar Configuração",
+ "helpText": "Digite a URL onde seu servidor Termix está rodando (ex.: http://localhost:30001 ou https://seu-servidor.com)"
+ },
+ "versionCheck": {
+ "error": "Erro na verificação de versão",
+ "checkFailed": "Falha ao verificar atualizações",
+ "upToDate": "Aplicativo atualizado",
+ "currentVersion": "Você está usando a versão {{version}}",
+ "updateAvailable": "Atualização disponível",
+ "newVersionAvailable": "Uma nova versão está disponível! Você está usando {{current}}, mas {{latest}} está disponível.",
+ "releasedOn": "Lançada em {{date}}",
+ "downloadUpdate": "Baixar Atualização",
+ "dismiss": "Fechar",
+ "checking": "Verificando atualizações...",
+ "checkUpdates": "Verificar Atualizações",
+ "checkingUpdates": "Verificando atualizações...",
+ "refresh": "Atualizar",
+ "updateRequired": "Atualização necessária",
+ "updateDismissed": "Notificação de atualização dispensada",
+ "noUpdatesFound": "Nenhuma atualização encontrada"
+ },
+ "common": {
+ "close": "Fechar",
+ "minimize": "Minimizar",
+ "online": "Online",
+ "offline": "Offline",
+ "continue": "Continuar",
+ "maintenance": "Manutenção",
+ "degraded": "Degradado",
+ "discord": "Discord",
+ "error": "Erro",
+ "warning": "Aviso",
+ "info": "Info",
+ "success": "Sucesso",
+ "loading": "Carregando",
+ "required": "Obrigatório",
+ "optional": "Opcional",
+ "clear": "Limpar",
+ "toggleSidebar": "Alternar Barra Lateral",
+ "sidebar": "Barra Lateral",
+ "home": "Início",
+ "expired": "Expirado",
+ "expiresToday": "Expira hoje",
+ "expiresTomorrow": "Expira amanhã",
+ "expiresInDays": "Expira em {{days}} dias",
+ "updateAvailable": "Atualização Disponível",
+ "sshPath": "Caminho SSH",
+ "localPath": "Caminho Local",
+ "loading": "Carregando...",
+ "noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH",
+ "noReleases": "Sem Versões",
+ "updatesAndReleases": "Atualizações e Versões",
+ "newVersionAvailable": "Uma nova versão ({{version}}) está disponível.",
+ "failedToFetchUpdateInfo": "Falha ao buscar informações de atualização",
+ "preRelease": "Pré-lançamento",
+ "loginFailed": "Falha no login",
+ "noReleasesFound": "Nenhuma versão encontrada.",
+ "yourBackupCodes": "Seus Códigos de Backup",
+ "sendResetCode": "Enviar Código de Redefinição",
+ "verifyCode": "Verificar Código",
+ "resetPassword": "Redefinir Senha",
+ "resetCode": "Código de Redefinição",
+ "newPassword": "Nova Senha",
+ "sshPath": "Caminho SSH",
+ "localPath": "Caminho Local",
+ "folder": "Pasta",
+ "file": "Arquivo",
+ "renamedSuccessfully": "renomeado com sucesso",
+ "deletedSuccessfully": "excluído com sucesso",
+ "noAuthCredentials": "Não há credenciais de autenticação disponíveis para este host SSH",
+ "noTunnelConnections": "Não há conexões de túnel configuradas",
+ "sshTools": "Ferramentas SSH",
+ "english": "Inglês",
+ "chinese": "Chinês",
+ "german": "Alemão",
+ "cancel": "Cancelar",
+ "username": "Usuário",
+ "name": "Nome",
+ "login": "Entrar",
+ "logout": "Sair",
+ "register": "Registrar",
+ "username": "Usuário",
+ "password": "Senha",
+ "version": "Versão",
+ "confirmPassword": "Confirmar Senha",
+ "back": "Voltar",
+ "email": "Email",
+ "submit": "Enviar",
+ "cancel": "Cancelar",
+ "change": "Alterar",
+ "save": "Salvar",
+ "delete": "Excluir",
+ "edit": "Editar",
+ "add": "Adicionar",
+ "search": "Buscar",
+ "loading": "Carregando...",
+ "error": "Erro",
+ "success": "Sucesso",
+ "warning": "Aviso",
+ "info": "Info",
+ "confirm": "Confirmar",
+ "yes": "Sim",
+ "no": "Não",
+ "ok": "OK",
+ "close": "Fechar",
+ "enabled": "Habilitado",
+ "disabled": "Desabilitado",
+ "important": "Importante",
+ "notEnabled": "Não Habilitado",
+ "settingUp": "Configurando...",
+ "back": "Voltar",
+ "next": "Próximo",
+ "previous": "Anterior",
+ "refresh": "Atualizar",
+ "connect": "Conectar",
+ "connecting": "Conectando...",
+ "settings": "Configurações",
+ "profile": "Perfil",
+ "help": "Ajuda",
+ "about": "Sobre",
+ "language": "Idioma",
+ "autoDetect": "Detecção Automática",
+ "changeAccountPassword": "Alterar senha da conta",
+ "passwordResetTitle": "Redefinir Senha",
+ "passwordResetDescription": "Você está prestes a redefinir sua senha. Isso fará com que você seja desconectado de todas as sessões ativas.",
+ "enterSixDigitCode": "Digite o código de 6 dígitos dos logs do container docker para o usuário:",
+ "enterNewPassword": "Digite sua nova senha para o usuário:",
+ "passwordsDoNotMatch": "As senhas não correspondem",
+ "passwordMinLength": "A senha deve ter pelo menos 6 caracteres",
+ "passwordResetSuccess": "Senha redefinida com sucesso! Você pode agora entrar com sua nova senha.",
+ "failedToInitiatePasswordReset": "Falha ao iniciar redefinição de senha",
+ "failedToVerifyResetCode": "Falha ao verificar código de redefinição",
+ "failedToCompletePasswordReset": "Falha ao completar redefinição de senha",
+ "documentation": "Documentação",
+ "retry": "Tentar Novamente",
+ "checking": "Verificando...",
+ "checkingDatabase": "Verificando conexão com o banco de dados..."
+ },
+ "nav": {
+ "home": "Início",
+ "hosts": "Hosts",
+ "credentials": "Credenciais",
+ "terminal": "Terminal",
+ "tunnels": "Túneis",
+ "fileManager": "Gerenciador de Arquivos",
+ "serverStats": "Estatísticas do Servidor",
+ "admin": "Admin",
+ "userProfile": "Perfil do Usuário",
+ "tools": "Ferramentas",
+ "newTab": "Nova Aba",
+ "splitScreen": "Dividir Tela",
+ "closeTab": "Fechar Aba",
+ "sshManager": "Gerenciador SSH",
+ "hostManager": "Gerenciador de Hosts",
+ "cannotSplitTab": "Não é possível dividir esta aba",
+ "tabNavigation": "Navegação de Abas"
+ },
+ "admin": {
+ "title": "Configurações de Admin",
+ "oidc": "OIDC",
+ "users": "Usuários",
+ "userManagement": "Gerenciamento de Usuários",
+ "makeAdmin": "Tornar Admin",
+ "removeAdmin": "Remover Admin",
+ "deleteUser": "Excluir Usuário",
+ "allowRegistration": "Permitir Registro",
+ "oidcSettings": "Configurações OIDC",
+ "clientId": "ID do Cliente",
+ "clientSecret": "Segredo do Cliente",
+ "issuerUrl": "URL do Emissor",
+ "authorizationUrl": "URL de Autorização",
+ "tokenUrl": "URL do Token",
+ "updateSettings": "Atualizar Configurações",
+ "confirmDelete": "Tem certeza que deseja excluir este usuário?",
+ "confirmMakeAdmin": "Tem certeza que deseja tornar este usuário um admin?",
+ "confirmRemoveAdmin": "Tem certeza que deseja remover os privilégios de admin deste usuário?",
+ "externalAuthentication": "Autenticação Externa (OIDC)",
+ "configureExternalProvider": "Configure o provedor de identidade externo para autenticação OIDC/OAuth2.",
+ "userIdentifierPath": "Caminho do Identificador do Usuário",
+ "displayNamePath": "Caminho do Nome de Exibição",
+ "scopes": "Escopos",
+ "saving": "Salvando...",
+ "saveConfiguration": "Salvar Configuração",
+ "reset": "Redefinir",
+ "success": "Sucesso",
+ "loading": "Carregando...",
+ "refresh": "Atualizar",
+ "loadingUsers": "Carregando usuários...",
+ "username": "Usuário",
+ "type": "Tipo",
+ "actions": "Ações",
+ "external": "Externo",
+ "local": "Local",
+ "adminManagement": "Gerenciamento de Admin",
+ "makeUserAdmin": "Tornar Usuário Admin",
+ "adding": "Adicionando...",
+ "currentAdmins": "Admins Atuais",
+ "adminBadge": "Admin",
+ "removeAdminButton": "Remover Admin",
+ "general": "Geral",
+ "userRegistration": "Registro de Usuário",
+ "allowNewAccountRegistration": "Permitir registro de novas contas",
+ "missingRequiredFields": "Campos obrigatórios faltando: {{fields}}",
+ "oidcConfigurationUpdated": "Configuração OIDC atualizada com sucesso!",
+ "failedToFetchOidcConfig": "Falha ao buscar configuração OIDC",
+ "failedToFetchRegistrationStatus": "Falha ao buscar status do registro",
+ "failedToFetchUsers": "Falha ao buscar usuários",
+ "oidcConfigurationDisabled": "Configuração OIDC desativada com sucesso!",
+ "failedToUpdateOidcConfig": "Falha ao atualizar configuração OIDC",
+ "failedToDisableOidcConfig": "Falha ao desativar configuração OIDC",
+ "enterUsernameToMakeAdmin": "Insira o nome de usuário para tornar admin",
+ "userIsNowAdmin": "O usuário {{username}} agora é um administrador",
+ "failedToMakeUserAdmin": "Falha ao tornar o usuário administrador",
+ "removeAdminStatus": "Remover status de administrador de {{username}}?",
+ "adminStatusRemoved": "Status de administrador removido de {{username}}",
+ "failedToRemoveAdminStatus": "Falha ao remover o status de administrador",
+ "confirmDeleteUser": "Excluir usuário {{username}}? Esta ação não pode ser desfeita.",
+ "userDeletedSuccessfully": "Usuário {{username}} excluído com sucesso",
+ "failedToDeleteUser": "Falha ao excluir usuário",
+ "overrideUserInfoUrl": "Sobrescrever URL de informações do usuário (não obrigatório)",
+ "databaseSecurity": "Segurança do Banco de Dados",
+ "encryptionStatus": "Status da Criptografia",
+ "encryptionEnabled": "Criptografia Ativada",
+ "enabled": "Ativado",
+ "disabled": "Desativado",
+ "keyId": "ID da Chave",
+ "created": "Criado",
+ "migrationStatus": "Status da Migração",
+ "migrationCompleted": "Migração concluída",
+ "migrationRequired": "Migração necessária",
+ "deviceProtectedMasterKey": "Chave Mestra Protegida pelo Ambiente",
+ "legacyKeyStorage": "Armazenamento de Chave Legado",
+ "masterKeyEncryptedWithDeviceFingerprint": "Chave mestra criptografada com impressão digital do ambiente (proteção KEK ativa)",
+ "keyNotProtectedByDeviceBinding": "Chave não protegida pela vinculação ao ambiente (atualização recomendada)",
+ "valid": "Válido",
+ "initializeDatabaseEncryption": "Initialize Database Encryption",
+ "enableAes256EncryptionWithDeviceBinding": "Ativar criptografia AES-256 com proteção de chave mestra vinculada ao ambiente. Isso cria segurança de nível empresarial para chaves SSH, senhas e tokens de autenticação.",
+ "featuresEnabled": "Recursos ativados:",
+ "aes256GcmAuthenticatedEncryption": "Criptografia autenticada AES-256-GCM",
+ "deviceFingerprintMasterKeyProtection": "Proteção de chave mestra por impressão digital do ambiente (KEK)",
+ "pbkdf2KeyDerivation": "Derivação de chave PBKDF2 com 100K iterações",
+ "automaticKeyManagement": "Gerenciamento e rotação automática de chaves",
+ "initializing": "Inicializando...",
+ "initializeEnterpriseEncryption": "Inicializar Criptografia Empresarial",
+ "migrateExistingData": "Migrar Dados Existentes",
+ "encryptExistingUnprotectedData": "Criptografar dados não protegidos existentes no seu banco de dados. Este processo é seguro e cria backups automáticos.",
+ "testMigrationDryRun": "Verificar Compatibilidade de Criptografia",
+ "migrating": "Migrando...",
+ "migrateData": "Migrar Dados",
+ "securityInformation": "Informações de Segurança",
+ "sshPrivateKeysEncryptedWithAes256": "Chaves privadas SSH e senhas são criptografadas com AES-256-GCM",
+ "userAuthTokensProtected": "Tokens de autenticação do usuário e segredos 2FA são protegidos",
+ "masterKeysProtectedByDeviceFingerprint": "Chaves mestras de criptografia são protegidas pela impressão digital do dispositivo (KEK)",
+ "keysBoundToServerInstance": "Chaves estão vinculadas ao ambiente do servidor atual (migráveis via variáveis de ambiente)",
+ "pbkdf2HkdfKeyDerivation": "Derivação de chave PBKDF2 + HKDF com 100K iterações",
+ "backwardCompatibleMigration": "Todos os dados permanecem compatíveis durante a migração",
+ "enterpriseGradeSecurityActive": "Segurança de Nível Empresarial Ativa",
+ "masterKeysProtectedByDeviceBinding": "Suas chaves mestras de criptografia são protegidas pela impressão digital do ambiente. Isso usa o nome do host do servidor, caminhos e outras informações do ambiente para gerar chaves de proteção. Para migrar servidores, defina a variável de ambiente DB_ENCRYPTION_KEY no novo servidor.",
+ "important": "Importante",
+ "keepEncryptionKeysSecure": "Garanta a segurança dos dados: faça backup regularmente dos arquivos do banco de dados e da configuração do servidor. Para migrar para um novo servidor, defina a variável de ambiente DB_ENCRYPTION_KEY no novo ambiente, ou mantenha a mesma estrutura de nome de host e diretório.",
+ "loadingEncryptionStatus": "Carregando status da criptografia...",
+ "testMigrationDescription": "Verifique se os dados existentes podem ser migrados com segurança para o formato criptografado sem realmente modificar nenhum dado",
+ "serverMigrationGuide": "Guia de Migração do Servidor",
+ "migrationInstructions": "Para migrar dados criptografados para um novo servidor: 1) Faça backup dos arquivos do banco de dados, 2) Defina a variável de ambiente DB_ENCRYPTION_KEY=\"sua-chave\" no novo servidor, 3) Restaure os arquivos do banco de dados",
+ "environmentProtection": "Proteção do Ambiente",
+ "environmentProtectionDesc": "Protege as chaves de criptografia com base nas informações do ambiente do servidor (nome do host, caminhos, etc.), migráveis via variáveis de ambiente",
+ "verificationCompleted": "Verificação de compatibilidade concluída - nenhum dado foi alterado",
+ "verificationInProgress": "Verificação concluída",
+ "dataMigrationCompleted": "Migração de dados concluída com sucesso!",
+ "migrationCompleted": "Migração concluída",
+ "verificationFailed": "Falha na verificação de compatibilidade",
+ "migrationFailed": "Falha na migração",
+ "runningVerification": "Executando verificação de compatibilidade...",
+ "startingMigration": "Iniciando migração...",
+ "hardwareFingerprintSecurity": "Segurança por Impressão Digital de Hardware",
+ "hardwareBoundEncryption": "Criptografia Vinculada ao Hardware Ativa",
+ "masterKeysNowProtectedByHardwareFingerprint": "As chaves mestras agora são protegidas por impressão digital real do hardware em vez de variáveis de ambiente",
+ "cpuSerialNumberDetection": "Detecção do número de série da CPU",
+ "motherboardUuidIdentification": "Identificação UUID da placa-mãe",
+ "diskSerialNumberVerification": "Verificação do número de série do disco",
+ "biosSerialNumberCheck": "Verificação do número de série da BIOS",
+ "stableMacAddressFiltering": "Filtragem de endereço MAC estável",
+ "databaseFileEncryption": "Criptografia de Arquivo do Banco de Dados",
+ "dualLayerProtection": "Proteção em Duas Camadas Ativa",
+ "bothFieldAndFileEncryptionActive": "Tanto a criptografia em nível de campo quanto em nível de arquivo estão ativas para máxima segurança",
+ "fieldLevelAes256Encryption": "Criptografia AES-256 em nível de campo para dados sensíveis",
+ "fileLevelDatabaseEncryption": "Criptografia do banco de dados em nível de arquivo com vinculação ao hardware",
+ "hardwareBoundFileKeys": "Chaves de criptografia de arquivo vinculadas ao hardware",
+ "automaticEncryptedBackups": "Criação automática de backup criptografado",
+ "createEncryptedBackup": "Criar Backup Criptografado",
+ "creatingBackup": "Criando Backup...",
+ "backupCreated": "Backup Criado",
+ "encryptedBackupCreatedSuccessfully": "Backup criptografado criado com sucesso",
+ "backupCreationFailed": "Falha na criação do backup",
+ "databaseMigration": "Migração do Banco de Dados",
+ "exportForMigration": "Exportar para Migração",
+ "exportDatabaseForHardwareMigration": "Exportar banco de dados como arquivo SQLite com dados descriptografados para migração para novo hardware",
+ "exportDatabase": "Exportar Banco de Dados SQLite",
+ "exporting": "Exportando...",
+ "exportCreated": "Exportação SQLite Criada",
+ "exportContainsDecryptedData": "A exportação SQLite contém dados descriptografados - mantenha seguro!",
+ "databaseExportedSuccessfully": "Banco de dados SQLite exportado com sucesso",
+ "databaseExportFailed": "Falha na exportação do banco de dados SQLite",
+ "importFromMigration": "Importar da Migração",
+ "importDatabaseFromAnotherSystem": "Importar banco de dados SQLite de outro sistema ou hardware",
+ "importDatabase": "Importar Banco de Dados SQLite",
+ "importing": "Importando...",
+ "selectedFile": "Arquivo SQLite Selecionado",
+ "importWillReplaceExistingData": "A importação SQLite substituirá os dados existentes - backup recomendado!",
+ "pleaseSelectImportFile": "Por favor, selecione um arquivo SQLite para importar",
+ "databaseImportedSuccessfully": "Banco de dados SQLite importado com sucesso",
+ "databaseImportFailed": "Falha na importação do banco de dados SQLite",
+ "manageEncryptionAndBackups": "Gerenciar chaves de criptografia, segurança do banco de dados e operações de backup",
+ "activeSecurityFeatures": "Medidas e proteções de segurança atualmente ativas",
+ "deviceBindingTechnology": "Tecnologia avançada de proteção de chave baseada em hardware",
+ "backupAndRecovery": "Opções seguras de criação de backup e recuperação de banco de dados",
+ "crossSystemDataTransfer": "Exportar e importar bancos de dados entre diferentes sistemas",
+ "noMigrationNeeded": "Nenhuma migração necessária",
+ "encryptionKey": "Chave de Criptografia",
+ "keyProtection": "Proteção da Chave",
+ "active": "Ativo",
+ "legacy": "Legado",
+ "dataStatus": "Status dos Dados",
+ "encrypted": "Criptografado",
+ "needsMigration": "Necessita Migração",
+ "ready": "Pronto",
+ "initializeEncryption": "Inicializar Criptografia",
+ "initialize": "Inicializar",
+ "test": "Testar",
+ "migrate": "Migrar",
+ "backup": "Backup",
+ "createBackup": "Criar Backup",
+ "exportImport": "Exportar/Importar",
+ "export": "Exportar",
+ "import": "Importar",
+ "passwordRequired": "Senha necessária",
+ "confirmExport": "Confirmar Exportação",
+ "exportDescription": "Exportar hosts SSH e credenciais como arquivo SQLite",
+ "importDescription": "Importar arquivo SQLite com mesclagem incremental (ignora duplicados)",
+ "criticalWarning": "Aviso Crítico",
+ "cannotDisablePasswordLoginWithoutOIDC": "Não é possível desativar o login por senha sem OIDC configurado! Você deve configurar a autenticação OIDC antes de desativar o login por senha, ou perderá o acesso ao Termix.",
+ "confirmDisablePasswordLogin": "Tem certeza que deseja desativar o login por senha? Certifique-se de que o OIDC está configurado corretamente e funcionando antes de continuar, ou você perderá o acesso à sua instância do Termix.",
+ "passwordLoginDisabled": "Login por senha desativado com sucesso",
+ "passwordLoginAndRegistrationDisabled": "Login por senha e registro de novas contas desativados com sucesso",
+ "requiresPasswordLogin": "Requer login por senha ativado",
+ "passwordLoginDisabledWarning": "Login por senha está desativado. Certifique-se de que o OIDC está configurado corretamente ou você não conseguirá fazer login no Termix.",
+ "oidcRequiredWarning": "CRÍTICO: Login por senha está desativado. Se você redefinir ou configurar incorretamente o OIDC, você perderá todo o acesso ao Termix e inutilizará sua instância. Prossiga apenas se tiver absoluta certeza.",
+ "confirmDisableOIDCWarning": "AVISO: Você está prestes a desativar o OIDC enquanto o login por senha também está desativado. Isso inutilizará sua instância do Termix e você perderá todo o acesso. Tem absoluta certeza de que deseja continuar?",
+ "allowPasswordLogin": "Permitir login com nome de usuário/senha",
+ "failedToFetchPasswordLoginStatus": "Falha ao buscar status do login por senha",
+ "failedToUpdatePasswordLoginStatus": "Falha ao atualizar status do login por senha"
+ },
+ "hosts": {
+ "title": "Gerenciador de Hosts",
+ "sshHosts": "Hosts SSH",
+ "noHosts": "Sem Hosts SSH",
+ "noHostsMessage": "Você ainda não adicionou nenhum host SSH. Clique em \"Adicionar Host\" para começar.",
+ "loadingHosts": "Carregando hosts...",
+ "failedToLoadHosts": "Falha ao carregar hosts",
+ "retry": "Tentar Novamente",
+ "refresh": "Atualizar",
+ "hostsCount": "{{count}} hosts",
+ "importJson": "Importar JSON",
+ "importing": "Importando...",
+ "importJsonTitle": "Importar Hosts SSH do JSON",
+ "importJsonDesc": "Envie um arquivo JSON para importar vários hosts SSH de uma vez (máx. 100).",
+ "downloadSample": "Baixar Exemplo",
+ "formatGuide": "Guia de Formato",
+ "exportCredentialWarning": "Aviso: O host \"{{name}}\" usa autenticação por credencial. O arquivo exportado não incluirá os dados da credencial e precisará ser reconfigurado manualmente após a importação. Deseja continuar?",
+ "exportSensitiveDataWarning": "Aviso: O host \"{{name}}\" contém dados de autenticação sensíveis (senha/chave SSH). O arquivo exportado incluirá esses dados em texto simples. Mantenha o arquivo seguro e exclua-o após o uso. Deseja continuar?",
+ "uncategorized": "Sem categoria",
+ "confirmDelete": "Tem certeza que deseja excluir \"{{name}}\"?",
+ "failedToDeleteHost": "Falha ao excluir host",
+ "failedToExportHost": "Falha ao exportar host. Certifique-se de que está logado e tem acesso aos dados do host.",
+ "jsonMustContainHosts": "O JSON deve conter um array \"hosts\" ou ser um array de hosts",
+ "noHostsInJson": "Nenhum host encontrado no arquivo JSON",
+ "maxHostsAllowed": "Máximo de 100 hosts permitidos por importação",
+ "importCompleted": "Importação concluída: {{success}} com sucesso, {{failed}} falhas",
+ "importFailed": "Falha na importação",
+ "importError": "Erro na importação",
+ "failedToImportJson": "Falha ao importar arquivo JSON",
+ "connectionDetails": "Detalhes da Conexão",
+ "organization": "Organização",
+ "ipAddress": "Endereço IP",
+ "port": "Porta",
+ "name": "Nome",
+ "username": "Usuário",
+ "folder": "Pasta",
+ "tags": "Tags",
+ "pin": "Fixar",
+ "passwordRequired": "Senha é obrigatória quando usar autenticação por senha",
+ "sshKeyRequired": "Chave Privada SSH é obrigatória quando usar autenticação por chave",
+ "keyTypeRequired": "Tipo de Chave é obrigatório quando usar autenticação por chave",
+ "mustSelectValidSshConfig": "Deve selecionar uma configuração SSH válida da lista",
+ "addHost": "Adicionar Host",
+ "editHost": "Editar Host",
+ "cloneHost": "Clonar Host",
+ "updateHost": "Atualizar Host",
+ "hostUpdatedSuccessfully": "Host \"{{name}}\" atualizado com sucesso!",
+ "hostAddedSuccessfully": "Host \"{{name}}\" adicionado com sucesso!",
+ "hostDeletedSuccessfully": "Host \"{{name}}\" excluído com sucesso!",
+ "failedToSaveHost": "Falha ao salvar host. Por favor, tente novamente.",
+ "enableTerminal": "Habilitar Terminal",
+ "enableTerminalDesc": "Habilitar/desabilitar visibilidade do host na aba Terminal",
+ "enableTunnel": "Habilitar Túnel",
+ "enableTunnelDesc": "Habilitar/desabilitar visibilidade do host na aba Túnel",
+ "enableFileManager": "Habilitar Gerenciador de Arquivos",
+ "enableFileManagerDesc": "Habilitar/desabilitar visibilidade do host na aba Gerenciador de Arquivos",
+ "defaultPath": "Caminho Padrão",
+ "defaultPathDesc": "Diretório padrão ao abrir o gerenciador de arquivos para este host",
+ "tunnelConnections": "Conexões de Túnel",
+ "connection": "Conexão",
+ "remove": "Remover",
+ "sourcePort": "Porta de Origem",
+ "sourcePortDesc": "(Source refere-se aos Detalhes da Conexão Atual na aba Geral)",
+ "endpointPort": "Porta de Destino",
+ "endpointSshConfig": "Configuração SSH do Endpoint",
+ "tunnelForwardDescription": "Este túnel encaminhará o tráfego da porta {{sourcePort}} na máquina de origem (detalhes da conexão atual na aba Geral) para a porta {{endpointPort}} na máquina de destino.",
+ "maxRetries": "Máximo de Tentativas",
+ "maxRetriesDescription": "Número máximo de tentativas de reconexão para a conexão do túnel.",
+ "retryInterval": "Intervalo de Tentativas (segundos)",
+ "retryIntervalDescription": "Tempo de espera entre tentativas de reconexão.",
+ "autoStartContainer": "Iniciar Automaticamente ao Lançar Container",
+ "autoStartDesc": "Iniciar automaticamente este túnel quando o container for iniciado",
+ "addConnection": "Adicionar Conexão de Túnel",
+ "sshpassRequired": "sshpass é necessário para autenticação por senha",
+ "sshpassRequiredDesc": "Para autenticação por senha em túneis, o sshpass deve estar instalado no sistema.",
+ "otherInstallMethods": "Outros métodos de instalação:",
+ "debianUbuntuEquivalent": "(Debian/Ubuntu) ou o equivalente para seu SO.",
+ "or": "ou",
+ "centosRhelFedora": "CentOS/RHEL/Fedora",
+ "macos": "macOS",
+ "windows": "Windows",
+ "sshServerConfigRequired": "Configuração do Servidor SSH Necessária",
+ "sshServerConfigDesc": "Para conexões de túnel, o servidor SSH deve ser configurado para permitir o encaminhamento de porta:",
+ "gatewayPortsYes": "para vincular portas remotas a todas as interfaces",
+ "allowTcpForwardingYes": "para habilitar o encaminhamento de porta",
+ "permitRootLoginYes": "se estiver usando usuário root para tunelamento",
+ "editSshConfig": "Edite /etc/ssh/sshd_config e reinicie o SSH: sudo systemctl restart sshd",
+ "upload": "Enviar",
+ "authentication": "Autenticação",
+ "password": "Senha",
+ "key": "Chave",
+ "credential": "Credencial",
+ "none": "Nenhum",
+ "selectCredential": "Selecionar Credencial",
+ "selectCredentialPlaceholder": "Escolha uma credencial...",
+ "credentialRequired": "Credencial é obrigatória ao usar autenticação por credencial",
+ "credentialDescription": "Selecionar uma credencial irá sobrescrever o nome de usuário atual e usar os detalhes de autenticação da credencial.",
+ "sshPrivateKey": "Chave Privada SSH",
+ "keyPassword": "Senha da Chave",
+ "keyType": "Tipo de Chave",
+ "autoDetect": "Detecção Automática",
+ "rsa": "RSA",
+ "ed25519": "ED25519",
+ "ecdsaNistP256": "ECDSA NIST P-256",
+ "ecdsaNistP384": "ECDSA NIST P-384",
+ "ecdsaNistP521": "ECDSA NIST P-521",
+ "dsa": "DSA",
+ "rsaSha2256": "RSA SHA2-256",
+ "rsaSha2512": "RSA SHA2-512",
+ "uploadFile": "Enviar Arquivo",
+ "pasteKey": "Colar Chave",
+ "updateKey": "Atualizar Chave",
+ "existingKey": "Chave Existente (clique para alterar)",
+ "existingCredential": "Credencial Existente (clique para alterar)",
+ "addTagsSpaceToAdd": "adicionar tags (espaço para adicionar)",
+ "terminalBadge": "Terminal",
+ "tunnelBadge": "Túnel",
+ "fileManagerBadge": "Gerenciador de Arquivos",
+ "general": "Geral",
+ "terminal": "Terminal",
+ "tunnel": "Túnel",
+ "fileManager": "Gerenciador de Arquivos",
+ "hostViewer": "Visualizador de Host",
+ "confirmRemoveFromFolder": "Tem certeza que deseja remover \"{{name}}\" da pasta \"{{folder}}\"? O host será movido para \"Sem Pasta\".",
+ "removedFromFolder": "Host \"{{name}}\" removido da pasta com sucesso",
+ "failedToRemoveFromFolder": "Falha ao remover host da pasta",
+ "folderRenamed": "Pasta \"{{oldName}}\" renomeada para \"{{newName}}\" com sucesso",
+ "failedToRenameFolder": "Falha ao renomear pasta",
+ "movedToFolder": "Host \"{{name}}\" movido para \"{{folder}}\" com sucesso",
+ "failedToMoveToFolder": "Falha ao mover host para a pasta",
+ "statistics": "Estatísticas",
+ "enabledWidgets": "Widgets Habilitados",
+ "enabledWidgetsDesc": "Selecione quais widgets de estatísticas exibir para este host",
+ "monitoringConfiguration": "Configuração de Monitoramento",
+ "monitoringConfigurationDesc": "Configure com que frequência as estatísticas e o status do servidor são verificados",
+ "statusCheckEnabled": "Habilitar Monitoramento de Status",
+ "statusCheckEnabledDesc": "Verificar se o servidor está online ou offline",
+ "statusCheckInterval": "Intervalo de Verificação de Status",
+ "statusCheckIntervalDesc": "Com que frequência verificar se o host está online (5s - 1h)",
+ "metricsEnabled": "Habilitar Monitoramento de Métricas",
+ "metricsEnabledDesc": "Coletar estatísticas de CPU, RAM, disco e outros sistemas",
+ "metricsInterval": "Intervalo de Coleta de Métricas",
+ "metricsIntervalDesc": "Com que frequência coletar estatísticas do servidor (5s - 1h)",
+ "intervalSeconds": "segundos",
+ "intervalMinutes": "minutos",
+ "intervalValidation": "Os intervalos de monitoramento devem estar entre 5 segundos e 1 hora (3600 segundos)",
+ "monitoringDisabled": "O monitoramento do servidor está desabilitado para este host",
+ "enableMonitoring": "Habilite o monitoramento em Gerenciador de Hosts → aba Estatísticas",
+ "monitoringDisabledBadge": "Monitoramento Desligado",
+ "statusMonitoring": "Status",
+ "metricsMonitoring": "Métricas",
+ "terminalCustomizationNotice": "Nota: As personalizações do terminal funcionam apenas na versão Desktop Website. Aplicativos Mobile e Electron usam as configurações padrão do terminal do sistema.",
+ "noneAuthTitle": "Autenticação Interativa por Teclado",
+ "noneAuthDescription": "Este método de autenticação usará autenticação interativa por teclado ao conectar ao servidor SSH.",
+ "noneAuthDetails": "A autenticação interativa por teclado permite que o servidor solicite credenciais durante a conexão. Isso é útil para servidores que requerem autenticação multifator ou entrada de senha dinâmica.",
+ "forceKeyboardInteractive": "Forçar Interativo com Teclado",
+ "forceKeyboardInteractiveDesc": "Força o uso da autenticação interativa com teclado. Isso é frequentemente necessário para servidores que usam Autenticação de Dois Fatores (TOTP/2FA)."
+ },
+ "terminal": {
+ "title": "Terminal",
+ "connect": "Conectar ao Host",
+ "disconnect": "Desconectar",
+ "clear": "Limpar",
+ "copy": "Copiar",
+ "paste": "Colar",
+ "find": "Localizar",
+ "fullscreen": "Tela Cheia",
+ "splitHorizontal": "Dividir Horizontalmente",
+ "splitVertical": "Dividir Verticalmente",
+ "closePanel": "Fechar Painel",
+ "reconnect": "Reconectar",
+ "sessionEnded": "Sessão Encerrada",
+ "connectionLost": "Conexão Perdida",
+ "error": "ERRO: {{message}}",
+ "disconnected": "Desconectado",
+ "connectionClosed": "Conexão fechada",
+ "connectionError": "Erro de conexão: {{message}}",
+ "connected": "Conectado",
+ "sshConnected": "Conexão SSH estabelecida",
+ "authError": "Falha na autenticação: {{message}}",
+ "unknownError": "Ocorreu um erro desconhecido",
+ "messageParseError": "Falha ao analisar mensagem do servidor",
+ "websocketError": "Erro na conexão WebSocket",
+ "connecting": "Conectando...",
+ "reconnecting": "Reconectando... ({{attempt}}/{{max}})",
+ "reconnected": "Reconectado com sucesso",
+ "maxReconnectAttemptsReached": "Número máximo de tentativas de reconexão atingido",
+ "connectionTimeout": "Tempo limite de conexão esgotado",
+ "terminalTitle": "Terminal - {{host}}",
+ "terminalWithPath": "Terminal - {{host}}:{{path}}",
+ "runTitle": "Executando {{command}} - {{host}}"
+ },
+ "fileManager": {
+ "title": "Gerenciador de Arquivos",
+ "file": "Arquivo",
+ "folder": "Pasta",
+ "connectToSsh": "Conecte-se ao SSH para usar operações de arquivo",
+ "uploadFile": "Enviar Arquivo",
+ "downloadFile": "Baixar",
+ "edit": "Editar",
+ "preview": "Visualizar",
+ "previous": "Anterior",
+ "next": "Próximo",
+ "pageXOfY": "Página {{current}} de {{total}}",
+ "zoomOut": "Diminuir Zoom",
+ "zoomIn": "Aumentar Zoom",
+ "newFile": "Novo Arquivo",
+ "newFolder": "Nova Pasta",
+ "rename": "Renomear",
+ "renameItem": "Renomear Item",
+ "deleteItem": "Excluir Item",
+ "currentPath": "Caminho Atual",
+ "uploadFileTitle": "Enviar Arquivo",
+ "maxFileSize": "Máx: 1GB (JSON) / 5GB (Binário) - Arquivos grandes suportados",
+ "removeFile": "Remover Arquivo",
+ "clickToSelectFile": "Clique para selecionar um arquivo",
+ "chooseFile": "Escolher Arquivo",
+ "uploading": "Enviando...",
+ "downloading": "Baixando...",
+ "uploadingFile": "Enviando {{name}}...",
+ "uploadingLargeFile": "Enviando arquivo grande {{name}} ({{size}})...",
+ "downloadingFile": "Baixando {{name}}...",
+ "creatingFile": "Criando {{name}}...",
+ "creatingFolder": "Criando {{name}}...",
+ "deletingItem": "Excluindo {{type}} {{name}}...",
+ "renamingItem": "Renomeando {{type}} {{oldName}} para {{newName}}...",
+ "createNewFile": "Criar Novo Arquivo",
+ "fileName": "Nome do Arquivo",
+ "creating": "Criando...",
+ "createFile": "Criar Arquivo",
+ "createNewFolder": "Criar Nova Pasta",
+ "folderName": "Nome da Pasta",
+ "createFolder": "Criar Pasta",
+ "warningCannotUndo": "Aviso: Esta ação não pode ser desfeita",
+ "itemPath": "Caminho do Item",
+ "thisIsDirectory": "Isto é um diretório (será excluído recursivamente)",
+ "deleting": "Excluindo...",
+ "currentPathLabel": "Caminho Atual",
+ "newName": "Novo Nome",
+ "thisIsDirectoryRename": "Isto é um diretório",
+ "renaming": "Renomeando...",
+ "fileUploadedSuccessfully": "Arquivo \"{{name}}\" enviado com sucesso",
+ "failedToUploadFile": "Falha ao enviar arquivo",
+ "fileDownloadedSuccessfully": "Arquivo \"{{name}}\" baixado com sucesso",
+ "failedToDownloadFile": "Falha ao baixar arquivo",
+ "noFileContent": "Nenhum conteúdo de arquivo recebido",
+ "filePath": "Caminho do Arquivo",
+ "fileCreatedSuccessfully": "Arquivo \"{{name}}\" criado com sucesso",
+ "failedToCreateFile": "Falha ao criar arquivo",
+ "folderCreatedSuccessfully": "Pasta \"{{name}}\" criada com sucesso",
+ "failedToCreateFolder": "Falha ao criar pasta",
+ "failedToCreateItem": "Falha ao criar item",
+ "operationFailed": "Operação {{operation}} falhou para {{name}}: {{error}}",
+ "failedToResolveSymlink": "Falha ao resolver link simbólico",
+ "itemDeletedSuccessfully": "{{type}} excluído com sucesso",
+ "itemsDeletedSuccessfully": "{{count}} itens excluídos com sucesso",
+ "failedToDeleteItems": "Falha ao excluir itens",
+ "dragFilesToUpload": "Arraste arquivos aqui para enviar",
+ "emptyFolder": "Esta pasta está vazia",
+ "itemCount": "{{count}} itens",
+ "selectedCount": "{{count}} selecionados",
+ "searchFiles": "Pesquisar arquivos...",
+ "upload": "Enviar",
+ "selectHostToStart": "Selecione um host para iniciar o gerenciamento de arquivos",
+ "failedToConnect": "Falha ao conectar ao SSH",
+ "failedToLoadDirectory": "Falha ao carregar diretório",
+ "noSSHConnection": "Nenhuma conexão SSH disponível",
+ "enterFolderName": "Digite o nome da pasta:",
+ "enterFileName": "Digite o nome do arquivo:",
+ "copy": "Copiar",
+ "cut": "Recortar",
+ "paste": "Colar",
+ "delete": "Excluir",
+ "properties": "Propriedades",
+ "preview": "Visualizar",
+ "refresh": "Atualizar",
+ "downloadFiles": "Baixar {{count}} arquivos para o Navegador",
+ "copyFiles": "Copiar {{count}} itens",
+ "cutFiles": "Recortar {{count}} itens",
+ "deleteFiles": "Excluir {{count}} itens",
+ "filesCopiedToClipboard": "{{count}} itens copiados para a área de transferência",
+ "filesCutToClipboard": "{{count}} itens recortados para a área de transferência",
+ "movedItems": "{{count}} itens movidos",
+ "failedToDeleteItem": "Falha ao excluir item",
+ "itemRenamedSuccessfully": "{{type}} renomeado com sucesso",
+ "failedToRenameItem": "Falha ao renomear item",
+ "upload": "Enviar",
+ "download": "Baixar",
+ "newFile": "Novo Arquivo",
+ "newFolder": "Nova Pasta",
+ "rename": "Renomear",
+ "delete": "Excluir",
+ "permissions": "Permissões",
+ "size": "Tamanho",
+ "modified": "Modificado",
+ "path": "Caminho",
+ "fileName": "Nome do Arquivo",
+ "folderName": "Nome da Pasta",
+ "confirmDelete": "Tem certeza que deseja excluir {{name}}?",
+ "uploadSuccess": "Arquivo enviado com sucesso",
+ "uploadFailed": "Falha ao enviar arquivo",
+ "downloadSuccess": "Arquivo baixado com sucesso",
+ "downloadFailed": "Falha ao baixar arquivo",
+ "permissionDenied": "Permissão negada",
+ "checkDockerLogs": "Verifique os logs do Docker para informações detalhadas do erro",
+ "internalServerError": "Ocorreu um erro interno do servidor",
+ "serverError": "Erro do Servidor",
+ "error": "Erro",
+ "requestFailed": "Requisição falhou com código de status",
+ "unknownFileError": "desconhecido",
+ "cannotReadFile": "Não é possível ler o arquivo",
+ "noSshSessionId": "Nenhum ID de sessão SSH disponível",
+ "noFilePath": "Nenhum caminho de arquivo disponível",
+ "noCurrentHost": "Nenhum host atual disponível",
+ "fileSavedSuccessfully": "Arquivo salvo com sucesso",
+ "saveTimeout": "Tempo limite da operação de salvamento esgotado. O arquivo pode ter sido salvo com sucesso, mas a operação demorou muito para ser concluída. Verifique os logs do Docker para confirmação.",
+ "failedToSaveFile": "Falha ao salvar arquivo",
+ "folder": "Pasta",
+ "file": "Arquivo",
+ "deletedSuccessfully": "excluído com sucesso",
+ "failedToDeleteItem": "Falha ao excluir item",
+ "connectToServer": "Conectar a um Servidor",
+ "selectServerToEdit": "Selecione um servidor da barra lateral para começar a editar arquivos",
+ "fileOperations": "Operações de Arquivo",
+ "confirmDeleteMessage": "Tem certeza que deseja excluir {{name}} ?",
+ "confirmDeleteSingleItem": "Tem certeza que deseja excluir permanentemente \"{{name}}\"?",
+ "confirmDeleteMultipleItems": "Tem certeza que deseja excluir permanentemente {{count}} itens?",
+ "confirmDeleteMultipleItemsWithFolders": "Tem certeza que deseja excluir permanentemente {{count}} itens? Isso inclui pastas e seus conteúdos.",
+ "confirmDeleteFolder": "Tem certeza que deseja excluir permanentemente a pasta \"{{name}}\" e todo seu conteúdo?",
+ "deleteDirectoryWarning": "Isso excluirá a pasta e todo seu conteúdo.",
+ "actionCannotBeUndone": "Esta ação não pode ser desfeita.",
+ "permanentDeleteWarning": "Esta ação não pode ser desfeita. O(s) item(ns) será(ão) excluído(s) permanentemente do servidor.",
+ "recent": "Recente",
+ "pinned": "Fixado",
+ "folderShortcuts": "Atalhos de Pasta",
+ "noRecentFiles": "Nenhum arquivo recente.",
+ "noPinnedFiles": "Nenhum arquivo fixado.",
+ "enterFolderPath": "Digite o caminho da pasta",
+ "noShortcuts": "Nenhum atalho.",
+ "searchFilesAndFolders": "Pesquisar arquivos e pastas...",
+ "noFilesOrFoldersFound": "Nenhum arquivo ou pasta encontrado.",
+ "failedToConnectSSH": "Falha ao conectar ao SSH",
+ "failedToReconnectSSH": "Falha ao reconectar sessão SSH",
+ "failedToListFiles": "Falha ao listar arquivos",
+ "fetchHomeDataTimeout": "Tempo limite excedido ao buscar dados iniciais",
+ "sshStatusCheckTimeout": "Tempo limite excedido na verificação do status SSH",
+ "sshReconnectionTimeout": "Tempo limite excedido na reconexão SSH",
+ "saveOperationTimeout": "Tempo limite excedido na operação de salvar",
+ "cannotSaveFile": "Não é possível salvar o arquivo",
+ "dragSystemFilesToUpload": "Arraste arquivos do sistema aqui para fazer upload",
+ "dragFilesToWindowToDownload": "Arraste arquivos para fora da janela para baixar",
+ "openTerminalHere": "Abrir Terminal Aqui",
+ "run": "Executar",
+ "saveToSystem": "Salvar como...",
+ "selectLocationToSave": "Selecionar Local para Salvar",
+ "openTerminalInFolder": "Abrir Terminal nesta Pasta",
+ "openTerminalInFileLocation": "Abrir Terminal no Local do Arquivo",
+ "terminalWithPath": "Terminal - {{host}}:{{path}}",
+ "runningFile": "Executando - {{file}}",
+ "onlyRunExecutableFiles": "Só é possível executar arquivos executáveis",
+ "noHostSelected": "Nenhum host selecionado",
+ "starred": "Favoritos",
+ "shortcuts": "Atalhos",
+ "directories": "Diretórios",
+ "removedFromRecentFiles": "\"{{name}}\" removido dos arquivos recentes",
+ "removeFailed": "Falha ao remover",
+ "unpinnedSuccessfully": "\"{{name}}\" desfixado com sucesso",
+ "unpinFailed": "Falha ao desfixar",
+ "removedShortcut": "Atalho \"{{name}}\" removido",
+ "removeShortcutFailed": "Falha ao remover atalho",
+ "clearedAllRecentFiles": "Todos os arquivos recentes foram limpos",
+ "clearFailed": "Falha ao limpar",
+ "removeFromRecentFiles": "Remover dos arquivos recentes",
+ "clearAllRecentFiles": "Limpar todos os arquivos recentes",
+ "unpinFile": "Desfixar arquivo",
+ "removeShortcut": "Remover atalho",
+ "saveFilesToSystem": "Salvar {{count}} arquivos como...",
+ "saveToSystem": "Salvar como...",
+ "pinFile": "Fixar arquivo",
+ "addToShortcuts": "Adicionar aos atalhos",
+ "selectLocationToSave": "Selecionar local para salvar",
+ "downloadToDefaultLocation": "Baixar para o local padrão",
+ "pasteFailed": "Falha ao colar",
+ "noUndoableActions": "Nenhuma ação pode ser desfeita",
+ "undoCopySuccess": "Operação de cópia desfeita: {{count}} arquivos copiados foram excluídos",
+ "undoCopyFailedDelete": "Falha ao desfazer: Não foi possível excluir os arquivos copiados",
+ "undoCopyFailedNoInfo": "Falha ao desfazer: Não foi possível encontrar informações do arquivo copiado",
+ "undoMoveSuccess": "Operação de mover desfeita: {{count}} arquivos movidos de volta ao local original",
+ "undoMoveFailedMove": "Falha ao desfazer: Não foi possível mover os arquivos de volta",
+ "undoMoveFailedNoInfo": "Falha ao desfazer: Não foi possível encontrar informações do arquivo movido",
+ "undoDeleteNotSupported": "Operação de exclusão não pode ser desfeita: Os arquivos foram excluídos permanentemente do servidor",
+ "undoTypeNotSupported": "Tipo de operação de desfazer não suportado",
+ "undoOperationFailed": "Falha na operação de desfazer",
+ "unknownError": "Erro desconhecido",
+ "enterPath": "Digite o caminho...",
+ "editPath": "Editar caminho",
+ "confirm": "Confirmar",
+ "cancel": "Cancelar",
+ "folderName": "Nome da pasta",
+ "find": "Localizar...",
+ "replaceWith": "Substituir por...",
+ "replace": "Substituir",
+ "replaceAll": "Substituir Tudo",
+ "downloadInstead": "Baixar em Vez Disso",
+ "keyboardShortcuts": "Atalhos do Teclado",
+ "searchAndReplace": "Localizar & Substituir",
+ "editing": "Edição",
+ "navigation": "Navegação",
+ "code": "Código",
+ "search": "Pesquisar",
+ "findNext": "Localizar Próximo",
+ "findPrevious": "Localizar Anterior",
+ "save": "Salvar",
+ "selectAll": "Selecionar Tudo",
+ "undo": "Desfazer",
+ "redo": "Refazer",
+ "goToLine": "Ir para Linha",
+ "moveLineUp": "Mover Linha para Cima",
+ "moveLineDown": "Mover Linha para Baixo",
+ "toggleComment": "Alternar Comentário",
+ "indent": "Indentar",
+ "outdent": "Remover Indentação",
+ "autoComplete": "Auto Completar",
+ "imageLoadError": "Falha ao carregar imagem",
+ "zoomIn": "Aumentar Zoom",
+ "zoomOut": "Diminuir Zoom",
+ "rotate": "Rotacionar",
+ "originalSize": "Tamanho Original",
+ "startTyping": "Comece a digitar...",
+ "unknownSize": "Tamanho desconhecido",
+ "fileIsEmpty": "Arquivo está vazio",
+ "modified": "Modificado",
+ "largeFileWarning": "Aviso de Arquivo Grande",
+ "largeFileWarningDesc": "Este arquivo tem {{size}} de tamanho, o que pode causar problemas de desempenho quando aberto como texto.",
+ "fileNotFoundAndRemoved": "Arquivo \"{{name}}\" não encontrado e foi removido dos arquivos recentes/fixados",
+ "failedToLoadFile": "Falha ao carregar arquivo: {{error}}",
+ "serverErrorOccurred": "Ocorreu um erro no servidor. Por favor, tente novamente mais tarde.",
+ "fileSavedSuccessfully": "Arquivo salvo com sucesso",
+ "autoSaveFailed": "Falha no salvamento automático",
+ "fileAutoSaved": "Arquivo salvo automaticamente",
+
+ "moveFileFailed": "Falha ao mover {{name}}",
+ "moveOperationFailed": "Falha na operação de mover",
+ "canOnlyCompareFiles": "Só é possível comparar dois arquivos",
+ "comparingFiles": "Comparando arquivos: {{file1}} e {{file2}}",
+ "dragFailed": "Falha na operação de arrastar",
+ "filePinnedSuccessfully": "Arquivo \"{{name}}\" fixado com sucesso",
+ "pinFileFailed": "Falha ao fixar arquivo",
+ "fileUnpinnedSuccessfully": "Arquivo \"{{name}}\" desfixado com sucesso",
+ "unpinFileFailed": "Falha ao desfixar arquivo",
+ "shortcutAddedSuccessfully": "Atalho de pasta \"{{name}}\" adicionado com sucesso",
+ "addShortcutFailed": "Falha ao adicionar atalho",
+ "operationCompletedSuccessfully": "{{operation}} {{count}} itens com sucesso",
+ "operationCompleted": "{{operation}} {{count}} itens",
+ "downloadFileSuccess": "Arquivo {{name}} baixado com sucesso",
+ "downloadFileFailed": "Falha no download",
+ "moveTo": "Mover para {{name}}",
+ "diffCompareWith": "Comparar diferenças com {{name}}",
+ "dragOutsideToDownload": "Arraste para fora da janela para baixar ({{count}} arquivos)",
+ "newFolderDefault": "NovaPasta",
+ "newFileDefault": "NovoArquivo.txt",
+ "successfullyMovedItems": "{{count}} itens movidos com sucesso para {{target}}",
+ "move": "Mover",
+ "searchInFile": "Pesquisar no arquivo (Ctrl+F)",
+ "showKeyboardShortcuts": "Mostrar atalhos do teclado",
+ "startWritingMarkdown": "Comece a escrever seu conteúdo markdown...",
+ "loadingFileComparison": "Carregando comparação de arquivos...",
+ "reload": "Recarregar",
+ "compare": "Comparar",
+ "sideBySide": "Lado a Lado",
+ "inline": "Em linha",
+ "fileComparison": "Comparação de Arquivos: {{file1}} vs {{file2}}",
+ "fileTooLarge": "Arquivo muito grande: {{error}}",
+ "sshConnectionFailed": "Falha na conexão SSH. Por favor, verifique sua conexão com {{name}} ({{ip}}:{{port}})",
+ "loadFileFailed": "Falha ao carregar arquivo: {{error}}"
+ },
+ "tunnels": {
+ "title": "Túneis SSH",
+ "noSshTunnels": "Sem Túneis SSH",
+ "createFirstTunnelMessage": "Você ainda não criou nenhum túnel SSH. Configure conexões de túnel no Gerenciador de Hosts para começar.",
+ "connected": "Conectado",
+ "disconnected": "Desconectado",
+ "connecting": "Conectando...",
+ "disconnecting": "Desconectando...",
+ "unknownTunnelStatus": "Desconhecido",
+ "unknown": "Desconhecido",
+ "error": "Erro",
+ "failed": "Falhou",
+ "retrying": "Tentando novamente",
+ "waiting": "Aguardando",
+ "waitingForRetry": "Aguardando nova tentativa",
+ "retryingConnection": "Tentando reconectar",
+ "canceling": "Cancelando...",
+ "connect": "Conectar",
+ "disconnect": "Desconectar",
+ "cancel": "Cancelar",
+ "port": "Porta",
+ "attempt": "Tentativa {{current}} de {{max}}",
+ "nextRetryIn": "Próxima tentativa em {{seconds}} segundos",
+ "checkDockerLogs": "Verifique seus logs do Docker para ver o motivo do erro, entre no",
+ "noTunnelConnections": "Nenhuma conexão de túnel configurada",
+ "tunnelConnections": "Conexões de Túnel",
+ "addTunnel": "Adicionar Túnel",
+ "editTunnel": "Editar Túnel",
+ "deleteTunnel": "Excluir Túnel",
+ "tunnelName": "Nome do Túnel",
+ "localPort": "Porta Local",
+ "remoteHost": "Host Remoto",
+ "remotePort": "Porta Remota",
+ "autoStart": "Iniciar Automaticamente",
+ "status": "Status",
+ "active": "Ativo",
+ "inactive": "Inativo",
+ "start": "Iniciar",
+ "stop": "Parar",
+ "restart": "Reiniciar",
+ "connectionType": "Tipo de Conexão",
+ "local": "Local",
+ "remote": "Remoto",
+ "dynamic": "Dinâmico",
+ "noSshTunnels": "Sem Túneis SSH",
+ "createFirstTunnelMessage": "Crie seu primeiro túnel SSH para começar. Use o Gerenciador SSH para adicionar hosts com conexões de túnel.",
+ "unknownConnectionStatus": "Desconhecido",
+ "connected": "Conectado",
+ "connecting": "Conectando...",
+ "disconnecting": "Desconectando...",
+ "disconnected": "Desconectado",
+ "portMapping": "Porta {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
+ "disconnect": "Desconectar",
+ "connect": "Conectar",
+ "canceling": "Cancelando...",
+ "endpointHostNotFound": "Host de destino não encontrado",
+ "discord": "Discord",
+ "githubIssue": "issue no GitHub",
+ "forHelp": "para ajuda"
+ },
+ "serverStats": {
+ "title": "Estatísticas do Servidor",
+ "cpu": "CPU",
+ "memory": "Memória",
+ "disk": "Disco",
+ "network": "Rede",
+ "uptime": "Tempo Ativo",
+ "loadAverage": "Carga Média",
+ "processes": "Processos",
+ "connections": "Conexões",
+ "usage": "Uso",
+ "available": "Disponível",
+ "total": "Total",
+ "free": "Livre",
+ "used": "Usado",
+ "percentage": "Porcentagem",
+ "refreshStatusAndMetrics": "Atualizar status e métricas",
+ "refreshStatus": "Atualizar Status",
+ "fileManagerAlreadyOpen": "Gerenciador de Arquivos já está aberto para este host",
+ "openFileManager": "Abrir Gerenciador de Arquivos",
+ "cpuCores_one": "{{count}} CPU",
+ "cpuCores_other": "{{count}} CPUs",
+ "naCpus": "N/D CPU(s)",
+ "loadAverage": "Média: {{avg1}}, {{avg5}}, {{avg15}}",
+ "loadAverageNA": "Média: N/D",
+ "cpuUsage": "Uso da CPU",
+ "memoryUsage": "Uso de Memória",
+ "rootStorageSpace": "Espaço de Armazenamento Root",
+ "of": "de",
+ "feedbackMessage": "Tem ideias sobre o que deveria vir a seguir para o gerenciamento do servidor? Compartilhe em",
+ "failedToFetchHostConfig": "Falha ao buscar configuração do host",
+ "failedToFetchStatus": "Falha ao buscar status do servidor",
+ "failedToFetchMetrics": "Falha ao buscar métricas do servidor",
+ "failedToFetchHomeData": "Falha ao buscar dados da home",
+ "loadingMetrics": "Carregando métricas...",
+ "refreshing": "Atualizando...",
+ "serverOffline": "Servidor Offline",
+ "cannotFetchMetrics": "Não é possível buscar métricas do servidor offline",
+ "load": "Carga",
+ "free": "Livre",
+ "available": "Disponível"
+ },
+ "auth": {
+ "loginTitle": "Entrar no Termix",
+ "registerTitle": "Criar Conta",
+ "loginButton": "Entrar",
+ "registerButton": "Registrar",
+ "forgotPassword": "Esqueceu a Senha?",
+ "rememberMe": "Lembrar de Mim",
+ "noAccount": "Não tem uma conta?",
+ "hasAccount": "Já tem uma conta?",
+ "loginSuccess": "Login realizado com sucesso",
+ "loginFailed": "Falha no login",
+ "registerSuccess": "Registro realizado com sucesso",
+ "registerFailed": "Falha no registro",
+ "logoutSuccess": "Desconectado com sucesso",
+ "invalidCredentials": "Usuário ou senha inválidos",
+ "accountCreated": "Conta criada com sucesso",
+ "passwordReset": "Link de redefinição de senha enviado",
+ "twoFactorAuth": "Autenticação de Dois Fatores",
+ "enterCode": "Digite o código de verificação",
+ "backupCode": "Ou use o código de backup",
+ "verifyCode": "Verificar Código",
+ "enableTwoFactor": "Ativar Autenticação de Dois Fatores",
+ "disableTwoFactor": "Desativar Autenticação de Dois Fatores",
+ "scanQRCode": "Escaneie este código QR com seu aplicativo autenticador",
+ "backupCodes": "Códigos de Backup",
+ "saveBackupCodes": "Salve estes códigos de backup em um local seguro",
+ "twoFactorEnabledSuccess": "Autenticação de dois fatores ativada com sucesso!",
+ "twoFactorDisabled": "Autenticação de dois fatores desativada",
+ "newBackupCodesGenerated": "Novos códigos de backup gerados",
+ "backupCodesDownloaded": "Códigos de backup baixados",
+ "pleaseEnterSixDigitCode": "Por favor, digite um código de 6 dígitos",
+ "invalidVerificationCode": "Código de verificação inválido",
+ "failedToDisableTotp": "Falha ao desativar TOTP",
+ "failedToGenerateBackupCodes": "Falha ao gerar códigos de backup",
+ "enterPassword": "Digite sua senha",
+ "lockedOidcAuth": "Bloqueado (Auth OIDC)",
+ "twoFactorTitle": "Autenticação de Dois Fatores",
+ "twoFactorProtected": "Sua conta está protegida com autenticação de dois fatores",
+ "twoFactorActive": "A autenticação de dois fatores está atualmente ativa em sua conta",
+ "disable2FA": "Desativar 2FA",
+ "disableTwoFactorWarning": "Desativar a autenticação de dois fatores tornará sua conta menos segura",
+ "passwordOrTotpCode": "Senha ou Código TOTP",
+ "or": "Ou",
+ "generateNewBackupCodesText": "Gere novos códigos de backup se você perdeu os existentes",
+ "generateNewBackupCodes": "Gerar Novos Códigos de Backup",
+ "yourBackupCodes": "Seus Códigos de Backup",
+ "download": "Baixar",
+ "setupTwoFactorTitle": "Configurar Autenticação de Dois Fatores",
+ "step1ScanQR": "Passo 1: Escaneie o código QR com seu aplicativo autenticador",
+ "manualEntryCode": "Código de Entrada Manual",
+ "cannotScanQRText": "Se você não conseguir escanear o código QR, digite este código manualmente no seu aplicativo autenticador",
+ "nextVerifyCode": "Próximo: Verificar Código",
+ "verifyAuthenticator": "Verifique Seu Autenticador",
+ "step2EnterCode": "Passo 2: Digite o código de 6 dígitos do seu aplicativo autenticador",
+ "verificationCode": "Código de Verificação",
+ "back": "Voltar",
+ "verifyAndEnable": "Verificar e Ativar",
+ "saveBackupCodesTitle": "Salve Seus Códigos de Backup",
+ "step3StoreCodesSecurely": "Passo 3: Guarde estes códigos em um local seguro",
+ "importantBackupCodesText": "Salve estes códigos de backup em um local seguro. Você pode usá-los para acessar sua conta se perder seu dispositivo autenticador.",
+ "completeSetup": "Concluir Configuração",
+ "notEnabledText": "A autenticação de dois fatores adiciona uma camada extra de segurança ao exigir um código do seu aplicativo autenticador ao fazer login.",
+ "enableTwoFactorButton": "Ativar Autenticação de Dois Fatores",
+ "addExtraSecurityLayer": "Adicione uma camada extra de segurança à sua conta",
+ "firstUser": "Primeiro Usuário",
+ "firstUserMessage": "Você é o primeiro usuário e será tornado admin. Você pode ver as configurações de admin no menu suspenso do usuário na barra lateral. Se você acha que isso é um erro, verifique os logs do docker ou crie uma issue no GitHub.",
+ "external": "Externo",
+ "loginWithExternal": "Entrar com Provedor Externo",
+ "loginWithExternalDesc": "Entre usando seu provedor de identidade externo configurado",
+ "externalNotSupportedInElectron": "A autenticação externa ainda não é suportada no aplicativo Electron. Por favor, use a versão web para login OIDC.",
+ "resetPasswordButton": "Redefinir Senha",
+ "sendResetCode": "Enviar Código de Redefinição",
+ "resetCodeDesc": "Digite seu nome de usuário para receber um código de redefinição de senha. O código será registrado nos logs do container docker.",
+ "resetCode": "Código de Redefinição",
+ "verifyCodeButton": "Verificar Código",
+ "enterResetCode": "Digite o código de 6 dígitos dos logs do container docker para o usuário:",
+ "goToLogin": "Ir para Login",
+ "newPassword": "Nova Senha",
+ "confirmNewPassword": "Confirmar Senha",
+ "enterNewPassword": "Digite sua nova senha para o usuário:",
+ "passwordResetSuccess": "Sucesso!",
+ "signUp": "Cadastrar",
+ "dataLossWarning": "Redefinir sua senha desta forma excluirá todos os seus hosts SSH salvos, credenciais e outros dados criptografados. Esta ação não pode ser desfeita. Use isso apenas se você esqueceu sua senha e não está logado.",
+ "sshAuthenticationRequired": "Autenticação SSH Necessária",
+ "sshNoKeyboardInteractive": "Autenticação Interativa por Teclado Indisponível",
+ "sshAuthenticationFailed": "Falha na Autenticação",
+ "sshAuthenticationTimeout": "Tempo Limite de Autenticação",
+ "sshNoKeyboardInteractiveDescription": "O servidor não suporta autenticação interativa por teclado. Por favor, forneça sua senha ou chave SSH.",
+ "sshAuthFailedDescription": "As credenciais fornecidas estavam incorretas. Por favor, tente novamente com credenciais válidas.",
+ "sshTimeoutDescription": "A tentativa de autenticação expirou. Por favor, tente novamente.",
+ "sshProvideCredentialsDescription": "Por favor, forneça suas credenciais SSH para conectar a este servidor.",
+ "sshPasswordDescription": "Digite a senha para esta conexão SSH.",
+ "sshKeyPasswordDescription": "Se sua chave SSH estiver criptografada, digite a senha aqui."
+ },
+ "errors": {
+ "notFound": "Página não encontrada",
+ "unauthorized": "Acesso não autorizado",
+ "forbidden": "Acesso proibido",
+ "serverError": "Erro no servidor",
+ "networkError": "Erro de rede",
+ "databaseConnection": "Não foi possível conectar ao banco de dados.",
+ "unknownError": "Erro desconhecido",
+ "loginFailed": "Falha no login",
+ "failedPasswordReset": "Falha ao iniciar redefinição de senha",
+ "failedVerifyCode": "Falha ao verificar código de redefinição",
+ "failedCompleteReset": "Falha ao completar redefinição de senha",
+ "invalidTotpCode": "Código TOTP inválido",
+ "failedOidcLogin": "Falha ao iniciar login OIDC",
+ "failedUserInfo": "Falha ao obter informações do usuário após login OIDC",
+ "oidcAuthFailed": "Falha na autenticação OIDC",
+ "noTokenReceived": "Nenhum token recebido do login",
+ "invalidAuthUrl": "URL de autorização inválida recebida do backend",
+ "invalidInput": "Entrada inválida",
+ "requiredField": "Este campo é obrigatório",
+ "minLength": "O comprimento mínimo é {{min}}",
+ "maxLength": "O comprimento máximo é {{max}}",
+ "invalidEmail": "Endereço de email inválido",
+ "passwordMismatch": "As senhas não correspondem",
+ "weakPassword": "A senha é muito fraca",
+ "usernameExists": "Nome de usuário já existe",
+ "emailExists": "Email já existe",
+ "loadFailed": "Falha ao carregar dados",
+ "saveError": "Falha ao salvar",
+ "sessionExpired": "Sessão expirada - por favor, faça login novamente"
+ },
+ "messages": {
+ "saveSuccess": "Salvo com sucesso",
+ "saveError": "Falha ao salvar",
+ "deleteSuccess": "Excluído com sucesso",
+ "deleteError": "Falha ao excluir",
+ "updateSuccess": "Atualizado com sucesso",
+ "updateError": "Falha ao atualizar",
+ "copySuccess": "Copiado para a área de transferência",
+ "copyError": "Falha ao copiar",
+ "copiedToClipboard": "{{item}} copiado para a área de transferência",
+ "connectionEstablished": "Conexão estabelecida",
+ "connectionClosed": "Conexão fechada",
+ "reconnecting": "Reconectando...",
+ "processing": "Processando...",
+ "pleaseWait": "Por favor, aguarde...",
+ "registrationDisabled": "O registro de novas contas está atualmente desativado pelo administrador. Por favor, faça login ou entre em contato com um administrador.",
+ "databaseConnected": "Conectado ao banco de dados com sucesso",
+ "databaseConnectionFailed": "Falha ao conectar ao servidor de banco de dados",
+ "checkServerConnection": "Por favor, verifique sua conexão com o servidor e tente novamente",
+ "resetCodeSent": "Código de redefinição enviado para os logs do Docker",
+ "codeVerified": "Código verificado com sucesso",
+ "passwordResetSuccess": "Senha redefinida com sucesso",
+ "loginSuccess": "Login realizado com sucesso",
+ "registrationSuccess": "Registro realizado com sucesso"
+ },
+ "profile": {
+ "title": "Perfil do Usuário",
+ "description": "Gerenciar suas configurações de conta e segurança",
+ "security": "Segurança",
+ "changePassword": "Alterar Senha",
+ "twoFactorAuth": "Autenticação de Dois Fatores",
+ "accountInfo": "Informações da Conta",
+ "role": "Função",
+ "admin": "Administrador",
+ "user": "Usuário",
+ "authMethod": "Método de Autenticação",
+ "local": "Local",
+ "external": "Externo (OIDC)",
+ "selectPreferredLanguage": "Selecione seu idioma preferido para a interface",
+ "currentPassword": "Senha Atual",
+ "passwordChangedSuccess": "Senha alterada com sucesso! Por favor, faça login novamente.",
+ "failedToChangePassword": "Falha ao alterar a senha. Por favor, verifique sua senha atual e tente novamente."
+ },
+ "user": {
+ "failedToLoadVersionInfo": "Falha ao carregar informações da versão"
+ },
+ "placeholders": {
+ "enterCode": "000000",
+ "ipAddress": "127.0.0.1",
+ "port": "22",
+ "maxRetries": "3",
+ "retryInterval": "10",
+ "language": "Idioma",
+ "username": "nome de usuário",
+ "hostname": "nome do host",
+ "folder": "pasta",
+ "password": "senha",
+ "keyPassword": "senha da chave",
+ "pastePrivateKey": "Cole sua chave privada aqui...",
+ "pastePublicKey": "Cole sua chave pública aqui...",
+ "credentialName": "Meu Servidor SSH",
+ "description": "Descrição da credencial SSH",
+ "searchCredentials": "Pesquisar credenciais por nome, usuário ou tags...",
+ "sshConfig": "configuração do endpoint ssh",
+ "homePath": "/home",
+ "clientId": "seu-client-id",
+ "clientSecret": "seu-client-secret",
+ "authUrl": "https://seu-provedor.com/application/o/authorize/",
+ "redirectUrl": "https://seu-provedor.com/application/o/termix/",
+ "tokenUrl": "https://seu-provedor.com/application/o/token/",
+ "userIdField": "sub",
+ "usernameField": "name",
+ "scopes": "openid email profile",
+ "userinfoUrl": "https://your-provider.com/application/o/userinfo/",
+ "enterUsername": "Digite o nome de usuário para tornar admin",
+ "searchHosts": "Procurar hosts por nome, usuário, IP, pasta, tags...",
+ "enterPassword": "Digite sua senha",
+ "totpCode": "Código TOTP de 6 dígitos",
+ "searchHostsAny": "Procurar hosts por qualquer informação...",
+ "confirmPassword": "Digite sua senha para confirmar",
+ "typeHere": "Digite aqui",
+ "fileName": "Digite o nome do arquivo (ex: exemplo.txt)",
+ "folderName": "Digite o nome da pasta",
+ "fullPath": "Digite o caminho completo do item",
+ "currentPath": "Digite o caminho atual do item",
+ "newName": "Digite o novo nome"
+ },
+ "leftSidebar": {
+ "failedToLoadHosts": "Falha ao carregar hosts",
+ "noFolder": "Sem Pasta",
+ "passwordRequired": "Senha é obrigatória",
+ "failedToDeleteAccount": "Falha ao excluir conta",
+ "failedToMakeUserAdmin": "Falha ao tornar usuário admin",
+ "userIsNowAdmin": "Usuário {{username}} agora é um admin",
+ "removeAdminConfirm": "Tem certeza que deseja remover o status de admin de {{username}}?",
+ "deleteUserConfirm": "Tem certeza que deseja excluir o usuário {{username}}? Esta ação não pode ser desfeita.",
+ "deleteAccount": "Excluir Conta",
+ "closeDeleteAccount": "Fechar Exclusão de Conta",
+ "deleteAccountWarning": "Esta ação não pode ser desfeita. Isso excluirá permanentemente sua conta e todos os dados associados.",
+ "deleteAccountWarningDetails": "Excluir sua conta removerá todos os seus dados, incluindo hosts SSH, configurações e preferências. Esta ação é irreversível.",
+ "deleteAccountWarningShort": "Esta ação é irreversível e excluirá permanentemente sua conta.",
+ "cannotDeleteAccount": "Não é Possível Excluir Conta",
+ "lastAdminWarning": "Você é o último usuário administrador. Você não pode excluir sua conta pois isso deixaria o sistema sem administradores. Por favor, torne outro usuário administrador primeiro, ou contate o suporte do sistema.",
+ "confirmPassword": "Confirmar Senha",
+ "deleting": "Excluindo...",
+ "cancel": "Cancelar"
+ },
+ "interface": {
+ "sidebar": "Barra lateral",
+ "toggleSidebar": "Alternar Barra lateral",
+ "close": "Fechar",
+ "online": "Online",
+ "offline": "Offline",
+ "maintenance": "Manutenção",
+ "degraded": "Degradado",
+ "noTunnelConnections": "Nenhuma conexão de túnel configurada",
+ "discord": "Discord",
+ "connectToSshForOperations": "Conecte-se ao SSH para usar operações de arquivos",
+ "uploadFile": "Enviar Arquivo",
+ "newFile": "Novo Arquivo",
+ "newFolder": "Nova Pasta",
+ "rename": "Renomear",
+ "deleteItem": "Excluir Item",
+ "createNewFile": "Criar Novo Arquivo",
+ "createNewFolder": "Criar Nova Pasta",
+ "deleteItem": "Excluir Item",
+ "renameItem": "Renomear Item",
+ "clickToSelectFile": "Clique para selecionar um arquivo",
+ "noSshHosts": "Sem Hosts SSH",
+ "sshHosts": "Hosts SSH",
+ "importSshHosts": "Importar Hosts SSH do JSON",
+ "clientId": "ID do Cliente",
+ "clientSecret": "Segredo do Cliente",
+ "error": "Erro",
+ "warning": "Aviso",
+ "deleteAccount": "Excluir Conta",
+ "closeDeleteAccount": "Fechar Exclusão de Conta",
+ "cannotDeleteAccount": "Não é Possível Excluir a Conta",
+ "confirmPassword": "Confirmar Senha",
+ "deleting": "Excluindo...",
+ "externalAuth": "Autenticação Externa (OIDC)",
+ "configureExternalProvider": "Configurar provedor de identidade externa para",
+ "waitingForRetry": "Aguardando nova tentativa",
+ "retryingConnection": "Tentando reconectar",
+ "resetSplitSizes": "Redefinir tamanhos divididos",
+ "sshManagerAlreadyOpen": "Gerenciador SSH já está aberto",
+ "disabledDuringSplitScreen": "Desativado durante tela dividida",
+ "unknown": "Desconhecido",
+ "connected": "Conectado",
+ "disconnected": "Desconectado",
+ "maxRetriesExhausted": "Número máximo de tentativas esgotado",
+ "endpointHostNotFound": "Host do endpoint não encontrado",
+ "administrator": "Administrador",
+ "user": "Usuário",
+ "external": "Externo",
+ "local": "Local",
+ "saving": "Salvando...",
+ "saveConfiguration": "Salvar Configuração",
+ "loading": "Carregando...",
+ "refresh": "Atualizar",
+ "adding": "Adicionando...",
+ "makeAdmin": "Tornar Administrador",
+ "verifying": "Verificando...",
+ "verifyAndEnable": "Verificar e Habilitar",
+ "secretKey": "Chave secreta",
+ "totpQrCode": "QR Code TOTP",
+ "passwordRequired": "Senha é obrigatória quando usar autenticação por senha",
+ "sshKeyRequired": "Chave Privada SSH é obrigatória quando usar autenticação por chave",
+ "keyTypeRequired": "Tipo de Chave é obrigatório quando usar autenticação por chave",
+ "validSshConfigRequired": "Deve selecionar uma configuração SSH válida da lista",
+ "updateHost": "Atualizar Host",
+ "addHost": "Adicionar Host",
+ "editHost": "Editar Host",
+ "pinConnection": "Fixar Conexão",
+ "authentication": "Autenticação",
+ "password": "Senha",
+ "key": "Chave",
+ "sshPrivateKey": "Chave Privada SSH",
+ "keyPassword": "Senha da Chave",
+ "keyType": "Tipo de Chave",
+ "enableTerminal": "Habilitar Terminal",
+ "enableTunnel": "Habilitar Túnel",
+ "enableFileManager": "Habilitar Gerenciador de Arquivos",
+ "defaultPath": "Caminho Padrão",
+ "tunnelConnections": "Conexões de Túnel",
+ "maxRetries": "Máximo de Tentativas",
+ "upload": "Enviar",
+ "updateKey": "Atualizar Chave",
+ "productionFolder": "Produção",
+ "databaseServer": "Servidor de Banco de Dados",
+ "developmentServer": "Servidor de Desenvolvimento",
+ "developmentFolder": "Desenvolvimento",
+ "webServerProduction": "Servidor Web - Produção",
+ "unknownError": "Erro desconhecido",
+ "failedToInitiatePasswordReset": "Falha ao iniciar redefinição de senha",
+ "failedToVerifyResetCode": "Falha ao verificar código de redefinição",
+ "failedToCompletePasswordReset": "Falha ao completar redefinição de senha",
+ "invalidTotpCode": "Código TOTP inválido",
+ "failedToStartOidcLogin": "Falha ao iniciar login OIDC",
+ "failedToGetUserInfoAfterOidc": "Falha ao obter informações do usuário após login OIDC",
+ "loginWithExternalProvider": "Login com provedor externo",
+ "loginWithExternal": "Login com Provedor Externo",
+ "sendResetCode": "Enviar Código de Redefinição",
+ "verifyCode": "Verificar Código",
+ "resetPassword": "Redefinir Senha",
+ "login": "Login",
+ "signUp": "Cadastrar",
+ "failedToUpdateOidcConfig": "Falha ao atualizar configuração OIDC",
+ "failedToMakeUserAdmin": "Falha ao tornar usuário administrador",
+ "failedToStartTotpSetup": "Falha ao iniciar configuração TOTP",
+ "invalidVerificationCode": "Código de verificação inválido",
+ "failedToDisableTotp": "Falha ao desabilitar TOTP",
+ "failedToGenerateBackupCodes": "Falha ao gerar códigos de backup"
+ },
+ "mobile": {
+ "selectHostToStart": "Selecione um host para iniciar sua sessão do terminal",
+ "limitedSupportMessage": "O suporte móvel do site ainda está em desenvolvimento. Use o aplicativo móvel para uma melhor experiência.",
+ "mobileAppInProgress": "Aplicativo móvel em desenvolvimento",
+ "mobileAppInProgressDesc": "Estamos trabalhando em um aplicativo móvel dedicado para proporcionar uma melhor experiência em dispositivos móveis.",
+ "viewMobileAppDocs": "Instalar Aplicativo Móvel",
+ "mobileAppDocumentation": "Documentação do Aplicativo Móvel"
+ }
+}
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
new file mode 100644
index 00000000..4a757ac7
--- /dev/null
+++ b/src/locales/ru/translation.json
@@ -0,0 +1,1591 @@
+{
+ "credentials": {
+ "credentialsViewer": "Просмотр учетных данных",
+ "manageYourSSHCredentials": "Безопасное управление вашими SSH-учетными данными",
+ "addCredential": "Добавить учетные данные",
+ "createCredential": "Создать учетные данные",
+ "editCredential": "Редактировать учетные данные",
+ "viewCredential": "Просмотреть учетные данные",
+ "duplicateCredential": "Дублировать учетные данные",
+ "deleteCredential": "Удалить учетные данные",
+ "updateCredential": "Обновить учетные данные",
+ "credentialName": "Название учетных данных",
+ "credentialDescription": "Описание",
+ "username": "Имя пользователя",
+ "searchCredentials": "Поиск учетных данных...",
+ "selectFolder": "Выбрать папку",
+ "selectAuthType": "Выбрать тип аутентификации",
+ "allFolders": "Все папки",
+ "allAuthTypes": "Все типы аутентификации",
+ "uncategorized": "Без категории",
+ "totalCredentials": "Всего",
+ "keyBased": "На основе ключа",
+ "passwordBased": "На основе пароля",
+ "folders": "Папки",
+ "noCredentialsMatchFilters": "Нет учетных данных, соответствующих вашим фильтрам",
+ "noCredentialsYet": "Учетные данные еще не созданы",
+ "createFirstCredential": "Создайте свои первые учетные данные",
+ "failedToFetchCredentials": "Не удалось загрузить учетные данные",
+ "credentialDeletedSuccessfully": "Учетные данные успешно удалены",
+ "failedToDeleteCredential": "Не удалось удалить учетные данные",
+ "confirmDeleteCredential": "Вы уверены, что хотите удалить учетные данные \"{{name}}\"?",
+ "credentialCreatedSuccessfully": "Учетные данные успешно созданы",
+ "credentialUpdatedSuccessfully": "Учетные данные успешно обновлены",
+ "failedToSaveCredential": "Не удалось сохранить учетные данные",
+ "failedToFetchCredentialDetails": "Не удалось загрузить детали учетных данных",
+ "failedToFetchHostsUsing": "Не удалось загрузить хосты, использующие эти учетные данные",
+ "loadingCredentials": "Загрузка учетных данных...",
+ "retry": "Повторить",
+ "noCredentials": "Нет учетных данных",
+ "noCredentialsMessage": "Вы еще не добавили учетные данные. Нажмите \"Добавить учетные данные\", чтобы начать.",
+ "sshCredentials": "SSH-учетные данные",
+ "credentialsCount": "{{count}} учетных данных",
+ "refresh": "Обновить",
+ "passwordRequired": "Требуется пароль",
+ "sshKeyRequired": "Требуется SSH-ключ",
+ "credentialAddedSuccessfully": "Учетные данные \"{{name}}\" успешно добавлены",
+ "general": "Общее",
+ "description": "Описание",
+ "folder": "Папка",
+ "tags": "Теги",
+ "addTagsSpaceToAdd": "Добавить теги (нажмите пробел для добавления)",
+ "password": "Пароль",
+ "key": "Ключ",
+ "sshPrivateKey": "Приватный SSH-ключ",
+ "upload": "Загрузить",
+ "updateKey": "Обновить ключ",
+ "keyPassword": "Пароль ключа (опционально)",
+ "keyType": "Тип ключа",
+ "keyTypeRSA": "RSA",
+ "keyTypeECDSA": "ECDSA",
+ "keyTypeEd25519": "Ed25519",
+ "updateCredential": "Обновить учетные данные",
+ "basicInfo": "Основная информация",
+ "authentication": "Аутентификация",
+ "organization": "Организация",
+ "basicInformation": "Основная информация",
+ "basicInformationDescription": "Введите основную информацию для этих учетных данных",
+ "authenticationMethod": "Метод аутентификации",
+ "authenticationMethodDescription": "Выберите способ аутентификации на SSH-серверах",
+ "organizationDescription": "Организуйте ваши учетные данные с помощью папок и тегов",
+ "enterCredentialName": "Введите название учетных данных",
+ "enterCredentialDescription": "Введите описание (опционально)",
+ "enterUsername": "Введите имя пользователя",
+ "nameIsRequired": "Требуется название учетных данных",
+ "usernameIsRequired": "Требуется имя пользователя",
+ "authenticationType": "Тип аутентификации",
+ "passwordAuthDescription": "Использовать аутентификацию по паролю",
+ "sshKeyAuthDescription": "Использовать аутентификацию по SSH-ключу",
+ "passwordIsRequired": "Требуется пароль",
+ "sshKeyIsRequired": "Требуется SSH-ключ",
+ "sshKeyType": "Тип SSH-ключа",
+ "privateKey": "Приватный ключ",
+ "enterPassword": "Введите пароль",
+ "enterPrivateKey": "Введите приватный ключ",
+ "keyPassphrase": "Парольная фраза ключа",
+ "enterKeyPassphrase": "Введите парольную фразу ключа (опционально)",
+ "keyPassphraseOptional": "Опционально: оставьте пустым, если у ключа нет парольной фразы",
+ "leaveEmptyToKeepCurrent": "Оставьте пустым, чтобы сохранить текущее значение",
+ "uploadKeyFile": "Загрузить файл ключа",
+ "generateKeyPairButton": "Сгенерировать пару ключей",
+ "generateKeyPair": "Сгенерировать пару ключей",
+ "generateKeyPairDescription": "Сгенерировать новую пару SSH-ключей. Если вы хотите защитить ключ парольной фразой, сначала введите ее в поле Пароль ключа ниже.",
+ "deploySSHKey": "Развернуть SSH-ключ",
+ "deploySSHKeyDescription": "Развернуть публичный ключ на целевом сервере",
+ "sourceCredential": "Исходные учетные данные",
+ "targetHost": "Целевой хост",
+ "deploymentProcess": "Процесс развертывания",
+ "deploymentProcessDescription": "Это безопасно добавит публичный ключ в файл ~/.ssh/authorized_keys целевого хоста без перезаписи существующих ключей. Операция обратима.",
+ "chooseHostToDeploy": "Выберите хост для развертывания...",
+ "deploying": "Развертывание...",
+ "name": "Имя",
+ "noHostsAvailable": "Нет доступных хостов",
+ "noHostsMatchSearch": "Нет хостов, соответствующих вашему запросу",
+ "sshKeyGenerationNotImplemented": "Функция генерации SSH-ключей скоро будет доступна",
+ "connectionTestingNotImplemented": "Функция тестирования подключения скоро будет доступна",
+ "testConnection": "Тестировать подключение",
+ "selectOrCreateFolder": "Выбрать или создать папку",
+ "noFolder": "Без папки",
+ "orCreateNewFolder": "Или создать новую папку",
+ "addTag": "Добавить тег",
+ "saving": "Сохранение...",
+ "overview": "Обзор",
+ "security": "Безопасность",
+ "usage": "Использование",
+ "securityDetails": "Детали безопасности",
+ "securityDetailsDescription": "Просмотр зашифрованной информации учетных данных",
+ "credentialSecured": "Учетные данные защищены",
+ "credentialSecuredDescription": "Все конфиденциальные данные зашифрованы с помощью AES-256",
+ "passwordAuthentication": "Аутентификация по паролю",
+ "keyAuthentication": "Аутентификация по ключу",
+ "keyType": "Тип ключа",
+ "securityReminder": "Напоминание о безопасности",
+ "securityReminderText": "Никогда не передавайте ваши учетные данные. Все данные зашифрованы при хранении.",
+ "hostsUsingCredential": "Хосты, использующие эти учетные данные",
+ "noHostsUsingCredential": "В настоящее время эти учетные данные не используются ни на одном хосте",
+ "timesUsed": "Количество использований",
+ "lastUsed": "Последнее использование",
+ "connectedHosts": "Подключенные хосты",
+ "created": "Создано",
+ "lastModified": "Последнее изменение",
+ "usageStatistics": "Статистика использования",
+ "copiedToClipboard": "{{field}} скопировано в буфер обмена",
+ "failedToCopy": "Не удалось скопировать в буфер обмена",
+ "sshKey": "SSH-ключ",
+ "createCredentialDescription": "Создать новые SSH-учетные данные для безопасного доступа",
+ "editCredentialDescription": "Обновить информацию об учетных данных",
+ "listView": "Список",
+ "folderView": "Папки",
+ "unknownCredential": "Неизвестно",
+ "confirmRemoveFromFolder": "Вы уверены, что хотите удалить \"{{name}}\" из папки \"{{folder}}\"? Учетные данные будут перемещены в \"Без категории\".",
+ "removedFromFolder": "Учетные данные \"{{name}}\" успешно удалены из папки",
+ "failedToRemoveFromFolder": "Не удалось удалить учетные данные из папки",
+ "folderRenamed": "Папка \"{{oldName}}\" успешно переименована в \"{{newName}}\"",
+ "failedToRenameFolder": "Не удалось переименовать папку",
+ "movedToFolder": "Учетные данные \"{{name}}\" успешно перемещены в \"{{folder}}\"",
+ "failedToMoveToFolder": "Не удалось переместить учетные данные в папку",
+ "sshPublicKey": "Публичный SSH-ключ",
+ "publicKeyNote": "Публичный ключ опционален, но рекомендуется для проверки ключа",
+ "publicKeyUploaded": "Публичный ключ загружен",
+ "uploadPublicKey": "Загрузить публичный ключ",
+ "uploadPrivateKeyFile": "Загрузить файл приватного ключа",
+ "uploadPublicKeyFile": "Загрузить файл публичного ключа",
+ "privateKeyRequiredForGeneration": "Для генерации публичного ключа требуется приватный ключ",
+ "failedToGeneratePublicKey": "Не удалось сгенерировать публичный ключ",
+ "generatePublicKey": "Сгенерировать из приватного ключа",
+ "publicKeyGeneratedSuccessfully": "Публичный ключ успешно сгенерирован",
+ "detectedKeyType": "Обнаруженный тип ключа",
+ "detectingKeyType": "определение...",
+ "optional": "Опционально",
+ "generateKeyPairNew": "Сгенерировать новую пару ключей",
+ "generateEd25519": "Сгенерировать Ed25519",
+ "generateECDSA": "Сгенерировать ECDSA",
+ "generateRSA": "Сгенерировать RSA",
+ "keyPairGeneratedSuccessfully": "Пара ключей {{keyType}} успешно сгенерирована",
+ "failedToGenerateKeyPair": "Не удалось сгенерировать пару ключей",
+ "generateKeyPairNote": "Сгенерировать новую пару SSH-ключей напрямую. Это заменит любые существующие ключи в форме.",
+ "invalidKey": "Неверный ключ",
+ "detectionError": "Ошибка определения",
+ "unknown": "Неизвестно"
+ },
+ "dragIndicator": {
+ "error": "Ошибка: {{error}}",
+ "dragging": "Перетаскивание {{fileName}}",
+ "preparing": "Подготовка {{fileName}}",
+ "readySingle": "Готово к скачиванию {{fileName}}",
+ "readyMultiple": "Готово к скачиванию {{count}} файлов",
+ "batchDrag": "Перетащите {{count}} файлов на рабочий стол",
+ "dragToDesktop": "Перетащите на рабочий стол",
+ "canDragAnywhere": "Вы можете перетаскивать файлы в любое место на рабочем столе"
+ },
+ "sshTools": {
+ "title": "SSH-инструменты",
+ "closeTools": "Закрыть SSH-инструменты",
+ "keyRecording": "Запись клавиш",
+ "startKeyRecording": "Начать запись клавиш",
+ "stopKeyRecording": "Остановить запись клавиш",
+ "selectTerminals": "Выберите терминалы:",
+ "typeCommands": "Введите команды (поддерживаются все клавиши):",
+ "commandsWillBeSent": "Команды будут отправлены в {{count}} выбранных терминалов.",
+ "settings": "Настройки",
+ "enableRightClickCopyPaste": "Включить копирование/вставку по правому клику",
+ "shareIdeas": "Есть идеи, что должно быть следующим для SSH-инструментов? Поделитесь ими на"
+ },
+ "snippets": {
+ "title": "Сниппеты",
+ "new": "Новый сниппет",
+ "create": "Создать сниппет",
+ "edit": "Редактировать сниппет",
+ "run": "Выполнить",
+ "empty": "Сниппетов пока нет",
+ "emptyHint": "Создайте сниппет для сохранения часто используемых команд",
+ "name": "Название",
+ "description": "Описание",
+ "content": "Команда",
+ "namePlaceholder": "например, Перезапуск Nginx",
+ "descriptionPlaceholder": "Опциональное описание",
+ "contentPlaceholder": "например, sudo systemctl restart nginx",
+ "nameRequired": "Требуется название",
+ "contentRequired": "Требуется команда",
+ "createDescription": "Создать новый сниппет команды для быстрого выполнения",
+ "editDescription": "Редактировать этот сниппет команды",
+ "deleteConfirmTitle": "Удалить сниппет",
+ "deleteConfirmDescription": "Вы уверены, что хотите удалить \"{{name}}\"?",
+ "createSuccess": "Сниппет успешно создан",
+ "updateSuccess": "Сниппет успешно обновлен",
+ "deleteSuccess": "Сниппет успешно удален",
+ "createFailed": "Не удалось создать сниппет",
+ "updateFailed": "Не удалось обновить сниппет",
+ "deleteFailed": "Не удалось удалить сниппет",
+ "failedToFetch": "Не удалось загрузить сниппеты",
+ "executeSuccess": "Выполняется: {{name}}",
+ "copySuccess": "Сниппет \"{{name}}\" скопирован в буфер обмена",
+ "runTooltip": "Выполнить этот сниппет в терминале",
+ "copyTooltip": "Скопировать сниппет в буфер обмена",
+ "editTooltip": "Редактировать этот сниппет",
+ "deleteTooltip": "Удалить этот сниппет"
+ },
+ "homepage": {
+ "loggedInTitle": "Вы вошли в систему!",
+ "loggedInMessage": "Вы вошли в систему! Используйте боковую панель для доступа ко всем доступным инструментам. Чтобы начать, создайте SSH-хост в разделе SSH-менеджера. После создания вы можете подключиться к этому хосту, используя другие приложения на боковой панели.",
+ "failedToLoadAlerts": "Не удалось загрузить оповещения",
+ "failedToDismissAlert": "Не удалось закрыть оповещение"
+ },
+ "serverConfig": {
+ "title": "Конфигурация сервера",
+ "description": "Настройте URL сервера Termix для подключения к вашим серверным службам",
+ "serverUrl": "URL сервера",
+ "enterServerUrl": "Пожалуйста, введите URL сервера",
+ "testConnectionFirst": "Пожалуйста, сначала проверьте подключение",
+ "connectionSuccess": "Подключение успешно!",
+ "connectionFailed": "Подключение не удалось",
+ "connectionError": "Произошла ошибка подключения",
+ "connected": "Подключено",
+ "disconnected": "Отключено",
+ "configSaved": "Конфигурация успешно сохранена",
+ "saveFailed": "Не удалось сохранить конфигурацию",
+ "saveError": "Ошибка сохранения конфигурации",
+ "saving": "Сохранение...",
+ "saveConfig": "Сохранить конфигурацию",
+ "helpText": "Введите URL, где работает ваш сервер Termix (например, http://localhost:30001 или https://your-server.com)"
+ },
+ "versionCheck": {
+ "error": "Ошибка проверки версии",
+ "checkFailed": "Не удалось проверить наличие обновлений",
+ "upToDate": "Приложение обновлено",
+ "currentVersion": "Вы используете версию {{version}}",
+ "updateAvailable": "Доступно обновление",
+ "newVersionAvailable": "Доступна новая версия! Вы используете {{current}}, но доступна {{latest}}.",
+ "releasedOn": "Выпущена {{date}}",
+ "downloadUpdate": "Скачать обновление",
+ "dismiss": "Закрыть",
+ "checking": "Проверка обновлений...",
+ "checkUpdates": "Проверить обновления",
+ "checkingUpdates": "Проверка обновлений...",
+ "refresh": "Обновить",
+ "updateRequired": "Требуется обновление",
+ "updateDismissed": "Уведомление об обновлении закрыто",
+ "noUpdatesFound": "Обновления не найдены"
+ },
+ "common": {
+ "close": "Закрыть",
+ "minimize": "Свернуть",
+ "online": "В сети",
+ "offline": "Не в сети",
+ "continue": "Продолжить",
+ "maintenance": "Обслуживание",
+ "degraded": "Снижена производительность",
+ "discord": "Discord",
+ "error": "Ошибка",
+ "warning": "Предупреждение",
+ "info": "Информация",
+ "success": "Успех",
+ "loading": "Загрузка",
+ "required": "Обязательно",
+ "optional": "Опционально",
+ "clear": "Очистить",
+ "toggleSidebar": "Переключить боковую панель",
+ "sidebar": "Боковая панель",
+ "home": "Главная",
+ "expired": "Истек",
+ "expiresToday": "Истекает сегодня",
+ "expiresTomorrow": "Истекает завтра",
+ "expiresInDays": "Истекает через {{days}} дней",
+ "updateAvailable": "Доступно обновление",
+ "sshPath": "SSH-путь",
+ "localPath": "Локальный путь",
+ "loading": "Загрузка...",
+ "noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста",
+ "noReleases": "Нет выпусков",
+ "updatesAndReleases": "Обновления и выпуски",
+ "newVersionAvailable": "Доступна новая версия ({{version}}).",
+ "failedToFetchUpdateInfo": "Не удалось загрузить информацию об обновлениях",
+ "preRelease": "Предварительный выпуск",
+ "loginFailed": "Ошибка входа",
+ "noReleasesFound": "Выпуски не найдены.",
+ "yourBackupCodes": "Ваши резервные коды",
+ "sendResetCode": "Отправить код сброса",
+ "verifyCode": "Проверить код",
+ "resetPassword": "Сбросить пароль",
+ "resetCode": "Код сброса",
+ "newPassword": "Новый пароль",
+ "sshPath": "SSH-путь",
+ "localPath": "Локальный путь",
+ "folder": "Папка",
+ "file": "Файл",
+ "renamedSuccessfully": "успешно переименован",
+ "deletedSuccessfully": "успешно удален",
+ "noAuthCredentials": "Нет учетных данных аутентификации для этого SSH-хоста",
+ "noTunnelConnections": "Нет настроенных туннельных подключений",
+ "sshTools": "SSH-инструменты",
+ "english": "Английский",
+ "russia": "Русский",
+ "chinese": "Китайский",
+ "german": "Немецкий",
+ "cancel": "Отмена",
+ "username": "Имя пользователя",
+ "name": "Имя",
+ "login": "Войти",
+ "logout": "Выйти",
+ "register": "Зарегистрироваться",
+ "username": "Имя пользователя",
+ "password": "Пароль",
+ "version": "Версия",
+ "confirmPassword": "Подтвердите пароль",
+ "back": "Назад",
+ "email": "Email",
+ "submit": "Отправить",
+ "cancel": "Отмена",
+ "change": "Изменить",
+ "save": "Сохранить",
+ "delete": "Удалить",
+ "edit": "Редактировать",
+ "add": "Добавить",
+ "search": "Поиск",
+ "loading": "Загрузка...",
+ "error": "Ошибка",
+ "success": "Успех",
+ "warning": "Предупреждение",
+ "info": "Информация",
+ "confirm": "Подтвердить",
+ "yes": "Да",
+ "no": "Нет",
+ "ok": "OK",
+ "close": "Закрыть",
+ "enabled": "Включено",
+ "disabled": "Отключено",
+ "important": "Важно",
+ "notEnabled": "Не включено",
+ "settingUp": "Настройка...",
+ "back": "Назад",
+ "next": "Далее",
+ "previous": "Назад",
+ "refresh": "Обновить",
+ "settings": "Настройки",
+ "profile": "Профиль",
+ "help": "Помощь",
+ "about": "О программе",
+ "language": "Язык",
+ "autoDetect": "Автоопределение",
+ "changeAccountPassword": "Изменить пароль вашей учетной записи",
+ "passwordResetTitle": "Сброс пароля",
+ "passwordResetDescription": "Вы собираетесь сбросить пароль. Это приведет к выходу из всех активных сеансов.",
+ "enterSixDigitCode": "Введите 6-значный код из логов docker-контейнера для пользователя:",
+ "enterNewPassword": "Введите новый пароль для пользователя:",
+ "passwordsDoNotMatch": "Пароли не совпадают",
+ "passwordMinLength": "Пароль должен содержать не менее 6 символов",
+ "passwordResetSuccess": "Пароль успешно сброшен! Теперь вы можете войти с новым паролем.",
+ "failedToInitiatePasswordReset": "Не удалось инициировать сброс пароля",
+ "failedToVerifyResetCode": "Не удалось проверить код сброса",
+ "failedToCompletePasswordReset": "Не удалось завершить сброс пароля",
+ "documentation": "Документация",
+ "retry": "Повторить",
+ "checking": "Проверка...",
+ "checkingDatabase": "Проверка подключения к базе данных..."
+ },
+ "nav": {
+ "home": "Главная",
+ "hosts": "Хосты",
+ "credentials": "Учетные данные",
+ "terminal": "Терминал",
+ "tunnels": "Туннели",
+ "fileManager": "Файловый менеджер",
+ "serverStats": "Статистика сервера",
+ "admin": "Администрирование",
+ "userProfile": "Профиль пользователя",
+ "tools": "Инструменты",
+ "snippets": "Сниппеты",
+ "newTab": "Новая вкладка",
+ "splitScreen": "Разделить экран",
+ "closeTab": "Закрыть вкладку",
+ "sshManager": "SSH-менеджер",
+ "hostManager": "Менеджер хостов",
+ "cannotSplitTab": "Невозможно разделить эту вкладку",
+ "tabNavigation": "Навигация по вкладкам"
+ },
+ "admin": {
+ "title": "Настройки администратора",
+ "oidc": "OIDC",
+ "users": "Пользователи",
+ "userManagement": "Управление пользователями",
+ "makeAdmin": "Сделать администратором",
+ "removeAdmin": "Убрать администратора",
+ "deleteUser": "Удалить пользователя",
+ "allowRegistration": "Разрешить регистрацию",
+ "oidcSettings": "Настройки OIDC",
+ "clientId": "Client ID",
+ "clientSecret": "Client Secret",
+ "issuerUrl": "Issuer URL",
+ "authorizationUrl": "Authorization URL",
+ "tokenUrl": "Token URL",
+ "updateSettings": "Обновить настройки",
+ "confirmDelete": "Вы уверены, что хотите удалить этого пользователя?",
+ "confirmMakeAdmin": "Вы уверены, что хотите сделать этого пользователя администратором?",
+ "confirmRemoveAdmin": "Вы уверены, что хотите убрать права администратора у этого пользователя?",
+ "externalAuthentication": "Внешняя аутентификация (OIDC)",
+ "configureExternalProvider": "Настройте внешнего провайдера идентификации для аутентификации OIDC/OAuth2.",
+ "userIdentifierPath": "Путь к идентификатору пользователя",
+ "displayNamePath": "Путь к отображаемому имени",
+ "scopes": "Области действия",
+ "saving": "Сохранение...",
+ "saveConfiguration": "Сохранить конфигурацию",
+ "reset": "Сбросить",
+ "success": "Успех",
+ "loading": "Загрузка...",
+ "refresh": "Обновить",
+ "loadingUsers": "Загрузка пользователей...",
+ "username": "Имя пользователя",
+ "type": "Тип",
+ "actions": "Действия",
+ "external": "Внешний",
+ "local": "Локальный",
+ "adminManagement": "Управление администраторами",
+ "makeUserAdmin": "Сделать пользователя администратором",
+ "adding": "Добавление...",
+ "currentAdmins": "Текущие администраторы",
+ "adminBadge": "Администратор",
+ "removeAdminButton": "Убрать администратора",
+ "general": "Общее",
+ "userRegistration": "Регистрация пользователей",
+ "allowNewAccountRegistration": "Разрешить регистрацию новых учетных записей",
+ "allowPasswordLogin": "Разрешить вход по имени пользователя/паролю",
+ "missingRequiredFields": "Отсутствуют обязательные поля: {{fields}}",
+ "oidcConfigurationUpdated": "Конфигурация OIDC успешно обновлена!",
+ "failedToFetchOidcConfig": "Не удалось загрузить конфигурацию OIDC",
+ "failedToFetchRegistrationStatus": "Не удалось загрузить статус регистрации",
+ "failedToFetchPasswordLoginStatus": "Не удалось загрузить статус входа по паролю",
+ "failedToFetchUsers": "Не удалось загрузить пользователей",
+ "oidcConfigurationDisabled": "Конфигурация OIDC успешно отключена!",
+ "failedToUpdateOidcConfig": "Не удалось обновить конфигурацию OIDC",
+ "failedToDisableOidcConfig": "Не удалось отключить конфигурацию OIDC",
+ "enterUsernameToMakeAdmin": "Введите имя пользователя, чтобы сделать администратором",
+ "userIsNowAdmin": "Пользователь {{username}} теперь администратор",
+ "failedToMakeUserAdmin": "Не удалось сделать пользователя администратором",
+ "removeAdminStatus": "Убрать статус администратора у {{username}}?",
+ "adminStatusRemoved": "Статус администратора убран у {{username}}",
+ "failedToRemoveAdminStatus": "Не удалось убрать статус администратора",
+ "deleteUser": "Удалить пользователя {{username}}? Это нельзя отменить.",
+ "userDeletedSuccessfully": "Пользователь {{username}} успешно удален",
+ "failedToDeleteUser": "Не удалось удалить пользователя",
+ "overrideUserInfoUrl": "Переопределить User Info URL (не требуется)",
+ "databaseSecurity": "Безопасность базы данных",
+ "encryptionStatus": "Статус шифрования",
+ "encryptionEnabled": "Шифрование включено",
+ "enabled": "Включено",
+ "disabled": "Отключено",
+ "keyId": "ID ключа",
+ "created": "Создано",
+ "migrationStatus": "Статус миграции",
+ "migrationCompleted": "Миграция завершена",
+ "migrationRequired": "Требуется миграция",
+ "deviceProtectedMasterKey": "Мастер-ключ, защищенный средой",
+ "legacyKeyStorage": "Устаревшее хранилище ключей",
+ "masterKeyEncryptedWithDeviceFingerprint": "Мастер-ключ зашифрован с помощью отпечатка среды (защита KEK активна)",
+ "keyNotProtectedByDeviceBinding": "Ключ не защищен привязкой к среде (рекомендуется обновление)",
+ "valid": "Действителен",
+ "initializeDatabaseEncryption": "Инициализировать шифрование базы данных",
+ "enableAes256EncryptionWithDeviceBinding": "Включить AES-256 шифрование с защитой мастер-ключа, привязанного к среде. Это создает безопасность корпоративного уровня для SSH-ключей, паролей и токенов аутентификации.",
+ "featuresEnabled": "Включенные функции:",
+ "aes256GcmAuthenticatedEncryption": "Аутентифицированное шифрование AES-256-GCM",
+ "deviceFingerprintMasterKeyProtection": "Защита мастер-ключа отпечатком среды (KEK)",
+ "pbkdf2KeyDerivation": "Производство ключей PBKDF2 с 100K итерациями",
+ "automaticKeyManagement": "Автоматическое управление ключами и их ротация",
+ "initializing": "Инициализация...",
+ "initializeEnterpriseEncryption": "Инициализировать корпоративное шифрование",
+ "migrateExistingData": "Мигрировать существующие данные",
+ "encryptExistingUnprotectedData": "Зашифровать существующие незащищенные данные в вашей базе данных. Этот процесс безопасен и создает автоматические резервные копии.",
+ "testMigrationDryRun": "Проверить совместимость шифрования",
+ "migrating": "Миграция...",
+ "migrateData": "Мигрировать данные",
+ "securityInformation": "Информация о безопасности",
+ "sshPrivateKeysEncryptedWithAes256": "SSH-приватные ключи и пароли зашифрованы с помощью AES-256-GCM",
+ "userAuthTokensProtected": "Токены аутентификации пользователей и секреты 2FA защищены",
+ "masterKeysProtectedByDeviceFingerprint": "Мастер-ключи шифрования защищены отпечатком устройства (KEK)",
+ "keysBoundToServerInstance": "Ключи привязаны к текущей серверной среде (мигрируемы через переменные окружения)",
+ "pbkdf2HkdfKeyDerivation": "Производство ключей PBKDF2 + HKDF с 100K итерациями",
+ "backwardCompatibleMigration": "Все данные остаются обратно совместимыми во время миграции",
+ "enterpriseGradeSecurityActive": "Безопасность корпоративного уровня активна",
+ "masterKeysProtectedByDeviceBinding": "Ваши мастер-ключи шифрования защищены отпечатком среды. Это использует имя хоста сервера, пути и другую информацию о среде для генерации ключей защиты. Для миграции серверов установите переменную окружения DB_ENCRYPTION_KEY на новом сервере.",
+ "important": "Важно",
+ "keepEncryptionKeysSecure": "Обеспечьте безопасность данных: регулярно создавайте резервные копии файлов базы данных и конфигурации сервера. Для миграции на новый сервер установите переменную окружения DB_ENCRYPTION_KEY в новой среде или сохраните то же имя хоста и структуру каталогов.",
+ "loadingEncryptionStatus": "Загрузка статуса шифрования...",
+ "testMigrationDescription": "Проверить, что существующие данные могут быть безопасно мигрированы в зашифрованный формат без фактического изменения каких-либо данных",
+ "serverMigrationGuide": "Руководство по миграции сервера",
+ "migrationInstructions": "Для миграции зашифрованных данных на новый сервер: 1) Создайте резервную копию файлов базы данных, 2) Установите переменную окружения DB_ENCRYPTION_KEY=\"your-key\" на новом сервере, 3) Восстановите файлы базы данных",
+ "environmentProtection": "Защита среды",
+ "environmentProtectionDesc": "Защищает ключи шифрования на основе информации о серверной среде (имя хоста, пути и т.д.), мигрируемы через переменные окружения",
+ "verificationCompleted": "Проверка совместимости завершена - данные не изменялись",
+ "verificationInProgress": "Проверка завершена",
+ "dataMigrationCompleted": "Миграция данных успешно завершена!",
+ "migrationCompleted": "Миграция завершена",
+ "verificationFailed": "Проверка совместимости не удалась",
+ "migrationFailed": "Миграция не удалась",
+ "runningVerification": "Выполняется проверка совместимости...",
+ "startingMigration": "Начало миграции...",
+ "hardwareFingerprintSecurity": "Безопасность отпечатка оборудования",
+ "hardwareBoundEncryption": "Активно шифрование, привязанное к оборудованию",
+ "masterKeysNowProtectedByHardwareFingerprint": "Мастер-ключи теперь защищены реальным отпечатком оборудования вместо переменных окружения",
+ "cpuSerialNumberDetection": "Обнаружение серийного номера CPU",
+ "motherboardUuidIdentification": "Идентификация UUID материнской платы",
+ "diskSerialNumberVerification": "Проверка серийного номера диска",
+ "biosSerialNumberCheck": "Проверка серийного номера BIOS",
+ "stableMacAddressFiltering": "Фильтрация стабильных MAC-адресов",
+ "databaseFileEncryption": "Шифрование файлов базы данных",
+ "dualLayerProtection": "Активна двухуровневая защита",
+ "bothFieldAndFileEncryptionActive": "Теперь активны как полевое, так и файловое шифрование для максимальной безопасности",
+ "fieldLevelAes256Encryption": "Полевое шифрование AES-256 для конфиденциальных данных",
+ "fileLevelDatabaseEncryption": "Файловое шифрование базы данных с привязкой к оборудованию",
+ "hardwareBoundFileKeys": "Ключи шифрования файлов, привязанные к оборудованию",
+ "automaticEncryptedBackups": "Автоматическое создание зашифрованных резервных копий",
+ "createEncryptedBackup": "Создать зашифрованную резервную копию",
+ "creatingBackup": "Создание резервной копии...",
+ "backupCreated": "Резервная копия создана",
+ "encryptedBackupCreatedSuccessfully": "Зашифрованная резервная копия успешно создана",
+ "backupCreationFailed": "Не удалось создать резервную копию",
+ "databaseMigration": "Миграция базы данных",
+ "exportForMigration": "Экспорт для миграции",
+ "exportDatabaseForHardwareMigration": "Экспортировать базу данных как файл SQLite с расшифрованными данными для миграции на новое оборудование",
+ "exportDatabase": "Экспортировать базу данных SQLite",
+ "exporting": "Экспорт...",
+ "exportCreated": "Экспорт SQLite создан",
+ "exportContainsDecryptedData": "Экспорт SQLite содержит расшифрованные данные - храните безопасно!",
+ "databaseExportedSuccessfully": "База данных SQLite успешно экспортирована",
+ "databaseExportFailed": "Не удалось экспортировать базу данных SQLite",
+ "importFromMigration": "Импорт из миграции",
+ "importDatabaseFromAnotherSystem": "Импортировать базу данных SQLite из другой системы или оборудования",
+ "importDatabase": "Импортировать базу данных SQLite",
+ "importing": "Импорт...",
+ "selectedFile": "Выбранный файл SQLite",
+ "importWillReplaceExistingData": "Импорт SQLite заменит существующие данные - рекомендуется резервное копирование!",
+ "pleaseSelectImportFile": "Пожалуйста, выберите файл для импорта SQLite",
+ "databaseImportedSuccessfully": "База данных SQLite успешно импортирована",
+ "databaseImportFailed": "Не удалось импортировать базу данных SQLite",
+ "manageEncryptionAndBackups": "Управление ключами шифрования, безопасностью базы данных и операциями резервного копирования",
+ "activeSecurityFeatures": "Текущие активные меры безопасности и защиты",
+ "deviceBindingTechnology": "Продвинутая технология защиты ключей на основе оборудования",
+ "backupAndRecovery": "Безопасное создание резервных копий и восстановление базы данных",
+ "crossSystemDataTransfer": "Экспорт и импорт баз данных между разными системами",
+ "noMigrationNeeded": "Миграция не требуется",
+ "encryptionKey": "Ключ шифрования",
+ "keyProtection": "Защита ключа",
+ "active": "Активно",
+ "legacy": "Устаревшее",
+ "dataStatus": "Статус данных",
+ "encrypted": "Зашифровано",
+ "needsMigration": "Требуется миграция",
+ "ready": "Готово",
+ "initializeEncryption": "Инициализировать шифрование",
+ "initialize": "Инициализировать",
+ "test": "Тест",
+ "migrate": "Мигрировать",
+ "backup": "Резервная копия",
+ "createBackup": "Создать резервную копию",
+ "exportImport": "Экспорт/Импорт",
+ "export": "Экспорт",
+ "import": "Импорт",
+ "passwordRequired": "Требуется пароль",
+ "confirmExport": "Подтвердить экспорт",
+ "exportDescription": "Экспортировать SSH-хосты и учетные данные как файл SQLite",
+ "importDescription": "Импортировать файл SQLite с инкрементным слиянием (пропускает дубликаты)",
+ "criticalWarning": "Критическое предупреждение",
+ "cannotDisablePasswordLoginWithoutOIDC": "Невозможно отключить вход по паролю без настройки OIDC! Вы должны настроить аутентификацию OIDC перед отключением входа по паролю, иначе вы потеряете доступ к Termix.",
+ "confirmDisablePasswordLogin": "Вы уверены, что хотите отключить вход по паролю? Убедитесь, что OIDC правильно настроен и работает, прежде чем продолжить, иначе вы потеряете доступ к вашему экземпляру Termix.",
+ "passwordLoginDisabled": "Вход по паролю успешно отключен",
+ "passwordLoginAndRegistrationDisabled": "Вход по паролю и регистрация новых учетных записей успешно отключены",
+ "requiresPasswordLogin": "Требуется включенный вход по паролю",
+ "passwordLoginDisabledWarning": "Вход по паролю отключен. Убедитесь, что OIDC правильно настроен, иначе вы не сможете войти в Termix.",
+ "oidcRequiredWarning": "КРИТИЧЕСКИ: Вход по паролю отключен. Если вы сбросите или неправильно настроите OIDC, вы потеряете весь доступ к Termix и заблокируете свой экземпляр. Продолжайте только если вы абсолютно уверены.",
+ "confirmDisableOIDCWarning": "ПРЕДУПРЕЖДЕНИЕ: Вы собираетесь отключить OIDC, пока вход по паролю также отключен. Это заблокирует ваш экземпляр Termix, и вы потеряете весь доступ. Вы абсолютно уверены, что хотите продолжить?"
+ },
+ "hosts": {
+ "title": "Менеджер хостов",
+ "sshHosts": "SSH-хосты",
+ "noHosts": "Нет SSH-хостов",
+ "noHostsMessage": "Вы еще не добавили SSH-хосты. Нажмите \"Добавить хост\", чтобы начать.",
+ "loadingHosts": "Загрузка хостов...",
+ "failedToLoadHosts": "Не удалось загрузить хосты",
+ "retry": "Повторить",
+ "refresh": "Обновить",
+ "hostsCount": "{{count}} хостов",
+ "importJson": "Импорт JSON",
+ "importing": "Импорт...",
+ "importJsonTitle": "Импорт SSH-хостов из JSON",
+ "importJsonDesc": "Загрузите JSON-файл для массового импорта нескольких SSH-хостов (макс. 100).",
+ "downloadSample": "Скачать образец",
+ "formatGuide": "Руководство по формату",
+ "exportCredentialWarning": "Предупреждение: Хост \"{{name}}\" использует аутентификацию по учетным данным. Экспортируемый файл не будет включать данные учетных данных, и их нужно будет вручную перенастроить после импорта. Вы хотите продолжить?",
+ "exportSensitiveDataWarning": "Предупреждение: Хост \"{{name}}\" содержит конфиденциальные данные аутентификации (пароль/SSH-ключ). Экспортируемый файл будет включать эти данные в открытом виде. Пожалуйста, храните файл в безопасности и удалите его после использования. Вы хотите продолжить?",
+ "uncategorized": "Без категории",
+ "confirmDelete": "Вы уверены, что хотите удалить \"{{name}}\"?",
+ "failedToDeleteHost": "Не удалось удалить хост",
+ "failedToExportHost": "Не удалось экспортировать хост. Пожалуйста, убедитесь, что вы вошли в систему и имеете доступ к данным хоста.",
+ "jsonMustContainHosts": "JSON должен содержать массив \"hosts\" или быть массивом хостов",
+ "noHostsInJson": "В JSON-файле не найдено хостов",
+ "maxHostsAllowed": "Разрешено максимум 100 хостов за импорт",
+ "importCompleted": "Импорт завершен: {{success}} успешно, {{failed}} не удалось",
+ "importFailed": "Импорт не удался",
+ "importError": "Ошибка импорта",
+ "failedToImportJson": "Не удалось импортировать JSON-файл",
+ "connectionDetails": "Детали подключения",
+ "organization": "Организация",
+ "ipAddress": "IP-адрес",
+ "port": "Порт",
+ "name": "Имя",
+ "username": "Имя пользователя",
+ "folder": "Папка",
+ "tags": "Теги",
+ "pin": "Закрепить",
+ "passwordRequired": "Пароль требуется при использовании аутентификации по паролю",
+ "sshKeyRequired": "Приватный SSH-ключ требуется при использовании аутентификации по ключу",
+ "keyTypeRequired": "Тип ключа требуется при использовании аутентификации по ключу",
+ "mustSelectValidSshConfig": "Необходимо выбрать допустимую SSH-конфигурацию из списка",
+ "addHost": "Добавить хост",
+ "editHost": "Редактировать хост",
+ "cloneHost": "Клонировать хост",
+ "updateHost": "Обновить хост",
+ "hostUpdatedSuccessfully": "Хост \"{{name}}\" успешно обновлен!",
+ "hostAddedSuccessfully": "Хост \"{{name}}\" успешно добавлен!",
+ "hostDeletedSuccessfully": "Хост \"{{name}}\" успешно удален!",
+ "failedToSaveHost": "Не удалось сохранить хост. Пожалуйста, попробуйте снова.",
+ "enableTerminal": "Включить терминал",
+ "enableTerminalDesc": "Включить/отключить видимость хоста во вкладке Терминал",
+ "enableTunnel": "Включить туннель",
+ "enableTunnelDesc": "Включить/отключить видимость хоста во вкладке Туннель",
+ "enableFileManager": "Включить файловый менеджер",
+ "enableFileManagerDesc": "Включить/отключить видимость хоста во вкладке Файловый менеджер",
+ "defaultPath": "Путь по умолчанию",
+ "defaultPathDesc": "Каталог по умолчанию при открытии файлового менеджера для этого хоста",
+ "tunnelConnections": "Туннельные подключения",
+ "connection": "Подключение",
+ "remove": "Удалить",
+ "sourcePort": "Исходный порт",
+ "sourcePortDesc": " (Источник относится к Текущим деталям подключения во вкладке Общее)",
+ "endpointPort": "Порт конечной точки",
+ "endpointSshConfig": "SSH-конфигурация конечной точки",
+ "tunnelForwardDescription": "Этот туннель будет перенаправлять трафик с порта {{sourcePort}} на исходной машине (текущие детали подключения во вкладке общее) на порт {{endpointPort}} на машине конечной точки.",
+ "maxRetries": "Макс. попыток",
+ "maxRetriesDescription": "Максимальное количество попыток повторного подключения туннеля.",
+ "retryInterval": "Интервал повтора (секунды)",
+ "retryIntervalDescription": "Время ожидания между попытками повторного подключения.",
+ "autoStartContainer": "Автозапуск при запуске контейнера",
+ "autoStartDesc": "Автоматически запускать этот туннель при запуске контейнера",
+ "addConnection": "Добавить туннельное подключение",
+ "sshpassRequired": "Требуется Sshpass для аутентификации по паролю",
+ "sshpassRequiredDesc": "Для аутентификации по паролю в туннелях, sshpass должен быть установлен в системе.",
+ "otherInstallMethods": "Другие способы установки:",
+ "debianUbuntuEquivalent": "(Debian/Ubuntu) или эквивалент для вашей ОС.",
+ "or": "или",
+ "centosRhelFedora": "CentOS/RHEL/Fedora",
+ "macos": "macOS",
+ "windows": "Windows",
+ "sshServerConfigRequired": "Требуется конфигурация SSH-сервера",
+ "sshServerConfigDesc": "Для туннельных подключений SSH-сервер должен быть настроен для разрешения переадресации портов:",
+ "gatewayPortsYes": "для привязки удаленных портов ко всем интерфейсам",
+ "allowTcpForwardingYes": "для включения переадресации портов",
+ "permitRootLoginYes": "если используется пользователь root для туннелирования",
+ "editSshConfig": "Отредактируйте /etc/ssh/sshd_config и перезапустите SSH: sudo systemctl restart sshd",
+ "upload": "Загрузить",
+ "authentication": "Аутентификация",
+ "password": "Пароль",
+ "key": "Ключ",
+ "credential": "Учетные данные",
+ "none": "Нет",
+ "selectCredential": "Выбрать учетные данные",
+ "selectCredentialPlaceholder": "Выберите учетные данные...",
+ "credentialRequired": "Учетные данные требуются при использовании аутентификации по учетным данным",
+ "credentialDescription": "Выбор учетных данных перезапишет текущее имя пользователя и будет использовать детали аутентификации учетных данных.",
+ "sshPrivateKey": "Приватный SSH-ключ",
+ "keyPassword": "Пароль ключа",
+ "keyType": "Тип ключа",
+ "autoDetect": "Автоопределение",
+ "rsa": "RSA",
+ "ed25519": "ED25519",
+ "ecdsaNistP256": "ECDSA NIST P-256",
+ "ecdsaNistP384": "ECDSA NIST P-384",
+ "ecdsaNistP521": "ECDSA NIST P-521",
+ "dsa": "DSA",
+ "rsaSha2256": "RSA SHA2-256",
+ "rsaSha2512": "RSA SHA2-512",
+ "uploadFile": "Загрузить файл",
+ "pasteKey": "Вставить ключ",
+ "updateKey": "Обновить ключ",
+ "existingKey": "Существующий ключ (нажмите для изменения)",
+ "existingCredential": "Существующие учетные данные (нажмите для изменения)",
+ "addTagsSpaceToAdd": "добавить теги (пробел для добавления)",
+ "terminalBadge": "Терминал",
+ "tunnelBadge": "Туннель",
+ "fileManagerBadge": "Файловый менеджер",
+ "general": "Общее",
+ "terminal": "Терминал",
+ "tunnel": "Туннель",
+ "fileManager": "Файловый менеджер",
+ "serverStats": "Статистика сервера",
+ "hostViewer": "Просмотрщик хостов",
+ "enableServerStats": "Включить статистику сервера",
+ "enableServerStatsDesc": "Включить/отключить сбор статистики сервера для этого хоста",
+ "displayItems": "Элементы отображения",
+ "displayItemsDesc": "Выберите, какие метрики отображать на странице статистики сервера",
+ "enableCpu": "Использование CPU",
+ "enableMemory": "Использование памяти",
+ "enableDisk": "Использование диска",
+ "enableNetwork": "Сетевая статистика (Скоро)",
+ "enableProcesses": "Количество процессов (Скоро)",
+ "enableUptime": "Время работы (Скоро)",
+ "enableHostname": "Имя хоста (Скоро)",
+ "enableOs": "Операционная система (Скоро)",
+ "customCommands": "Пользовательские команды (Скоро)",
+ "customCommandsDesc": "Определите пользовательские команды выключения и перезагрузки для этого сервера",
+ "shutdownCommand": "Команда выключения",
+ "rebootCommand": "Команда перезагрузки",
+ "confirmRemoveFromFolder": "Вы уверены, что хотите удалить \"{{name}}\" из папки \"{{folder}}\"? Хост будет перемещен в \"Без папки\".",
+ "removedFromFolder": "Хост \"{{name}}\" успешно удален из папки",
+ "failedToRemoveFromFolder": "Не удалось удалить хост из папки",
+ "folderRenamed": "Папка \"{{oldName}}\" успешно переименована в \"{{newName}}\"",
+ "failedToRenameFolder": "Не удалось переименовать папку",
+ "movedToFolder": "Хост \"{{name}}\" успешно перемещен в \"{{folder}}\"",
+ "failedToMoveToFolder": "Не удалось переместить хост в папку",
+ "statistics": "Статистика",
+ "enabledWidgets": "Включенные виджеты",
+ "enabledWidgetsDesc": "Выберите, какие виджеты статистики отображать для этого хоста",
+ "monitoringConfiguration": "Конфигурация мониторинга",
+ "monitoringConfigurationDesc": "Настройте, как часто проверяются статистика и статус сервера",
+ "statusCheckEnabled": "Включить мониторинг статуса",
+ "statusCheckEnabledDesc": "Проверять, находится ли сервер в сети или вне сети",
+ "statusCheckInterval": "Интервал проверки статуса",
+ "statusCheckIntervalDesc": "Как часто проверять, находится ли хост в сети (5с - 1ч)",
+ "metricsEnabled": "Включить мониторинг метрик",
+ "metricsEnabledDesc": "Собирать статистику CPU, RAM, диска и другую системную статистику",
+ "metricsInterval": "Интервал сбора метрик",
+ "metricsIntervalDesc": "Как часто собирать статистику сервера (5с - 1ч)",
+ "intervalSeconds": "секунд",
+ "intervalMinutes": "минут",
+ "intervalValidation": "Интервалы мониторинга должны быть между 5 секундами и 1 часом (3600 секунд)",
+ "monitoringDisabled": "Мониторинг сервера отключен для этого хоста",
+ "enableMonitoring": "Включите мониторинг в Менеджере хостов → вкладка Статистика",
+ "monitoringDisabledBadge": "Мониторинг выключен",
+ "statusMonitoring": "Статус",
+ "metricsMonitoring": "Метрики",
+ "terminalCustomizationNotice": "Примечание: Настройки терминала работают только на рабочем столе (веб-сайт и Electron-приложение). Мобильные приложения и мобильный веб-сайт используют системные настройки терминала по умолчанию.",
+ "noneAuthTitle": "Интерактивная аутентификация по клавиатуре",
+ "noneAuthDescription": "Этот метод аутентификации будет использовать интерактивную аутентификацию по клавиатуре при подключении к SSH-серверу.",
+ "noneAuthDetails": "Интерактивная аутентификация по клавиатуре позволяет серверу запрашивать у вас учетные данные во время подключения. Это полезно для серверов, которые требуют многофакторную аутентификацию или динамический ввод пароля."
+ },
+ "terminal": {
+ "title": "Терминал",
+ "connect": "Подключиться к хосту",
+ "disconnect": "Отключиться",
+ "clear": "Очистить",
+ "copy": "Копировать",
+ "paste": "Вставить",
+ "find": "Найти",
+ "fullscreen": "Полный экран",
+ "splitHorizontal": "Разделить горизонтально",
+ "splitVertical": "Разделить вертикально",
+ "closePanel": "Закрыть панель",
+ "reconnect": "Переподключиться",
+ "sessionEnded": "Сеанс завершен",
+ "connectionLost": "Подключение потеряно",
+ "error": "ОШИБКА: {{message}}",
+ "disconnected": "Отключено",
+ "connectionClosed": "Подключение закрыто",
+ "connectionError": "Ошибка подключения: {{message}}",
+ "connected": "Подключено",
+ "sshConnected": "SSH-подключение установлено",
+ "authError": "Ошибка аутентификации: {{message}}",
+ "unknownError": "Произошла неизвестная ошибка",
+ "messageParseError": "Не удалось разобрать сообщение сервера",
+ "websocketError": "Ошибка подключения WebSocket",
+ "connecting": "Подключение...",
+ "reconnecting": "Переподключение... ({{attempt}}/{{max}})",
+ "reconnected": "Успешно переподключено",
+ "maxReconnectAttemptsReached": "Достигнуто максимальное количество попыток переподключения",
+ "connectionTimeout": "Таймаут подключения",
+ "terminalTitle": "Терминал - {{host}}",
+ "terminalWithPath": "Терминал - {{host}}:{{path}}",
+ "runTitle": "Выполнение {{command}} - {{host}}",
+ "totpRequired": "Требуется двухфакторная аутентификация",
+ "totpCodeLabel": "Код проверки",
+ "totpPlaceholder": "000000",
+ "totpVerify": "Проверить"
+ },
+ "fileManager": {
+ "title": "Файловый менеджер",
+ "file": "Файл",
+ "folder": "Папка",
+ "connectToSsh": "Подключитесь к SSH для использования файловых операций",
+ "uploadFile": "Загрузить файл",
+ "downloadFile": "Скачать",
+ "edit": "Редактировать",
+ "preview": "Просмотр",
+ "previous": "Предыдущий",
+ "next": "Следующий",
+ "pageXOfY": "Страница {{current}} из {{total}}",
+ "zoomOut": "Уменьшить",
+ "zoomIn": "Увеличить",
+ "newFile": "Новый файл",
+ "newFolder": "Новая папка",
+ "rename": "Переименовать",
+ "renameItem": "Переименовать элемент",
+ "deleteItem": "Удалить элемент",
+ "currentPath": "Текущий путь",
+ "uploadFileTitle": "Загрузить файл",
+ "maxFileSize": "Макс: 1GB (JSON) / 5GB (Binary) - Поддерживаются большие файлы",
+ "removeFile": "Удалить файл",
+ "clickToSelectFile": "Нажмите для выбора файла",
+ "chooseFile": "Выбрать файл",
+ "uploading": "Загрузка...",
+ "downloading": "Скачивание...",
+ "uploadingFile": "Загрузка {{name}}...",
+ "uploadingLargeFile": "Загрузка большого файла {{name}} ({{size}})...",
+ "downloadingFile": "Скачивание {{name}}...",
+ "creatingFile": "Создание {{name}}...",
+ "creatingFolder": "Создание {{name}}...",
+ "deletingItem": "Удаление {{type}} {{name}}...",
+ "renamingItem": "Переименование {{type}} {{oldName}} в {{newName}}...",
+ "createNewFile": "Создать новый файл",
+ "fileName": "Имя файла",
+ "creating": "Создание...",
+ "createFile": "Создать файл",
+ "createNewFolder": "Создать новую папку",
+ "folderName": "Имя папки",
+ "createFolder": "Создать папку",
+ "warningCannotUndo": "Предупреждение: Это действие нельзя отменить",
+ "itemPath": "Путь к элементу",
+ "thisIsDirectory": "Это каталог (будет удален рекурсивно)",
+ "deleting": "Удаление...",
+ "currentPathLabel": "Текущий путь",
+ "newName": "Новое имя",
+ "thisIsDirectoryRename": "Это каталог",
+ "renaming": "Переименование...",
+ "fileUploadedSuccessfully": "Файл \"{{name}}\" успешно загружен",
+ "failedToUploadFile": "Не удалось загрузить файл",
+ "fileDownloadedSuccessfully": "Файл \"{{name}}\" успешно скачан",
+ "failedToDownloadFile": "Не удалось скачать файл",
+ "noFileContent": "Содержимое файла не получено",
+ "filePath": "Путь к файлу",
+ "fileCreatedSuccessfully": "Файл \"{{name}}\" успешно создан",
+ "failedToCreateFile": "Не удалось создать файл",
+ "folderCreatedSuccessfully": "Папка \"{{name}}\" успешно создана",
+ "failedToCreateFolder": "Не удалось создать папку",
+ "failedToCreateItem": "Не удалось создать элемент",
+ "operationFailed": "Операция {{operation}} не удалась для {{name}}: {{error}}",
+ "failedToResolveSymlink": "Не удалось разрешить символическую ссылку",
+ "itemDeletedSuccessfully": "{{type}} успешно удален",
+ "itemsDeletedSuccessfully": "{{count}} элементов успешно удалено",
+ "failedToDeleteItems": "Не удалось удалить элементы",
+ "dragFilesToUpload": "Перетащите файлы сюда для загрузки",
+ "emptyFolder": "Эта папка пуста",
+ "itemCount": "{{count}} элементов",
+ "selectedCount": "{{count}} выбрано",
+ "searchFiles": "Поиск файлов...",
+ "upload": "Загрузить",
+ "selectHostToStart": "Выберите хост для начала управления файлами",
+ "failedToConnect": "Не удалось подключиться к SSH",
+ "failedToLoadDirectory": "Не удалось загрузить каталог",
+ "noSSHConnection": "Нет доступного SSH-подключения",
+ "enterFolderName": "Введите имя папки:",
+ "enterFileName": "Введите имя файла:",
+ "copy": "Копировать",
+ "cut": "Вырезать",
+ "paste": "Вставить",
+ "delete": "Удалить",
+ "properties": "Свойства",
+ "preview": "Просмотр",
+ "refresh": "Обновить",
+ "downloadFiles": "Скачать {{count}} файлов в браузер",
+ "copyFiles": "Копировать {{count}} элементов",
+ "cutFiles": "Вырезать {{count}} элементов",
+ "deleteFiles": "Удалить {{count}} элементов",
+ "filesCopiedToClipboard": "{{count}} элементов скопировано в буфер обмена",
+ "filesCutToClipboard": "{{count}} элементов вырезано в буфер обмена",
+ "movedItems": "Перемещено {{count}} элементов",
+ "failedToDeleteItem": "Не удалось удалить элемент",
+ "itemRenamedSuccessfully": "{{type}} успешно переименован",
+ "failedToRenameItem": "Не удалось переименовать элемент",
+ "upload": "Загрузить",
+ "download": "Скачать",
+ "newFile": "Новый файл",
+ "newFolder": "Новая папка",
+ "rename": "Переименовать",
+ "delete": "Удалить",
+ "permissions": "Права доступа",
+ "size": "Размер",
+ "modified": "Изменен",
+ "path": "Путь",
+ "fileName": "Имя файла",
+ "folderName": "Имя папки",
+ "confirmDelete": "Вы уверены, что хотите удалить {{name}}?",
+ "uploadSuccess": "Файл успешно загружен",
+ "uploadFailed": "Не удалось загрузить файл",
+ "downloadSuccess": "Файл успешно скачан",
+ "downloadFailed": "Не удалось скачать файл",
+ "permissionDenied": "Доступ запрещен",
+ "checkDockerLogs": "Проверьте логи Docker для получения подробной информации об ошибке",
+ "internalServerError": "Произошла внутренняя ошибка сервера",
+ "serverError": "Ошибка сервера",
+ "error": "Ошибка",
+ "requestFailed": "Запрос завершился с кодом состояния",
+ "unknownFileError": "неизвестно",
+ "cannotReadFile": "Невозможно прочитать файл",
+ "noSshSessionId": "Нет доступного ID SSH-сессии",
+ "noFilePath": "Нет доступного пути к файлу",
+ "noCurrentHost": "Нет текущего хоста",
+ "fileSavedSuccessfully": "Файл успешно сохранен",
+ "saveTimeout": "Операция сохранения превысила время ожидания. Файл мог быть успешно сохранен, но операция заняла слишком много времени для завершения. Проверьте логи Docker для подтверждения.",
+ "failedToSaveFile": "Не удалось сохранить файл",
+ "folder": "Папка",
+ "file": "Файл",
+ "deletedSuccessfully": "успешно удален",
+ "failedToDeleteItem": "Не удалось удалить элемент",
+ "connectToServer": "Подключиться к серверу",
+ "selectServerToEdit": "Выберите сервер на боковой панели, чтобы начать редактирование файлов",
+ "fileOperations": "Файловые операции",
+ "confirmDeleteMessage": "Вы уверены, что хотите удалить {{name}} ?",
+ "confirmDeleteSingleItem": "Вы уверены, что хотите окончательно удалить \"{{name}}\"?",
+ "confirmDeleteMultipleItems": "Вы уверены, что хотите окончательно удалить {{count}} элементов?",
+ "confirmDeleteMultipleItemsWithFolders": "Вы уверены, что хотите окончательно удалить {{count}} элементов? Это включает папки и их содержимое.",
+ "confirmDeleteFolder": "Вы уверены, что хотите окончательно удалить папку \"{{name}}\" и все ее содержимое?",
+ "deleteDirectoryWarning": "Это удалит папку и все ее содержимое.",
+ "actionCannotBeUndone": "Это действие нельзя отменить.",
+ "permanentDeleteWarning": "Это действие нельзя отменить. Элемент(ы) будут окончательно удалены с сервера.",
+ "recent": "Недавние",
+ "pinned": "Закрепленные",
+ "folderShortcuts": "Ярлыки папок",
+ "noRecentFiles": "Нет недавних файлов.",
+ "noPinnedFiles": "Нет закрепленных файлов.",
+ "enterFolderPath": "Введите путь к папке",
+ "noShortcuts": "Нет ярлыков.",
+ "searchFilesAndFolders": "Поиск файлов и папок...",
+ "noFilesOrFoldersFound": "Файлы или папки не найдены.",
+ "failedToConnectSSH": "Не удалось подключиться к SSH",
+ "failedToReconnectSSH": "Не удалось переподключить SSH-сессию",
+ "failedToListFiles": "Не удалось получить список файлов",
+ "fetchHomeDataTimeout": "Получение домашних данных превысило время ожидания",
+ "sshStatusCheckTimeout": "Проверка статуса SSH превысила время ожидания",
+ "sshReconnectionTimeout": "Переподключение SSH превысило время ожидания",
+ "saveOperationTimeout": "Операция сохранения превысила время ожидания",
+ "cannotSaveFile": "Невозможно сохранить файл",
+ "dragSystemFilesToUpload": "Перетащите системные файлы сюда для загрузки",
+ "dragFilesToWindowToDownload": "Перетащите файлы за пределы окна для скачивания",
+ "openTerminalHere": "Открыть терминал здесь",
+ "run": "Выполнить",
+ "saveToSystem": "Сохранить как...",
+ "selectLocationToSave": "Выберите место для сохранения",
+ "openTerminalInFolder": "Открыть терминал в этой папке",
+ "openTerminalInFileLocation": "Открыть терминал в расположении файла",
+ "terminalWithPath": "Терминал - {{host}}:{{path}}",
+ "runningFile": "Выполнение - {{file}}",
+ "onlyRunExecutableFiles": "Можно выполнять только исполняемые файлы",
+ "noHostSelected": "Хост не выбран",
+ "starred": "Избранное",
+ "shortcuts": "Ярлыки",
+ "directories": "Каталоги",
+ "removedFromRecentFiles": "Удалено \"{{name}}\" из недавних файлов",
+ "removeFailed": "Удаление не удалось",
+ "unpinnedSuccessfully": "Откреплено \"{{name}}\" успешно",
+ "unpinFailed": "Открепление не удалось",
+ "removedShortcut": "Удален ярлык \"{{name}}\"",
+ "removeShortcutFailed": "Не удалось удалить ярлык",
+ "clearedAllRecentFiles": "Все недавние файлы очищены",
+ "clearFailed": "Очистка не удалась",
+ "removeFromRecentFiles": "Удалить из недавних файлов",
+ "clearAllRecentFiles": "Очистить все недавние файлы",
+ "unpinFile": "Открепить файл",
+ "removeShortcut": "Удалить ярлык",
+ "saveFilesToSystem": "Сохранить {{count}} файлов как...",
+ "saveToSystem": "Сохранить как...",
+ "pinFile": "Закрепить файл",
+ "addToShortcuts": "Добавить в ярлыки",
+ "selectLocationToSave": "Выберите место для сохранения",
+ "downloadToDefaultLocation": "Скачать в место по умолчанию",
+ "pasteFailed": "Вставка не удалась",
+ "noUndoableActions": "Нет действий для отмены",
+ "undoCopySuccess": "Операция копирования отменена: Удалено {{count}} скопированных файлов",
+ "undoCopyFailedDelete": "Отмена не удалась: Не удалось удалить скопированные файлы",
+ "undoCopyFailedNoInfo": "Отмена не удалась: Не удалось найти информацию о скопированных файлах",
+ "undoMoveSuccess": "Операция перемещения отменена: Перемещено {{count}} файлов обратно в исходное расположение",
+ "undoMoveFailedMove": "Отмена не удалась: Не удалось переместить файлы обратно",
+ "undoMoveFailedNoInfo": "Отмена не удалась: Не удалось найти информацию о перемещенных файлах",
+ "undoDeleteNotSupported": "Операцию удаления нельзя отменить: Файлы окончательно удалены с сервера",
+ "undoTypeNotSupported": "Неподдерживаемый тип операции отмены",
+ "undoOperationFailed": "Операция отмены не удалась",
+ "unknownError": "Неизвестная ошибка",
+ "enterPath": "Введите путь...",
+ "editPath": "Редактировать путь",
+ "confirm": "Подтвердить",
+ "cancel": "Отмена",
+ "folderName": "Имя папки",
+ "find": "Найти...",
+ "replaceWith": "Заменить на...",
+ "replace": "Заменить",
+ "replaceAll": "Заменить все",
+ "downloadInstead": "Скачать вместо этого",
+ "keyboardShortcuts": "Горячие клавиши",
+ "searchAndReplace": "Поиск и замена",
+ "editing": "Редактирование",
+ "navigation": "Навигация",
+ "code": "Код",
+ "search": "Поиск",
+ "findNext": "Найти следующее",
+ "findPrevious": "Найти предыдущее",
+ "save": "Сохранить",
+ "selectAll": "Выделить все",
+ "undo": "Отменить",
+ "redo": "Повторить",
+ "goToLine": "Перейти к строке",
+ "moveLineUp": "Переместить строку вверх",
+ "moveLineDown": "Переместить строку вниз",
+ "toggleComment": "Закомментировать/раскомментировать",
+ "indent": "Увеличить отступ",
+ "outdent": "Уменьшить отступ",
+ "autoComplete": "Автозавершение",
+ "imageLoadError": "Не удалось загрузить изображение",
+ "zoomIn": "Увеличить",
+ "zoomOut": "Уменьшить",
+ "rotate": "Повернуть",
+ "originalSize": "Оригинальный размер",
+ "startTyping": "Начните печатать...",
+ "unknownSize": "Неизвестный размер",
+ "fileIsEmpty": "Файл пуст",
+ "modified": "Изменен",
+ "largeFileWarning": "Предупреждение о большом файле",
+ "largeFileWarningDesc": "Этот файл имеет размер {{size}}, что может вызвать проблемы с производительностью при открытии как текста.",
+ "fileNotFoundAndRemoved": "Файл \"{{name}}\" не найден и был удален из недавних/закрепленных файлов",
+ "failedToLoadFile": "Не удалось загрузить файл: {{error}}",
+ "serverErrorOccurred": "Произошла ошибка сервера. Пожалуйста, попробуйте позже.",
+ "fileSavedSuccessfully": "Файл успешно сохранен",
+ "autoSaveFailed": "Автосохранение не удалось",
+ "fileAutoSaved": "Файл автосохранен",
+ "fileDownloadedSuccessfully": "Файл успешно скачан",
+ "moveFileFailed": "Не удалось переместить {{name}}",
+ "moveOperationFailed": "Операция перемещения не удалась",
+ "canOnlyCompareFiles": "Можно сравнивать только два файла",
+ "comparingFiles": "Сравнение файлов: {{file1}} и {{file2}}",
+ "dragFailed": "Операция перетаскивания не удалась",
+ "filePinnedSuccessfully": "Файл \"{{name}}\" успешно закреплен",
+ "pinFileFailed": "Не удалось закрепить файл",
+ "fileUnpinnedSuccessfully": "Файл \"{{name}}\" успешно откреплен",
+ "unpinFileFailed": "Не удалось открепить файл",
+ "shortcutAddedSuccessfully": "Ярлык папки \"{{name}}\" успешно добавлен",
+ "addShortcutFailed": "Не удалось добавить ярлык",
+ "operationCompletedSuccessfully": "{{operation}} {{count}} элементов успешно завершено",
+ "operationCompleted": "{{operation}} {{count}} элементов",
+ "downloadFileSuccess": "Файл {{name}} успешно скачан",
+ "downloadFileFailed": "Скачивание не удалось",
+ "moveTo": "Переместить в {{name}}",
+ "diffCompareWith": "Сравнить различия с {{name}}",
+ "dragOutsideToDownload": "Перетащите за пределы окна для скачивания ({{count}} файлов)",
+ "newFolderDefault": "НоваяПапка",
+ "newFileDefault": "НовыйФайл.txt",
+ "successfullyMovedItems": "Успешно перемещено {{count}} элементов в {{target}}",
+ "move": "Переместить",
+ "searchInFile": "Поиск в файле (Ctrl+F)",
+ "showKeyboardShortcuts": "Показать горячие клавиши",
+ "startWritingMarkdown": "Начните писать ваш markdown-контент...",
+ "loadingFileComparison": "Загрузка сравнения файлов...",
+ "reload": "Перезагрузить",
+ "compare": "Сравнить",
+ "sideBySide": "Рядом",
+ "inline": "Встроенное",
+ "fileComparison": "Сравнение файлов: {{file1}} vs {{file2}}",
+ "fileTooLarge": "Файл слишком большой: {{error}}",
+ "sshConnectionFailed": "SSH-подключение не удалось. Пожалуйста, проверьте ваше подключение к {{name}} ({{ip}}:{{port}})",
+ "loadFileFailed": "Не удалось загрузить файл: {{error}}",
+ "connectedSuccessfully": "Успешно подключено",
+ "totpVerificationFailed": "Проверка TOTP не удалась"
+ },
+ "tunnels": {
+ "title": "SSH-туннели",
+ "noSshTunnels": "Нет SSH-туннелей",
+ "createFirstTunnelMessage": "Вы еще не создали SSH-туннели. Настройте туннельные подключения в Менеджере хостов, чтобы начать.",
+ "connected": "Подключено",
+ "disconnected": "Отключено",
+ "connecting": "Подключение...",
+ "disconnecting": "Отключение...",
+ "unknownTunnelStatus": "Неизвестно",
+ "unknown": "Неизвестно",
+ "error": "Ошибка",
+ "failed": "Не удалось",
+ "retrying": "Повторная попытка",
+ "waiting": "Ожидание",
+ "waitingForRetry": "Ожидание повторной попытки",
+ "retryingConnection": "Повторное подключение",
+ "canceling": "Отмена...",
+ "connect": "Подключить",
+ "disconnect": "Отключить",
+ "cancel": "Отмена",
+ "port": "Порт",
+ "attempt": "Попытка {{current}} из {{max}}",
+ "nextRetryIn": "Следующая попытка через {{seconds}} секунд",
+ "checkDockerLogs": "Проверьте ваши логи Docker для выяснения причины ошибки, присоединяйтесь к",
+ "noTunnelConnections": "Нет настроенных туннельных подключений",
+ "tunnelConnections": "Туннельные подключения",
+ "addTunnel": "Добавить туннель",
+ "editTunnel": "Редактировать туннель",
+ "deleteTunnel": "Удалить туннель",
+ "tunnelName": "Имя туннеля",
+ "localPort": "Локальный порт",
+ "remoteHost": "Удаленный хост",
+ "remotePort": "Удаленный порт",
+ "autoStart": "Автозапуск",
+ "status": "Статус",
+ "active": "Активно",
+ "inactive": "Неактивно",
+ "start": "Запустить",
+ "stop": "Остановить",
+ "restart": "Перезапустить",
+ "connectionType": "Тип подключения",
+ "local": "Локальный",
+ "remote": "Удаленный",
+ "dynamic": "Динамический",
+ "noSshTunnels": "Нет SSH-туннелей",
+ "createFirstTunnelMessage": "Создайте ваш первый SSH-туннель, чтобы начать. Используйте SSH-менеджер для добавления хостов с туннельными подключениями.",
+ "unknownConnectionStatus": "Неизвестно",
+ "connected": "Подключено",
+ "connecting": "Подключение...",
+ "disconnecting": "Отключение...",
+ "disconnected": "Отключено",
+ "portMapping": "Порт {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
+ "disconnect": "Отключить",
+ "connect": "Подключить",
+ "canceling": "Отмена...",
+ "endpointHostNotFound": "Хост конечной точки не найден",
+ "discord": "Discord",
+ "githubIssue": "Проблема на GitHub",
+ "forHelp": "для помощи"
+ },
+ "serverStats": {
+ "title": "Статистика сервера",
+ "cpu": "CPU",
+ "memory": "Память",
+ "disk": "Диск",
+ "network": "Сеть",
+ "uptime": "Время работы",
+ "loadAverage": "Средняя загрузка",
+ "processes": "Процессы",
+ "connections": "Подключения",
+ "usage": "Использование",
+ "available": "Доступно",
+ "total": "Всего",
+ "free": "Свободно",
+ "used": "Использовано",
+ "percentage": "Процент",
+ "refreshStatusAndMetrics": "Обновить статус и метрики",
+ "refreshStatus": "Обновить статус",
+ "fileManagerAlreadyOpen": "Файловый менеджер уже открыт для этого хоста",
+ "openFileManager": "Открыть файловый менеджер",
+ "cpuCores_one": "{{count}} CPU",
+ "cpuCores_other": "{{count}} CPU",
+ "naCpus": "N/A CPU",
+ "loadAverage": "Средняя: {{avg1}}, {{avg5}}, {{avg15}}",
+ "loadAverageNA": "Средняя: N/A",
+ "cpuUsage": "Использование CPU",
+ "memoryUsage": "Использование памяти",
+ "diskUsage": "Использование диска",
+ "rootStorageSpace": "Место в корневом хранилище",
+ "of": "из",
+ "feedbackMessage": "Есть идеи, что должно быть следующим для управления сервером? Поделитесь ими на",
+ "failedToFetchHostConfig": "Не удалось загрузить конфигурацию хоста",
+ "failedToFetchStatus": "Не удалось загрузить статус сервера",
+ "failedToFetchMetrics": "Не удалось загрузить метрики сервера",
+ "failedToFetchHomeData": "Не удалось загрузить домашние данные",
+ "loadingMetrics": "Загрузка метрик...",
+ "refreshing": "Обновление...",
+ "serverOffline": "Сервер не в сети",
+ "cannotFetchMetrics": "Невозможно получить метрики с отключенного сервера",
+ "totpRequired": "Требуется TOTP-аутентификация",
+ "totpUnavailable": "Статистика сервера недоступна для серверов с включенным TOTP",
+ "load": "Загрузка",
+ "free": "Свободно",
+ "available": "Доступно",
+ "editLayout": "Редактировать макет",
+ "cancelEdit": "Отмена",
+ "addWidget": "Добавить виджет",
+ "saveLayout": "Сохранить макет",
+ "unsavedChanges": "Несохраненные изменения",
+ "layoutSaved": "Макет успешно сохранен",
+ "failedToSaveLayout": "Не удалось сохранить макет",
+ "systemInfo": "Системная информация",
+ "hostname": "Имя хоста",
+ "operatingSystem": "Операционная система",
+ "kernel": "Ядро",
+ "totalUptime": "Общее время работы",
+ "seconds": "секунд",
+ "networkInterfaces": "Сетевые интерфейсы",
+ "noInterfacesFound": "Сетевые интерфейсы не найдены",
+ "totalProcesses": "Всего процессов",
+ "running": "Запущено",
+ "noProcessesFound": "Процессы не найдены"
+ },
+ "auth": {
+ "loginTitle": "Вход в Termix",
+ "registerTitle": "Создать учетную запись",
+ "loginButton": "Войти",
+ "registerButton": "Зарегистрироваться",
+ "forgotPassword": "Забыли пароль?",
+ "rememberMe": "Запомнить меня",
+ "noAccount": "Нет учетной записи?",
+ "hasAccount": "Уже есть учетная запись?",
+ "loginSuccess": "Вход выполнен успешно",
+ "loginFailed": "Ошибка входа",
+ "registerSuccess": "Регистрация успешна",
+ "registerFailed": "Ошибка регистрации",
+ "logoutSuccess": "Выход выполнен успешно",
+ "invalidCredentials": "Неверное имя пользователя или пароль",
+ "accountCreated": "Учетная запись успешно создана",
+ "passwordReset": "Ссылка для сброса пароля отправлена",
+ "twoFactorAuth": "Двухфакторная аутентификация",
+ "enterCode": "Введите код проверки",
+ "backupCode": "Или используйте резервный код",
+ "verifyCode": "Проверить код",
+ "enableTwoFactor": "Включить двухфакторную аутентификацию",
+ "disableTwoFactor": "Отключить двухфакторную аутентификацию",
+ "scanQRCode": "Отсканируйте этот QR-код вашим приложением-аутентификатором",
+ "backupCodes": "Резервные коды",
+ "saveBackupCodes": "Сохраните эти резервные коды в безопасном месте",
+ "twoFactorEnabledSuccess": "Двухфакторная аутентификация успешно включена!",
+ "twoFactorDisabled": "Двухфакторная аутентификация отключена",
+ "newBackupCodesGenerated": "Сгенерированы новые резервные коды",
+ "backupCodesDownloaded": "Резервные коды скачаны",
+ "pleaseEnterSixDigitCode": "Пожалуйста, введите 6-значный код",
+ "invalidVerificationCode": "Неверный код проверки",
+ "failedToDisableTotp": "Не удалось отключить TOTP",
+ "failedToGenerateBackupCodes": "Не удалось сгенерировать резервные коды",
+ "enterPassword": "Введите ваш пароль",
+ "lockedOidcAuth": "Заблокировано (OIDC Auth)",
+ "twoFactorTitle": "Двухфакторная аутентификация",
+ "twoFactorProtected": "Ваша учетная запись защищена двухфакторной аутентификацией",
+ "twoFactorActive": "Двухфакторная аутентификация в настоящее время активна на вашей учетной записи",
+ "disable2FA": "Отключить 2FA",
+ "disableTwoFactorWarning": "Отключение двухфакторной аутентификации сделает вашу учетную запись менее защищенной",
+ "passwordOrTotpCode": "Пароль или TOTP-код",
+ "or": "Или",
+ "generateNewBackupCodesText": "Сгенерируйте новые резервные коды, если вы потеряли существующие",
+ "generateNewBackupCodes": "Сгенерировать новые резервные коды",
+ "yourBackupCodes": "Ваши резервные коды",
+ "download": "Скачать",
+ "setupTwoFactorTitle": "Настройка двухфакторной аутентификации",
+ "step1ScanQR": "Шаг 1: Отсканируйте QR-код вашим приложением-аутентификатором",
+ "manualEntryCode": "Код для ручного ввода",
+ "cannotScanQRText": "Если вы не можете отсканировать QR-код, введите этот код вручную в вашем приложении-аутентификаторе",
+ "nextVerifyCode": "Далее: Проверить код",
+ "verifyAuthenticator": "Проверьте ваш аутентификатор",
+ "step2EnterCode": "Шаг 2: Введите 6-значный код из вашего приложения-аутентификатора",
+ "verificationCode": "Код проверки",
+ "back": "Назад",
+ "verifyAndEnable": "Проверить и включить",
+ "saveBackupCodesTitle": "Сохраните ваши резервные коды",
+ "step3StoreCodesSecurely": "Шаг 3: Сохраните эти коды в безопасном месте",
+ "importantBackupCodesText": "Сохраните эти резервные коды в безопасном месте. Вы можете использовать их для доступа к вашей учетной записи, если потеряете ваше устройство аутентификации.",
+ "completeSetup": "Завершить настройку",
+ "notEnabledText": "Двухфакторная аутентификация добавляет дополнительный уровень безопасности, требуя код из вашего приложения-аутентификатора при входе.",
+ "enableTwoFactorButton": "Включить двухфакторную аутентификацию",
+ "addExtraSecurityLayer": "Добавьте дополнительный уровень безопасности к вашей учетной записи",
+ "firstUser": "Первый пользователь",
+ "firstUserMessage": "Вы первый пользователь и будете сделаны администратором. Вы можете просмотреть настройки администратора в выпадающем меню пользователя на боковой панели. Если вы считаете, что это ошибка, проверьте логи docker или создайте проблему на GitHub.",
+ "external": "Внешний",
+ "loginWithExternal": "Войти через внешнего провайдера",
+ "loginWithExternalDesc": "Войти с использованием настроенного внешнего провайдера идентификации",
+ "externalNotSupportedInElectron": "Внешняя аутентификация пока не поддерживается в Electron-приложении. Пожалуйста, используйте веб-версию для входа через OIDC.",
+ "resetPasswordButton": "Сбросить пароль",
+ "sendResetCode": "Отправить код сброса",
+ "resetCodeDesc": "Введите ваше имя пользователя для получения кода сброса пароля. Код будет записан в логи docker-контейнера.",
+ "resetCode": "Код сброса",
+ "verifyCodeButton": "Проверить код",
+ "enterResetCode": "Введите 6-значный код из логов docker-контейнера для пользователя:",
+ "goToLogin": "Перейти ко входу",
+ "newPassword": "Новый пароль",
+ "confirmNewPassword": "Подтвердите пароль",
+ "enterNewPassword": "Введите ваш новый пароль для пользователя:",
+ "signUp": "Зарегистрироваться",
+ "dataLossWarning": "Сброс пароля этим способом удалит все ваши сохраненные SSH-хосты, учетные данные и другие зашифрованные данные. Это действие нельзя отменить. Используйте это только если вы забыли пароль и не вошли в систему.",
+ "authenticationDisabled": "Аутентификация отключена",
+ "authenticationDisabledDesc": "Все методы аутентификации в настоящее время отключены. Пожалуйста, свяжитесь с вашим администратором."
+ },
+ "errors": {
+ "notFound": "Страница не найдена",
+ "unauthorized": "Неавторизованный доступ",
+ "forbidden": "Доступ запрещен",
+ "serverError": "Ошибка сервера",
+ "networkError": "Сетевая ошибка",
+ "databaseConnection": "Не удалось подключиться к базе данных.",
+ "unknownError": "Неизвестная ошибка",
+ "loginFailed": "Ошибка входа",
+ "failedPasswordReset": "Не удалось инициировать сброс пароля",
+ "failedVerifyCode": "Не удалось проверить код сброса",
+ "failedCompleteReset": "Не удалось завершить сброс пароля",
+ "invalidTotpCode": "Неверный TOTP-код",
+ "failedOidcLogin": "Не удалось начать вход через OIDC",
+ "failedUserInfo": "Не удалось получить информацию о пользователе после входа через OIDC",
+ "oidcAuthFailed": "OIDC-аутентификация не удалась",
+ "noTokenReceived": "Токен не получен при входе",
+ "invalidAuthUrl": "Получен неверный URL авторизации от бэкенда",
+ "invalidInput": "Неверный ввод",
+ "requiredField": "Это поле обязательно",
+ "minLength": "Минимальная длина {{min}}",
+ "maxLength": "Максимальная длина {{max}}",
+ "invalidEmail": "Неверный адрес email",
+ "passwordMismatch": "Пароли не совпадают",
+ "passwordLoginDisabled": "Вход по имени пользователя/паролю в настоящее время отключен",
+ "weakPassword": "Пароль слишком слабый",
+ "usernameExists": "Имя пользователя уже существует",
+ "emailExists": "Email уже существует",
+ "loadFailed": "Не удалось загрузить данные",
+ "saveError": "Не удалось сохранить",
+ "sessionExpired": "Сеанс истек - пожалуйста, войдите снова"
+ },
+ "messages": {
+ "saveSuccess": "Успешно сохранено",
+ "saveError": "Не удалось сохранить",
+ "deleteSuccess": "Успешно удалено",
+ "deleteError": "Не удалось удалить",
+ "updateSuccess": "Успешно обновлено",
+ "updateError": "Не удалось обновить",
+ "copySuccess": "Скопировано в буфер обмена",
+ "copyError": "Не удалось скопировать",
+ "copiedToClipboard": "{{item}} скопировано в буфер обмена",
+ "connectionEstablished": "Подключение установлено",
+ "connectionClosed": "Подключение закрыто",
+ "reconnecting": "Переподключение...",
+ "processing": "Обработка...",
+ "pleaseWait": "Пожалуйста, подождите...",
+ "registrationDisabled": "Регистрация новых учетных записей в настоящее время отключена администратором. Пожалуйста, войдите или свяжитесь с администратором.",
+ "databaseConnected": "Подключение к базе данных успешно установлено",
+ "databaseConnectionFailed": "Не удалось подключиться к серверу базы данных",
+ "checkServerConnection": "Пожалуйста, проверьте ваше подключение к серверу и попробуйте снова",
+ "resetCodeSent": "Код сброса отправлен в логи Docker",
+ "codeVerified": "Код успешно проверен",
+ "passwordResetSuccess": "Пароль успешно сброшен",
+ "loginSuccess": "Вход выполнен успешно",
+ "registrationSuccess": "Регистрация успешна"
+ },
+ "profile": {
+ "title": "Профиль пользователя",
+ "description": "Управление настройками учетной записи и безопасностью",
+ "security": "Безопасность",
+ "changePassword": "Изменить пароль",
+ "twoFactorAuth": "Двухфакторная аутентификация",
+ "accountInfo": "Информация об учетной записи",
+ "role": "Роль",
+ "admin": "Администратор",
+ "user": "Пользователь",
+ "authMethod": "Метод аутентификации",
+ "local": "Локальный",
+ "external": "Внешний (OIDC)",
+ "selectPreferredLanguage": "Выберите предпочитаемый язык интерфейса",
+ "currentPassword": "Текущий пароль",
+ "passwordChangedSuccess": "Пароль успешно изменен! Пожалуйста, войдите снова.",
+ "failedToChangePassword": "Не удалось изменить пароль. Пожалуйста, проверьте ваш текущий пароль и попробуйте снова."
+ },
+ "user": {
+ "failedToLoadVersionInfo": "Не удалось загрузить информацию о версии"
+ },
+ "placeholders": {
+ "enterCode": "000000",
+ "ipAddress": "127.0.0.1",
+ "port": "22",
+ "maxRetries": "3",
+ "retryInterval": "10",
+ "language": "Язык",
+ "username": "имя пользователя",
+ "hostname": "имя хоста",
+ "folder": "папка",
+ "password": "пароль",
+ "keyPassword": "пароль ключа",
+ "pastePrivateKey": "Вставьте ваш приватный ключ здесь...",
+ "pastePublicKey": "Вставьте ваш публичный ключ здесь...",
+ "credentialName": "Мой SSH-сервер",
+ "description": "Описание SSH-учетных данных",
+ "searchCredentials": "Поиск учетных данных по имени, имени пользователя или тегам...",
+ "sshConfig": "конфигурация ssh конечной точки",
+ "homePath": "/home",
+ "clientId": "your-client-id",
+ "clientSecret": "your-client-secret",
+ "authUrl": "https://your-provider.com/application/o/authorize/",
+ "redirectUrl": "https://your-provider.com/application/o/termix/",
+ "tokenUrl": "https://your-provider.com/application/o/token/",
+ "userIdField": "sub",
+ "usernameField": "name",
+ "scopes": "openid email profile",
+ "userinfoUrl": "https://your-provider.com/application/o/userinfo/",
+ "enterUsername": "Введите имя пользователя, чтобы сделать администратором",
+ "searchHosts": "Поиск хостов по имени, имени пользователя, IP, папке, тегам...",
+ "enterPassword": "Введите ваш пароль",
+ "totpCode": "6-значный TOTP-код",
+ "searchHostsAny": "Поиск хостов по любой информации...",
+ "confirmPassword": "Введите ваш пароль для подтверждения",
+ "typeHere": "Введите здесь",
+ "fileName": "Введите имя файла (например, example.txt)",
+ "folderName": "Введите имя папки",
+ "fullPath": "Введите полный путь к элементу",
+ "currentPath": "Введите текущий путь к элементу",
+ "newName": "Введите новое имя"
+ },
+ "leftSidebar": {
+ "failedToLoadHosts": "Не удалось загрузить хосты",
+ "noFolder": "Без папки",
+ "passwordRequired": "Требуется пароль",
+ "failedToDeleteAccount": "Не удалось удалить учетную запись",
+ "failedToMakeUserAdmin": "Не удалось сделать пользователя администратором",
+ "userIsNowAdmin": "Пользователь {{username}} теперь администратор",
+ "removeAdminConfirm": "Вы уверены, что хотите убрать статус администратора у {{username}}?",
+ "deleteUserConfirm": "Вы уверены, что хотите удалить пользователя {{username}}? Это действие нельзя отменить.",
+ "deleteAccount": "Удалить учетную запись",
+ "closeDeleteAccount": "Закрыть удаление учетной записи",
+ "deleteAccountWarning": "Это действие нельзя отменить. Это окончательно удалит вашу учетную запись и все связанные данные.",
+ "deleteAccountWarningDetails": "Удаление вашей учетной записи удалит все ваши данные, включая SSH-хосты, конфигурации и настройки. Это действие необратимо.",
+ "cannotDeleteAccount": "Невозможно удалить учетную запись",
+ "lastAdminWarning": "Вы последний пользователь-администратор. Вы не можете удалить свою учетную запись, так как это оставит систему без администраторов. Пожалуйста, сначала сделайте другого пользователя администратором или свяжитесь с поддержкой системы.",
+ "confirmPassword": "Подтвердите пароль",
+ "deleting": "Удаление...",
+ "cancel": "Отмена"
+ },
+ "interface": {
+ "sidebar": "Боковая панель",
+ "toggleSidebar": "Переключить боковую панель",
+ "close": "Закрыть",
+ "online": "В сети",
+ "offline": "Не в сети",
+ "maintenance": "Обслуживание",
+ "degraded": "Снижена производительность",
+ "noTunnelConnections": "Нет настроенных туннельных подключений",
+ "discord": "Discord",
+ "connectToSshForOperations": "Подключитесь к SSH для использования файловых операций",
+ "uploadFile": "Загрузить файл",
+ "newFile": "Новый файл",
+ "newFolder": "Новая папка",
+ "rename": "Переименовать",
+ "deleteItem": "Удалить элемент",
+ "createNewFile": "Создать новый файл",
+ "createNewFolder": "Создать новую папку",
+ "deleteItem": "Удалить элемент",
+ "renameItem": "Переименовать элемент",
+ "clickToSelectFile": "Нажмите для выбора файла",
+ "noSshHosts": "Нет SSH-хостов",
+ "sshHosts": "SSH-хосты",
+ "importSshHosts": "Импорт SSH-хостов из JSON",
+ "clientId": "Client ID",
+ "clientSecret": "Client Secret",
+ "error": "Ошибка",
+ "warning": "Предупреждение",
+ "deleteAccount": "Удалить учетную запись",
+ "closeDeleteAccount": "Закрыть удаление учетной записи",
+ "cannotDeleteAccount": "Невозможно удалить учетную запись",
+ "confirmPassword": "Подтвердите пароль",
+ "deleting": "Удаление...",
+ "externalAuth": "Внешняя аутентификация (OIDC)",
+ "configureExternalProvider": "Настройте внешнего провайдера идентификации для",
+ "waitingForRetry": "Ожидание повторной попытки",
+ "retryingConnection": "Повторное подключение",
+ "resetSplitSizes": "Сбросить размеры разделения",
+ "sshManagerAlreadyOpen": "SSH-менеджер уже открыт",
+ "disabledDuringSplitScreen": "Отключено во время разделенного экрана",
+ "unknown": "Неизвестно",
+ "connected": "Подключено",
+ "disconnected": "Отключено",
+ "maxRetriesExhausted": "Исчерпаны максимальные попытки",
+ "endpointHostNotFound": "Хост конечной точки не найден",
+ "administrator": "Администратор",
+ "user": "Пользователь",
+ "external": "Внешний",
+ "local": "Локальный",
+ "saving": "Сохранение...",
+ "saveConfiguration": "Сохранить конфигурацию",
+ "loading": "Загрузка...",
+ "refresh": "Обновить",
+ "adding": "Добавление...",
+ "makeAdmin": "Сделать администратором",
+ "verifying": "Проверка...",
+ "verifyAndEnable": "Проверить и включить",
+ "secretKey": "Секретный ключ",
+ "totpQrCode": "TOTP QR-код",
+ "passwordRequired": "Пароль требуется при использовании аутентификации по паролю",
+ "sshKeyRequired": "Приватный SSH-ключ требуется при использовании аутентификации по ключу",
+ "keyTypeRequired": "Тип ключа требуется при использовании аутентификации по ключу",
+ "validSshConfigRequired": "Необходимо выбрать допустимую SSH-конфигурацию из списка",
+ "updateHost": "Обновить хост",
+ "addHost": "Добавить хост",
+ "editHost": "Редактировать хост",
+ "pinConnection": "Закрепить подключение",
+ "authentication": "Аутентификация",
+ "password": "Пароль",
+ "key": "Ключ",
+ "sshPrivateKey": "Приватный SSH-ключ",
+ "keyPassword": "Пароль ключа",
+ "keyType": "Тип ключа",
+ "enableTerminal": "Включить терминал",
+ "enableTunnel": "Включить туннель",
+ "enableFileManager": "Включить файловый менеджер",
+ "defaultPath": "Путь по умолчанию",
+ "tunnelConnections": "Туннельные подключения",
+ "maxRetries": "Макс. попыток",
+ "upload": "Загрузить",
+ "updateKey": "Обновить ключ",
+ "productionFolder": "Продакшен",
+ "databaseServer": "Сервер базы данных",
+ "developmentServer": "Сервер разработки",
+ "developmentFolder": "Разработка",
+ "webServerProduction": "Веб-сервер - Продакшен",
+ "unknownError": "Неизвестная ошибка",
+ "failedToInitiatePasswordReset": "Не удалось инициировать сброс пароля",
+ "failedToVerifyResetCode": "Не удалось проверить код сброса",
+ "failedToCompletePasswordReset": "Не удалось завершить сброс пароля",
+ "invalidTotpCode": "Неверный TOTP-код",
+ "failedToStartOidcLogin": "Не удалось начать вход через OIDC",
+ "failedToGetUserInfoAfterOidc": "Не удалось получить информацию о пользователе после OIDC-входа",
+ "loginWithExternalProvider": "Войти через внешнего провайдера",
+ "loginWithExternal": "Войти через внешнего провайдера",
+ "sendResetCode": "Отправить код сброса",
+ "verifyCode": "Проверить код",
+ "resetPassword": "Сбросить пароль",
+ "login": "Войти",
+ "signUp": "Зарегистрироваться",
+ "failedToUpdateOidcConfig": "Не удалось обновить конфигурацию OIDC",
+ "failedToMakeUserAdmin": "Не удалось сделать пользователя администратором",
+ "failedToStartTotpSetup": "Не удалось начать настройку TOTP",
+ "invalidVerificationCode": "Неверный код проверки",
+ "failedToDisableTotp": "Не удалось отключить TOTP",
+ "failedToGenerateBackupCodes": "Не удалось сгенерировать резервные коды"
+ },
+ "mobile": {
+ "selectHostToStart": "Выберите хост для начала сеанса терминала",
+ "limitedSupportMessage": "Поддержка мобильного веб-сайта все еще в разработке. Используйте мобильное приложение для лучшего опыта.",
+ "mobileAppInProgress": "Мобильное приложение в разработке",
+ "mobileAppInProgressDesc": "Мы работаем над специальным мобильным приложением, чтобы обеспечить лучший опыт на мобильных устройствах.",
+ "viewMobileAppDocs": "Установить мобильное приложение",
+ "mobileAppDocumentation": "Документация мобильного приложения"
+ },
+ "dashboard": {
+ "title": "Панель управления",
+ "github": "GitHub",
+ "support": "Поддержка",
+ "discord": "Discord",
+ "donate": "Пожертвовать",
+ "serverOverview": "Обзор сервера",
+ "version": "Версия",
+ "upToDate": "Обновлено",
+ "updateAvailable": "Доступно обновление",
+ "uptime": "Время работы",
+ "database": "База данных",
+ "healthy": "Работает",
+ "error": "Ошибка",
+ "totalServers": "Всего серверов",
+ "totalTunnels": "Всего туннелей",
+ "totalCredentials": "Всего учетных данных",
+ "recentActivity": "Недавняя активность",
+ "reset": "Сбросить",
+ "loadingRecentActivity": "Загрузка недавней активности...",
+ "noRecentActivity": "Нет недавней активности",
+ "quickActions": "Быстрые действия",
+ "addHost": "Добавить хост",
+ "addCredential": "Добавить учетные данные",
+ "adminSettings": "Настройки администратора",
+ "userProfile": "Профиль пользователя",
+ "serverStats": "Статистика сервера",
+ "loadingServerStats": "Загрузка статистики сервера...",
+ "noServerData": "Данные сервера недоступны",
+ "cpu": "CPU",
+ "ram": "RAM",
+ "notAvailable": "N/A"
+ }
+}
diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json
index cd1c0ef0..e9c7c14e 100644
--- a/src/locales/zh/translation.json
+++ b/src/locales/zh/translation.json
@@ -189,6 +189,40 @@
"enableRightClickCopyPaste": "启用右键复制/粘贴",
"shareIdeas": "对 SSH 工具有什么想法?在此分享"
},
+ "snippets": {
+ "title": "代码片段",
+ "new": "新建片段",
+ "create": "创建代码片段",
+ "edit": "编辑代码片段",
+ "run": "运行",
+ "empty": "暂无代码片段",
+ "emptyHint": "创建代码片段以保存常用命令",
+ "name": "名称",
+ "description": "描述",
+ "content": "命令",
+ "namePlaceholder": "例如: 重启 Nginx",
+ "descriptionPlaceholder": "可选描述",
+ "contentPlaceholder": "例如: sudo systemctl restart nginx",
+ "nameRequired": "名称不能为空",
+ "contentRequired": "命令不能为空",
+ "createDescription": "创建新的命令片段以便快速执行",
+ "editDescription": "编辑此命令片段",
+ "deleteConfirmTitle": "删除代码片段",
+ "deleteConfirmDescription": "确定要删除 \"{{name}}\" 吗?",
+ "createSuccess": "代码片段创建成功",
+ "updateSuccess": "代码片段更新成功",
+ "deleteSuccess": "代码片段删除成功",
+ "createFailed": "创建代码片段失败",
+ "updateFailed": "更新代码片段失败",
+ "deleteFailed": "删除代码片段失败",
+ "failedToFetch": "获取代码片段失败",
+ "executeSuccess": "正在执行: {{name}}",
+ "copySuccess": "已复制 \"{{name}}\" 到剪贴板",
+ "runTooltip": "在终端中执行此片段",
+ "copyTooltip": "复制片段到剪贴板",
+ "editTooltip": "编辑此片段",
+ "deleteTooltip": "删除此片段"
+ },
"homepage": {
"loggedInTitle": "登录成功!",
"loggedInMessage": "您已登录!使用侧边栏访问所有可用工具。要开始使用,请在 SSH 管理器选项卡中创建 SSH 主机。创建后,您可以使用侧边栏中的其他应用程序连接到该主机。",
@@ -311,6 +345,8 @@
"settingUp": "设置中...",
"next": "下一步",
"previous": "上一步",
+ "connect": "连接",
+ "connecting": "连接中...",
"refresh": "刷新",
"settings": "设置",
"profile": "个人资料",
@@ -319,6 +355,8 @@
"language": "语言",
"autoDetect": "自动检测",
"changeAccountPassword": "修改您的账户密码",
+ "passwordResetTitle": "重置密码",
+ "passwordResetDescription": "您即将重置密码。此操作将使您从所有活动会话中注销。",
"enterSixDigitCode": "输入来自 docker 容器日志中用户的 6 位数代码:",
"enterNewPassword": "为用户输入新密码:",
"passwordsDoNotMatch": "密码不匹配",
@@ -343,6 +381,7 @@
"admin": "管理员",
"userProfile": "用户资料",
"tools": "工具",
+ "snippets": "代码片段",
"newTab": "新标签页",
"splitScreen": "分屏",
"closeTab": "关闭标签页",
@@ -396,10 +435,12 @@
"general": "常规",
"userRegistration": "用户注册",
"allowNewAccountRegistration": "允许新账户注册",
+ "allowPasswordLogin": "允许用户名/密码登录",
"missingRequiredFields": "缺少必填字段:{{fields}}",
"oidcConfigurationUpdated": "OIDC 配置更新成功!",
"failedToFetchOidcConfig": "获取 OIDC 配置失败",
"failedToFetchRegistrationStatus": "获取注册状态失败",
+ "failedToFetchPasswordLoginStatus": "获取密码登录状态失败",
"failedToFetchUsers": "获取用户列表失败",
"oidcConfigurationDisabled": "OIDC 配置禁用成功!",
"failedToUpdateOidcConfig": "更新 OIDC 配置失败",
@@ -530,7 +571,17 @@
"passwordRequired": "密码为必填项",
"confirmExport": "确认导出",
"exportDescription": "将SSH主机和凭据导出为SQLite文件",
- "importDescription": "导入SQLite文件并进行增量合并(跳过重复项)"
+ "importDescription": "导入SQLite文件并进行增量合并(跳过重复项)",
+ "criticalWarning": "严重警告",
+ "cannotDisablePasswordLoginWithoutOIDC": "无法在未配置 OIDC 的情况下禁用密码登录!您必须先配置 OIDC 认证,然后再禁用密码登录,否则您将失去对 Termix 的访问权限。",
+ "confirmDisablePasswordLogin": "您确定要禁用密码登录吗?在继续之前,请确保 OIDC 已正确配置并且正常工作,否则您将失去对 Termix 实例的访问权限。",
+ "passwordLoginDisabled": "密码登录已成功禁用",
+ "passwordLoginAndRegistrationDisabled": "密码登录和新账户注册已成功禁用",
+ "requiresPasswordLogin": "需要启用密码登录",
+ "passwordLoginDisabledWarning": "密码登录已禁用。请确保 OIDC 已正确配置,否则您将无法登录 Termix。",
+ "oidcRequiredWarning": "严重警告:密码登录已禁用。如果您重置或错误配置 OIDC,您将失去对 Termix 的所有访问权限并使您的实例无法使用。只有在您完全确定的情况下才能继续。",
+ "confirmDisableOIDCWarning": "警告:您即将在密码登录也已禁用的情况下禁用 OIDC。这将使您的 Termix 实例无法使用,您将失去所有访问权限。您确定要继续吗?",
+ "failedToUpdatePasswordLoginStatus": "更新密码登录状态失败"
},
"hosts": {
"title": "主机管理",
@@ -636,6 +687,7 @@
"password": "密码",
"key": "密钥",
"credential": "凭证",
+ "none": "无",
"selectCredential": "选择凭证",
"selectCredentialPlaceholder": "选择一个凭证...",
"credentialRequired": "使用凭证认证时需要选择凭证",
@@ -691,19 +743,68 @@
"terminal": "终端",
"tunnel": "隧道",
"fileManager": "文件管理器",
+ "serverStats": "服务器统计",
+ "hostViewer": "主机查看器",
+ "enableServerStats": "启用服务器统计",
+ "enableServerStatsDesc": "启用/禁用此主机的服务器统计信息收集",
+ "displayItems": "显示项目",
+ "displayItemsDesc": "选择在服务器统计页面上显示哪些指标",
+ "enableCpu": "CPU使用率",
+ "enableMemory": "内存使用率",
+ "enableDisk": "磁盘使用率",
+ "enableNetwork": "网络统计(即将推出)",
+ "enableProcesses": "进程数(即将推出)",
+ "enableUptime": "运行时间(即将推出)",
+ "enableHostname": "主机名(即将推出)",
+ "enableOs": "操作系统(即将推出)",
+ "customCommands": "自定义命令(即将推出)",
+ "customCommandsDesc": "为此服务器定义自定义关机和重启命令",
+ "shutdownCommand": "关机命令",
+ "rebootCommand": "重启命令",
"confirmRemoveFromFolder": "确定要将\"{{name}}\"从文件夹\"{{folder}}\"中移除吗?主机将被移动到\"无文件夹\"。",
"removedFromFolder": "主机\"{{name}}\"已成功从文件夹中移除",
"failedToRemoveFromFolder": "从文件夹中移除主机失败",
"folderRenamed": "文件夹\"{{oldName}}\"已成功重命名为\"{{newName}}\"",
"failedToRenameFolder": "重命名文件夹失败",
"movedToFolder": "主机\"{{name}}\"已成功移动到\"{{folder}}\"",
- "failedToMoveToFolder": "移动主机到文件夹失败"
+ "failedToMoveToFolder": "移动主机到文件夹失败",
+ "statistics": "统计",
+ "enabledWidgets": "已启用组件",
+ "enabledWidgetsDesc": "选择要为此主机显示的统计组件",
+ "monitoringConfiguration": "监控配置",
+ "monitoringConfigurationDesc": "配置服务器统计信息和状态的检查频率",
+ "statusCheckEnabled": "启用状态监控",
+ "statusCheckEnabledDesc": "检查服务器是在线还是离线",
+ "statusCheckInterval": "状态检查间隔",
+ "statusCheckIntervalDesc": "检查主机是否在线的频率 (5秒 - 1小时)",
+ "metricsEnabled": "启用指标监控",
+ "metricsEnabledDesc": "收集CPU、内存、磁盘和其他系统统计信息",
+ "metricsInterval": "指标收集间隔",
+ "metricsIntervalDesc": "收集服务器统计信息的频率 (5秒 - 1小时)",
+ "intervalSeconds": "秒",
+ "intervalMinutes": "分钟",
+ "intervalValidation": "监控间隔必须在 5 秒到 1 小时(3600 秒)之间",
+ "monitoringDisabled": "此主机的服务器监控已禁用",
+ "enableMonitoring": "在主机管理器 → 统计选项卡中启用监控",
+ "monitoringDisabledBadge": "监控已关闭",
+ "statusMonitoring": "状态",
+ "metricsMonitoring": "指标",
+ "terminalCustomizationNotice": "注意:终端自定义仅在桌面网站版本中有效。移动和 Electron 应用程序使用系统默认终端设置。",
+ "noneAuthTitle": "键盘交互式认证",
+ "noneAuthDescription": "此认证方法在连接到 SSH 服务器时将使用键盘交互式认证。",
+ "noneAuthDetails": "键盘交互式认证允许服务器在连接期间提示您输入凭据。这对于需要多因素认证或动态密码输入的服务器很有用。",
+ "forceKeyboardInteractive": "强制键盘交互式认证",
+ "forceKeyboardInteractiveDesc": "强制使用键盘交互式认证。这通常是使用双因素认证(TOTP/2FA)的服务器所必需的。"
},
"terminal": {
"title": "终端",
"terminalTitle": "终端 - {{host}}",
"terminalWithPath": "终端 - {{host}}:{{path}}",
"runTitle": "运行 {{command}} - {{host}}",
+ "totpRequired": "需要双因素认证",
+ "totpCodeLabel": "验证码",
+ "totpPlaceholder": "000000",
+ "totpVerify": "验证",
"connect": "连接主机",
"disconnect": "断开连接",
"clear": "清屏",
@@ -985,7 +1086,9 @@
"fileComparison": "文件对比:{{file1}} 与 {{file2}}",
"fileTooLarge": "文件过大:{{error}}",
"sshConnectionFailed": "SSH 连接失败。请检查与 {{name}} ({{ip}}:{{port}}) 的连接",
- "loadFileFailed": "加载文件失败:{{error}}"
+ "loadFileFailed": "加载文件失败:{{error}}",
+ "connectedSuccessfully": "连接成功",
+ "totpVerificationFailed": "TOTP 验证失败"
},
"tunnels": {
"title": "SSH 隧道",
@@ -1063,6 +1166,7 @@
"loadAverageNA": "平均: N/A",
"cpuUsage": "CPU 使用率",
"memoryUsage": "内存使用率",
+ "diskUsage": "磁盘使用率",
"rootStorageSpace": "根目录存储空间",
"of": "的",
"feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧",
@@ -1073,7 +1177,29 @@
"refreshing": "正在刷新...",
"serverOffline": "服务器离线",
"cannotFetchMetrics": "无法从离线服务器获取指标",
- "load": "负载"
+ "totpRequired": "需要 TOTP 认证",
+ "totpUnavailable": "启用了 TOTP 的服务器无法使用服务器统计功能",
+ "load": "负载",
+ "free": "空闲",
+ "available": "可用",
+ "editLayout": "编辑布局",
+ "cancelEdit": "取消",
+ "addWidget": "添加小组件",
+ "saveLayout": "保存布局",
+ "unsavedChanges": "有未保存的更改",
+ "layoutSaved": "布局保存成功",
+ "failedToSaveLayout": "保存布局失败",
+ "systemInfo": "系统信息",
+ "hostname": "主机名",
+ "operatingSystem": "操作系统",
+ "kernel": "内核",
+ "totalUptime": "总运行时间",
+ "seconds": "秒",
+ "networkInterfaces": "网络接口",
+ "noInterfacesFound": "未找到网络接口",
+ "totalProcesses": "总进程数",
+ "running": "运行中",
+ "noProcessesFound": "未找到进程"
},
"auth": {
"loginTitle": "登录 Termix",
@@ -1157,7 +1283,18 @@
"enterNewPassword": "为用户输入新密码:",
"passwordResetSuccess": "成功!",
"passwordResetSuccessDesc": "您的密码已成功重置!您现在可以使用新密码登录。",
- "signUp": "注册"
+ "signUp": "注册",
+ "dataLossWarning": "以这种方式重置密码将删除所有已保存的 SSH 主机、凭据和其他加密数据。此操作无法撤销。仅当您忘记密码且未登录时才使用此功能。",
+ "sshAuthenticationRequired": "需要 SSH 身份验证",
+ "sshNoKeyboardInteractive": "键盘交互式身份验证不可用",
+ "sshAuthenticationFailed": "身份验证失败",
+ "sshAuthenticationTimeout": "身份验证超时",
+ "sshNoKeyboardInteractiveDescription": "服务器不支持键盘交互式身份验证。请提供您的密码或 SSH 密钥。",
+ "sshAuthFailedDescription": "提供的凭据不正确。请使用有效凭据重试。",
+ "sshTimeoutDescription": "身份验证尝试超时。请重试。",
+ "sshProvideCredentialsDescription": "请提供您的 SSH 凭据以连接到此服务器。",
+ "sshPasswordDescription": "输入此 SSH 连接的密码。",
+ "sshKeyPasswordDescription": "如果您的 SSH 密钥已加密,请在此处输入密码。"
},
"errors": {
"notFound": "页面未找到",
@@ -1183,6 +1320,7 @@
"maxLength": "最大长度为 {{max}}",
"invalidEmail": "邮箱地址无效",
"passwordMismatch": "密码不匹配",
+ "passwordLoginDisabled": "用户名/密码登录当前已禁用",
"weakPassword": "密码强度太弱",
"usernameExists": "用户名已存在",
"emailExists": "邮箱已存在",
@@ -1228,7 +1366,10 @@
"authMethod": "认证方式",
"local": "本地",
"external": "外部 (OIDC)",
- "selectPreferredLanguage": "选择您的界面首选语言"
+ "selectPreferredLanguage": "选择您的界面首选语言",
+ "currentPassword": "当前密码",
+ "passwordChangedSuccess": "密码修改成功!请重新登录。",
+ "failedToChangePassword": "修改密码失败。请检查您当前的密码并重试。"
},
"user": {
"failedToLoadVersionInfo": "加载版本信息失败"
@@ -1287,6 +1428,7 @@
"closeDeleteAccount": "关闭删除账户",
"deleteAccountWarning": "此操作无法撤销。这将永久删除您的账户和所有相关数据。",
"deleteAccountWarningDetails": "删除您的账户将删除所有数据,包括 SSH 主机、配置和设置。此操作不可逆。",
+ "deleteAccountWarningShort": "此操作不可逆,将永久删除您的帐户。",
"cannotDeleteAccount": "无法删除账户",
"lastAdminWarning": "您是最后一个管理员用户。您不能删除自己的账户,否则系统将没有任何管理员。请先将其他用户设为管理员,或联系系统支持。",
"confirmPassword": "确认密码",
@@ -1337,5 +1479,38 @@
"mobileAppInProgressDesc": "我们正在开发专门的移动应用,为移动设备提供更好的体验。",
"viewMobileAppDocs": "安装移动应用",
"mobileAppDocumentation": "移动应用文档"
+ },
+ "dashboard": {
+ "title": "仪表板",
+ "github": "GitHub",
+ "support": "支持",
+ "discord": "Discord",
+ "donate": "捐赠",
+ "serverOverview": "服务器概览",
+ "version": "版本",
+ "upToDate": "已是最新",
+ "updateAvailable": "有可用更新",
+ "uptime": "运行时间",
+ "database": "数据库",
+ "healthy": "健康",
+ "error": "错误",
+ "totalServers": "服务器总数",
+ "totalTunnels": "隧道总数",
+ "totalCredentials": "凭据总数",
+ "recentActivity": "最近活动",
+ "reset": "重置",
+ "loadingRecentActivity": "正在加载最近活动...",
+ "noRecentActivity": "无最近活动",
+ "quickActions": "快速操作",
+ "addHost": "添加主机",
+ "addCredential": "添加凭据",
+ "adminSettings": "管理员设置",
+ "userProfile": "用户资料",
+ "serverStats": "服务器统计",
+ "loadingServerStats": "正在加载服务器统计...",
+ "noServerData": "无可用服务器数据",
+ "cpu": "CPU",
+ "ram": "内存",
+ "notAvailable": "不可用"
}
}
diff --git a/src/main.tsx b/src/main.tsx
index 55a6815f..53ff210e 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,15 +1,16 @@
+/* eslint-disable react-refresh/only-export-components */
import { StrictMode, useEffect, useState, useRef } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
-import DesktopApp from "./ui/Desktop/DesktopApp.tsx";
-import { MobileApp } from "./ui/Mobile/MobileApp.tsx";
+import DesktopApp from "@/ui/desktop/DesktopApp.tsx";
+import { MobileApp } from "@/ui/mobile/MobileApp.tsx";
import { ThemeProvider } from "@/components/theme-provider";
+import { ElectronVersionCheck } from "@/ui/desktop/user/ElectronVersionCheck.tsx";
import "./i18n/i18n";
import { isElectron } from "./ui/main-axios.ts";
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
- const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const lastSwitchTime = useRef(0);
const isCurrentlyMobile = useRef(window.innerWidth < 768);
const hasSwitchedOnce = useRef(false);
@@ -36,7 +37,6 @@ function useWindowWidth() {
isCurrentlyMobile.current = newIsMobile;
hasSwitchedOnce.current = true;
setWidth(newWidth);
- setIsMobile(newIsMobile);
} else {
setWidth(newWidth);
}
@@ -56,11 +56,55 @@ function useWindowWidth() {
function RootApp() {
const width = useWindowWidth();
const isMobile = width < 768;
- if (isElectron()) {
- return ;
- }
+ const [showVersionCheck, setShowVersionCheck] = useState(true);
- return isMobile ? : ;
+ const userAgent =
+ navigator.userAgent || navigator.vendor || (window as any).opera || "";
+ const isTermixMobile = /Termix-Mobile/.test(userAgent);
+
+ const renderApp = () => {
+ if (isElectron()) {
+ return ;
+ }
+
+ if (isTermixMobile) {
+ return ;
+ }
+
+ return isMobile ? : ;
+ };
+
+ return (
+ <>
+
+
+ {isElectron() && showVersionCheck ? (
+ setShowVersionCheck(false)}
+ isAuthenticated={false}
+ />
+ ) : (
+ renderApp()
+ )}
+
+ >
+ );
}
createRoot(document.getElementById("root")!).render(
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index 6a544f20..ec3321e4 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -1,22 +1,49 @@
+interface ServerConfig {
+ serverUrl?: string;
+ [key: string]: unknown;
+}
+
+interface ConnectionTestResult {
+ success: boolean;
+ error?: string;
+ [key: string]: unknown;
+}
+
+interface DialogOptions {
+ title?: string;
+ defaultPath?: string;
+ buttonLabel?: string;
+ filters?: Array<{ name: string; extensions: string[] }>;
+ properties?: string[];
+ [key: string]: unknown;
+}
+
+interface DialogResult {
+ canceled: boolean;
+ filePath?: string;
+ filePaths?: string[];
+ [key: string]: unknown;
+}
+
export interface ElectronAPI {
getAppVersion: () => Promise;
getPlatform: () => Promise;
- getServerConfig: () => Promise;
- saveServerConfig: (config: any) => Promise;
- testServerConnection: (serverUrl: string) => Promise;
+ getServerConfig: () => Promise;
+ saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>;
+ testServerConnection: (serverUrl: string) => Promise;
- showSaveDialog: (options: any) => Promise;
- showOpenDialog: (options: any) => Promise;
+ showSaveDialog: (options: DialogOptions) => Promise;
+ showOpenDialog: (options: DialogOptions) => Promise;
- onUpdateAvailable: (callback: Function) => void;
- onUpdateDownloaded: (callback: Function) => void;
+ onUpdateAvailable: (callback: () => void) => void;
+ onUpdateDownloaded: (callback: () => void) => void;
removeAllListeners: (channel: string) => void;
isElectron: boolean;
isDev: boolean;
- invoke: (channel: string, ...args: any[]) => Promise;
+ invoke: (channel: string, ...args: unknown[]) => Promise;
createTempFile: (fileData: {
fileName: string;
diff --git a/src/types/index.ts b/src/types/index.ts
index ee7cedb2..027de232 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,9 +1,5 @@
-// ============================================================================
-// CENTRAL TYPE DEFINITIONS
-// ============================================================================
-// This file contains all shared interfaces and types used across the application
-
import type { Client } from "ssh2";
+import type { Request } from "express";
// ============================================================================
// SSH HOST TYPES
@@ -18,11 +14,12 @@ export interface SSHHost {
folder: string;
tags: string[];
pin: boolean;
- authType: "password" | "key" | "credential";
+ authType: "password" | "key" | "credential" | "none";
password?: string;
key?: string;
keyPassword?: string;
keyType?: string;
+ forceKeyboardInteractive?: boolean;
autostartPassword?: string;
autostartKey?: string;
@@ -35,6 +32,8 @@ export interface SSHHost {
enableFileManager: boolean;
defaultPath: string;
tunnelConnections: TunnelConnection[];
+ statsConfig?: string;
+ terminalConfig?: TerminalConfig;
createdAt: string;
updatedAt: string;
}
@@ -47,7 +46,7 @@ export interface SSHHostData {
folder?: string;
tags?: string[];
pin?: boolean;
- authType: "password" | "key" | "credential";
+ authType: "password" | "key" | "credential" | "none";
password?: string;
key?: File | null;
keyPassword?: string;
@@ -57,7 +56,10 @@ export interface SSHHostData {
enableTunnel?: boolean;
enableFileManager?: boolean;
defaultPath?: string;
- tunnelConnections?: any[];
+ forceKeyboardInteractive?: boolean;
+ tunnelConnections?: TunnelConnection[];
+ statsConfig?: string | Record;
+ terminalConfig?: TerminalConfig;
}
// ============================================================================
@@ -106,7 +108,6 @@ export interface TunnelConnection {
endpointPort: number;
endpointHost: string;
- // Endpoint host credentials for tunnel authentication
endpointPassword?: string;
endpointKey?: string;
endpointKeyPassword?: string;
@@ -246,6 +247,34 @@ export interface TermixAlert {
actionText?: string;
}
+// ============================================================================
+// TERMINAL CONFIGURATION TYPES
+// ============================================================================
+
+export interface TerminalConfig {
+ cursorBlink: boolean;
+ cursorStyle: "block" | "underline" | "bar";
+ fontSize: number;
+ fontFamily: string;
+ letterSpacing: number;
+ lineHeight: number;
+ theme: string;
+
+ scrollback: number;
+ bellStyle: "none" | "sound" | "visual" | "both";
+ rightClickSelectsWord: boolean;
+ fastScrollModifier: "alt" | "ctrl" | "shift";
+ fastScrollSensitivity: number;
+ minimumContrastRatio: number;
+
+ backspaceMode: "normal" | "control-h";
+ agentForwarding: boolean;
+ environmentVariables: Array<{ key: string; value: string }>;
+ startupSnippetId: number | null;
+ autoMosh: boolean;
+ moshCommand: string;
+}
+
// ============================================================================
// TAB TYPES
// ============================================================================
@@ -261,8 +290,9 @@ export interface TabContextTab {
| "file_manager"
| "user_profile";
title: string;
- hostConfig?: any;
- terminalRef?: React.RefObject;
+ hostConfig?: SSHHost;
+ terminalRef?: any;
+ initialTab?: string;
}
// ============================================================================
@@ -295,7 +325,7 @@ export type ErrorType =
// AUTHENTICATION TYPES
// ============================================================================
-export type AuthType = "password" | "key" | "credential";
+export type AuthType = "password" | "key" | "credential" | "none";
export type KeyType = "rsa" | "ecdsa" | "ed25519";
@@ -303,7 +333,7 @@ export type KeyType = "rsa" | "ecdsa" | "ed25519";
// API RESPONSE TYPES
// ============================================================================
-export interface ApiResponse {
+export interface ApiResponse {
data?: T;
error?: string;
message?: string;
@@ -337,6 +367,8 @@ export interface CredentialSelectorProps {
export interface HostManagerProps {
onSelectView?: (view: string) => void;
isTopbarOpen?: boolean;
+ initialTab?: string;
+ hostConfig?: SSHHost;
}
export interface SSHManagerHostEditorProps {
@@ -366,13 +398,13 @@ export interface SSHTunnelViewerProps {
action: "connect" | "disconnect" | "cancel",
host: SSHHost,
tunnelIndex: number,
- ) => Promise
+ ) => Promise
>;
onTunnelAction?: (
action: "connect" | "disconnect" | "cancel",
host: SSHHost,
tunnelIndex: number,
- ) => Promise;
+ ) => Promise;
}
export interface FileManagerProps {
@@ -400,7 +432,7 @@ export interface SSHTunnelObjectProps {
action: "connect" | "disconnect" | "cancel",
host: SSHHost,
tunnelIndex: number,
- ) => Promise;
+ ) => Promise;
compact?: boolean;
bare?: boolean;
}
@@ -413,6 +445,26 @@ export interface FolderStats {
}>;
}
+// ============================================================================
+// SNIPPETS TYPES
+// ============================================================================
+
+export interface Snippet {
+ id: number;
+ userId: string;
+ name: string;
+ content: string;
+ description?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface SnippetData {
+ name: string;
+ content: string;
+ description?: string;
+}
+
// ============================================================================
// BACKEND TYPES
// ============================================================================
@@ -439,3 +491,95 @@ export type Optional = Omit & Partial>;
export type RequiredFields = T & Required>;
export type PartialExcept = Partial & Pick;
+
+// ============================================================================
+// EXPRESS REQUEST TYPES
+// ============================================================================
+
+export interface AuthenticatedRequest extends Request {
+ userId: string;
+ user?: {
+ id: string;
+ username: string;
+ isAdmin: boolean;
+ };
+}
+
+// ============================================================================
+// GITHUB API TYPES
+// ============================================================================
+
+export interface GitHubAsset {
+ id: number;
+ name: string;
+ size: number;
+ download_count: number;
+ browser_download_url: string;
+}
+
+export interface GitHubRelease {
+ id: number;
+ tag_name: string;
+ name: string;
+ body: string;
+ published_at: string;
+ html_url: string;
+ assets: GitHubAsset[];
+ prerelease: boolean;
+ draft: boolean;
+}
+
+export interface GitHubAPIResponse {
+ data: T;
+ cached: boolean;
+ cache_age?: number;
+ timestamp?: number;
+}
+
+// ============================================================================
+// CACHE TYPES
+// ============================================================================
+
+export interface CacheEntry {
+ data: T;
+ timestamp: number;
+ expiresAt: number;
+}
+
+// ============================================================================
+// DATABASE EXPORT/IMPORT TYPES
+// ============================================================================
+
+export interface ExportSummary {
+ sshHostsImported: number;
+ sshCredentialsImported: number;
+ fileManagerItemsImported: number;
+ dismissedAlertsImported: number;
+ credentialUsageImported: number;
+ settingsImported: number;
+ skippedItems: number;
+ errors: string[];
+}
+
+export interface ImportResult {
+ success: boolean;
+ summary: ExportSummary;
+}
+
+export interface ExportRequestBody {
+ password: string;
+}
+
+export interface ImportRequestBody {
+ password: string;
+}
+
+export interface ExportPreviewBody {
+ scope?: string;
+ includeCredentials?: boolean;
+}
+
+export interface RestoreRequestBody {
+ backupPath: string;
+ targetPath?: string;
+}
diff --git a/src/types/stats-widgets.ts b/src/types/stats-widgets.ts
new file mode 100644
index 00000000..eb450aa7
--- /dev/null
+++ b/src/types/stats-widgets.ts
@@ -0,0 +1,24 @@
+export type WidgetType =
+ | "cpu"
+ | "memory"
+ | "disk"
+ | "network"
+ | "uptime"
+ | "processes"
+ | "system";
+
+export interface StatsConfig {
+ enabledWidgets: WidgetType[];
+ statusCheckEnabled: boolean;
+ statusCheckInterval: number;
+ metricsEnabled: boolean;
+ metricsInterval: number;
+}
+
+export const DEFAULT_STATS_CONFIG: StatsConfig = {
+ enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"],
+ statusCheckEnabled: true,
+ statusCheckInterval: 30,
+ metricsEnabled: true,
+ metricsInterval: 30,
+};
diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx
deleted file mode 100644
index 0fe881ef..00000000
--- a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx
+++ /dev/null
@@ -1,1550 +0,0 @@
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Controller, useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { Button } from "@/components/ui/button.tsx";
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
-} from "@/components/ui/form.tsx";
-import { Input } from "@/components/ui/input.tsx";
-import { PasswordInput } from "@/components/ui/password-input.tsx";
-import { ScrollArea } from "@/components/ui/scroll-area.tsx";
-import { Separator } from "@/components/ui/separator.tsx";
-import {
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
-} from "@/components/ui/tabs.tsx";
-import React, { useEffect, useRef, useState } from "react";
-import { Switch } from "@/components/ui/switch.tsx";
-import { Alert, AlertDescription } from "@/components/ui/alert.tsx";
-import { toast } from "sonner";
-import {
- createSSHHost,
- getCredentials,
- getSSHHosts,
- updateSSHHost,
- enableAutoStart,
- disableAutoStart,
-} from "@/ui/main-axios.ts";
-import { useTranslation } from "react-i18next";
-import { CredentialSelector } from "@/ui/Desktop/Apps/Credentials/CredentialSelector.tsx";
-import CodeMirror from "@uiw/react-codemirror";
-import { oneDark } from "@codemirror/theme-one-dark";
-import { EditorView } from "@codemirror/view";
-
-interface SSHHost {
- id: number;
- name: string;
- ip: string;
- port: number;
- username: string;
- folder: string;
- tags: string[];
- pin: boolean;
- authType: string;
- password?: string;
- key?: string;
- keyPassword?: string;
- keyType?: string;
- enableTerminal: boolean;
- enableTunnel: boolean;
- enableFileManager: boolean;
- defaultPath: string;
- tunnelConnections: any[];
- createdAt: string;
- updatedAt: string;
- credentialId?: number;
-}
-
-interface SSHManagerHostEditorProps {
- editingHost?: SSHHost | null;
- onFormSubmit?: (updatedHost?: SSHHost) => void;
-}
-
-export function HostManagerEditor({
- editingHost,
- onFormSubmit,
-}: SSHManagerHostEditorProps) {
- const { t } = useTranslation();
- const [hosts, setHosts] = useState([]);
- const [folders, setFolders] = useState([]);
- const [sshConfigurations, setSshConfigurations] = useState([]);
- const [credentials, setCredentials] = useState([]);
- const [loading, setLoading] = useState(true);
-
- const [authTab, setAuthTab] = useState<"password" | "key" | "credential">(
- "password",
- );
- const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">(
- "upload",
- );
- const isSubmittingRef = useRef(false);
-
- const ipInputRef = useRef(null);
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- setLoading(true);
- const [hostsData, credentialsData] = await Promise.all([
- getSSHHosts(),
- getCredentials(),
- ]);
- setHosts(hostsData);
- setCredentials(credentialsData);
-
- const uniqueFolders = [
- ...new Set(
- hostsData
- .filter((host) => host.folder && host.folder.trim() !== "")
- .map((host) => host.folder),
- ),
- ].sort();
-
- const uniqueConfigurations = [
- ...new Set(
- hostsData
- .filter((host) => host.name && host.name.trim() !== "")
- .map((host) => host.name),
- ),
- ].sort();
-
- setFolders(uniqueFolders);
- setSshConfigurations(uniqueConfigurations);
- } catch (error) {
- } finally {
- setLoading(false);
- }
- };
-
- fetchData();
- }, []);
-
- useEffect(() => {
- const handleCredentialChange = async () => {
- try {
- setLoading(true);
- const hostsData = await getSSHHosts();
- setHosts(hostsData);
-
- const uniqueFolders = [
- ...new Set(
- hostsData
- .filter((host) => host.folder && host.folder.trim() !== "")
- .map((host) => host.folder),
- ),
- ].sort();
-
- const uniqueConfigurations = [
- ...new Set(
- hostsData
- .filter((host) => host.name && host.name.trim() !== "")
- .map((host) => host.name),
- ),
- ].sort();
-
- setFolders(uniqueFolders);
- setSshConfigurations(uniqueConfigurations);
- } catch (error) {
- } finally {
- setLoading(false);
- }
- };
-
- window.addEventListener("credentials:changed", handleCredentialChange);
-
- return () => {
- window.removeEventListener("credentials:changed", handleCredentialChange);
- };
- }, []);
-
- const formSchema = z
- .object({
- name: z.string().optional(),
- ip: z.string().min(1),
- port: z.coerce.number().min(1).max(65535),
- username: z.string().min(1),
- folder: z.string().optional(),
- tags: z.array(z.string().min(1)).default([]),
- pin: z.boolean().default(false),
- authType: z.enum(["password", "key", "credential"]),
- credentialId: z.number().optional().nullable(),
- password: z.string().optional(),
- key: z.any().optional().nullable(),
- keyPassword: z.string().optional(),
- keyType: z
- .enum([
- "auto",
- "ssh-rsa",
- "ssh-ed25519",
- "ecdsa-sha2-nistp256",
- "ecdsa-sha2-nistp384",
- "ecdsa-sha2-nistp521",
- "ssh-dss",
- "ssh-rsa-sha2-256",
- "ssh-rsa-sha2-512",
- ])
- .optional(),
- enableTerminal: z.boolean().default(true),
- enableTunnel: z.boolean().default(true),
- tunnelConnections: z
- .array(
- z.object({
- sourcePort: z.coerce.number().min(1).max(65535),
- endpointPort: z.coerce.number().min(1).max(65535),
- endpointHost: z.string().min(1),
- maxRetries: z.coerce.number().min(0).max(100).default(3),
- retryInterval: z.coerce.number().min(1).max(3600).default(10),
- autoStart: z.boolean().default(false),
- }),
- )
- .default([]),
- enableFileManager: z.boolean().default(true),
- defaultPath: z.string().optional(),
- })
- .superRefine((data, ctx) => {
- if (data.authType === "password") {
- if (
- !data.password ||
- (typeof data.password === "string" && data.password.trim() === "")
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t("hosts.passwordRequired"),
- path: ["password"],
- });
- }
- } else if (data.authType === "key") {
- if (
- !data.key ||
- (typeof data.key === "string" && data.key.trim() === "")
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t("hosts.sshKeyRequired"),
- path: ["key"],
- });
- }
- if (!data.keyType) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t("hosts.keyTypeRequired"),
- path: ["keyType"],
- });
- }
- } else if (data.authType === "credential") {
- if (
- !data.credentialId ||
- (typeof data.credentialId === "string" &&
- data.credentialId.trim() === "")
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t("hosts.credentialRequired"),
- path: ["credentialId"],
- });
- }
- }
-
- data.tunnelConnections.forEach((connection, index) => {
- if (
- connection.endpointHost &&
- !sshConfigurations.includes(connection.endpointHost)
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t("hosts.mustSelectValidSshConfig"),
- path: ["tunnelConnections", index, "endpointHost"],
- });
- }
- });
- });
-
- type FormData = z.infer;
-
- const form = useForm({
- resolver: zodResolver(formSchema) as any,
- defaultValues: {
- name: "",
- ip: "",
- port: 22,
- username: "",
- folder: "",
- tags: [],
- pin: false,
- authType: "password" as const,
- credentialId: null,
- password: "",
- key: null,
- keyPassword: "",
- keyType: "auto" as const,
- enableTerminal: true,
- enableTunnel: true,
- enableFileManager: true,
- defaultPath: "/",
- tunnelConnections: [],
- },
- });
-
- useEffect(() => {
- if (authTab === "credential") {
- const currentCredentialId = form.getValues("credentialId");
- if (currentCredentialId) {
- const selectedCredential = credentials.find(
- (c) => c.id === currentCredentialId,
- );
- if (selectedCredential) {
- form.setValue("username", selectedCredential.username);
- }
- }
- }
- }, [authTab, credentials, form]);
-
- useEffect(() => {
- if (editingHost) {
- const cleanedHost = { ...editingHost };
- if (cleanedHost.credentialId && cleanedHost.key) {
- cleanedHost.key = undefined;
- cleanedHost.keyPassword = undefined;
- cleanedHost.keyType = undefined;
- } else if (cleanedHost.credentialId && cleanedHost.password) {
- cleanedHost.password = undefined;
- } else if (cleanedHost.key && cleanedHost.password) {
- cleanedHost.password = undefined;
- }
-
- const defaultAuthType = cleanedHost.credentialId
- ? "credential"
- : cleanedHost.key
- ? "key"
- : "password";
- setAuthTab(defaultAuthType);
-
- const formData = {
- name: cleanedHost.name || "",
- ip: cleanedHost.ip || "",
- port: cleanedHost.port || 22,
- username: cleanedHost.username || "",
- folder: cleanedHost.folder || "",
- tags: cleanedHost.tags || [],
- pin: Boolean(cleanedHost.pin),
- authType: defaultAuthType as "password" | "key" | "credential",
- credentialId: null,
- password: "",
- key: null,
- keyPassword: "",
- keyType: "auto" as const,
- enableTerminal: Boolean(cleanedHost.enableTerminal),
- enableTunnel: Boolean(cleanedHost.enableTunnel),
- enableFileManager: Boolean(cleanedHost.enableFileManager),
- defaultPath: cleanedHost.defaultPath || "/",
- tunnelConnections: cleanedHost.tunnelConnections || [],
- };
-
- if (defaultAuthType === "password") {
- formData.password = cleanedHost.password || "";
- } else if (defaultAuthType === "key") {
- formData.key = editingHost.id ? "existing_key" : editingHost.key;
- formData.keyPassword = cleanedHost.keyPassword || "";
- formData.keyType = (cleanedHost.keyType as any) || "auto";
- } else if (defaultAuthType === "credential") {
- formData.credentialId =
- cleanedHost.credentialId || "existing_credential";
- }
-
- form.reset(formData);
- } else {
- setAuthTab("password");
- const defaultFormData = {
- name: "",
- ip: "",
- port: 22,
- username: "",
- folder: "",
- tags: [],
- pin: false,
- authType: "password" as const,
- credentialId: null,
- password: "",
- key: null,
- keyPassword: "",
- keyType: "auto" as const,
- enableTerminal: true,
- enableTunnel: true,
- enableFileManager: true,
- defaultPath: "/",
- tunnelConnections: [],
- };
-
- form.reset(defaultFormData);
- }
- }, [editingHost?.id]);
-
- useEffect(() => {
- const focusTimer = setTimeout(() => {
- if (ipInputRef.current) {
- ipInputRef.current.focus();
- }
- }, 300);
-
- return () => clearTimeout(focusTimer);
- }, [editingHost]);
-
- const onSubmit = async (data: FormData) => {
- try {
- isSubmittingRef.current = true;
-
- if (!data.name || data.name.trim() === "") {
- data.name = `${data.username}@${data.ip}`;
- }
-
- const submitData: any = {
- name: data.name,
- ip: data.ip,
- port: data.port,
- username: data.username,
- folder: data.folder || "",
- tags: data.tags || [],
- pin: Boolean(data.pin),
- authType: data.authType,
- enableTerminal: Boolean(data.enableTerminal),
- enableTunnel: Boolean(data.enableTunnel),
- enableFileManager: Boolean(data.enableFileManager),
- defaultPath: data.defaultPath || "/",
- tunnelConnections: data.tunnelConnections || [],
- };
-
- submitData.credentialId = null;
- submitData.password = null;
- submitData.key = null;
- submitData.keyPassword = null;
- submitData.keyType = null;
-
- if (data.authType === "credential") {
- if (
- data.credentialId === "existing_credential" &&
- editingHost &&
- editingHost.id
- ) {
- delete submitData.credentialId;
- } else {
- submitData.credentialId = data.credentialId;
- }
- } else if (data.authType === "password") {
- submitData.password = data.password;
- } else if (data.authType === "key") {
- if (data.key instanceof File) {
- const keyContent = await data.key.text();
- submitData.key = keyContent;
- } else if (data.key === "existing_key") {
- delete submitData.key;
- } else {
- submitData.key = data.key;
- }
- submitData.keyPassword = data.keyPassword;
- submitData.keyType = data.keyType;
- }
-
- let savedHost;
- if (editingHost && editingHost.id) {
- savedHost = await updateSSHHost(editingHost.id, submitData);
- toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name }));
- } else {
- savedHost = await createSSHHost(submitData);
- toast.success(t("hosts.hostAddedSuccessfully", { name: data.name }));
- }
-
- if (savedHost && savedHost.id && data.tunnelConnections) {
- const hasAutoStartTunnels = data.tunnelConnections.some(
- (tunnel) => tunnel.autoStart,
- );
-
- if (hasAutoStartTunnels) {
- try {
- await enableAutoStart(savedHost.id);
- } catch (error) {
- console.warn(
- `Failed to enable AutoStart plaintext cache for SSH host ${savedHost.id}:`,
- error,
- );
- toast.warning(
- t("hosts.autoStartEnableFailed", { name: data.name }),
- );
- }
- } else {
- try {
- await disableAutoStart(savedHost.id);
- } catch (error) {
- console.warn(
- `Failed to disable AutoStart plaintext cache for SSH host ${savedHost.id}:`,
- error,
- );
- }
- }
- }
-
- if (onFormSubmit) {
- onFormSubmit(savedHost);
- }
-
- window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
-
- form.reset();
- } catch (error) {
- toast.error(t("hosts.failedToSaveHost"));
- } finally {
- isSubmittingRef.current = false;
- }
- };
-
- const [tagInput, setTagInput] = useState("");
-
- const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
- const folderInputRef = useRef(null);
- const folderDropdownRef = useRef(null);
-
- const folderValue = form.watch("folder");
- const filteredFolders = React.useMemo(() => {
- if (!folderValue) return folders;
- return folders.filter((f) =>
- f.toLowerCase().includes(folderValue.toLowerCase()),
- );
- }, [folderValue, folders]);
-
- const handleFolderClick = (folder: string) => {
- form.setValue("folder", folder);
- setFolderDropdownOpen(false);
- };
-
- useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (
- folderDropdownRef.current &&
- !folderDropdownRef.current.contains(event.target as Node) &&
- folderInputRef.current &&
- !folderInputRef.current.contains(event.target as Node)
- ) {
- setFolderDropdownOpen(false);
- }
- }
-
- if (folderDropdownOpen) {
- document.addEventListener("mousedown", handleClickOutside);
- } else {
- document.removeEventListener("mousedown", handleClickOutside);
- }
-
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
- }, [folderDropdownOpen]);
-
- const keyTypeOptions = [
- { value: "auto", label: t("hosts.autoDetect") },
- { value: "ssh-rsa", label: t("hosts.rsa") },
- { value: "ssh-ed25519", label: t("hosts.ed25519") },
- { value: "ecdsa-sha2-nistp256", label: t("hosts.ecdsaNistP256") },
- { value: "ecdsa-sha2-nistp384", label: t("hosts.ecdsaNistP384") },
- { value: "ecdsa-sha2-nistp521", label: t("hosts.ecdsaNistP521") },
- { value: "ssh-dss", label: t("hosts.dsa") },
- { value: "ssh-rsa-sha2-256", label: t("hosts.rsaSha2256") },
- { value: "ssh-rsa-sha2-512", label: t("hosts.rsaSha2512") },
- ];
-
- const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false);
- const keyTypeButtonRef = useRef(null);
- const keyTypeDropdownRef = useRef(null);
-
- useEffect(() => {
- function onClickOutside(event: MouseEvent) {
- if (
- keyTypeDropdownOpen &&
- keyTypeDropdownRef.current &&
- !keyTypeDropdownRef.current.contains(event.target as Node) &&
- keyTypeButtonRef.current &&
- !keyTypeButtonRef.current.contains(event.target as Node)
- ) {
- setKeyTypeDropdownOpen(false);
- }
- }
-
- document.addEventListener("mousedown", onClickOutside);
- return () => document.removeEventListener("mousedown", onClickOutside);
- }, [keyTypeDropdownOpen]);
-
- const [sshConfigDropdownOpen, setSshConfigDropdownOpen] = useState<{
- [key: number]: boolean;
- }>({});
- const sshConfigInputRefs = useRef<{ [key: number]: HTMLInputElement | null }>(
- {},
- );
- const sshConfigDropdownRefs = useRef<{
- [key: number]: HTMLDivElement | null;
- }>({});
-
- const getFilteredSshConfigs = (index: number) => {
- const value = form.watch(`tunnelConnections.${index}.endpointHost`);
-
- const currentHostName =
- form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`;
-
- let filtered = sshConfigurations.filter(
- (config) => config !== currentHostName,
- );
-
- if (value) {
- filtered = filtered.filter((config) =>
- config.toLowerCase().includes(value.toLowerCase()),
- );
- }
-
- return filtered;
- };
-
- const handleSshConfigClick = (config: string, index: number) => {
- form.setValue(`tunnelConnections.${index}.endpointHost`, config);
- setSshConfigDropdownOpen((prev) => ({ ...prev, [index]: false }));
- };
-
- useEffect(() => {
- function handleSshConfigClickOutside(event: MouseEvent) {
- const openDropdowns = Object.keys(sshConfigDropdownOpen).filter(
- (key) => sshConfigDropdownOpen[parseInt(key)],
- );
-
- openDropdowns.forEach((indexStr: string) => {
- const index = parseInt(indexStr);
- if (
- sshConfigDropdownRefs.current[index] &&
- !sshConfigDropdownRefs.current[index]?.contains(
- event.target as Node,
- ) &&
- sshConfigInputRefs.current[index] &&
- !sshConfigInputRefs.current[index]?.contains(event.target as Node)
- ) {
- setSshConfigDropdownOpen((prev) => ({ ...prev, [index]: false }));
- }
- });
- }
-
- const hasOpenDropdowns = Object.values(sshConfigDropdownOpen).some(
- (open) => open,
- );
-
- if (hasOpenDropdowns) {
- document.addEventListener("mousedown", handleSshConfigClickOutside);
- } else {
- document.removeEventListener("mousedown", handleSshConfigClickOutside);
- }
-
- return () => {
- document.removeEventListener("mousedown", handleSshConfigClickOutside);
- };
- }, [sshConfigDropdownOpen]);
-
- return (
-
- );
-}
diff --git a/src/ui/Desktop/Apps/Server/Server.tsx b/src/ui/Desktop/Apps/Server/Server.tsx
deleted file mode 100644
index 697de4d4..00000000
--- a/src/ui/Desktop/Apps/Server/Server.tsx
+++ /dev/null
@@ -1,480 +0,0 @@
-import React from "react";
-import { useSidebar } from "@/components/ui/sidebar.tsx";
-import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status";
-import { Separator } from "@/components/ui/separator.tsx";
-import { Button } from "@/components/ui/button.tsx";
-import { Progress } from "@/components/ui/progress.tsx";
-import { Cpu, HardDrive, MemoryStick } from "lucide-react";
-import { Tunnel } from "@/ui/Desktop/Apps/Tunnel/Tunnel.tsx";
-import {
- getServerStatusById,
- getServerMetricsById,
- type ServerMetrics,
-} from "@/ui/main-axios.ts";
-import { useTabs } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx";
-import { useTranslation } from "react-i18next";
-import { toast } from "sonner";
-
-interface ServerProps {
- hostConfig?: any;
- title?: string;
- isVisible?: boolean;
- isTopbarOpen?: boolean;
- embedded?: boolean;
-}
-
-export function Server({
- hostConfig,
- title,
- isVisible = true,
- isTopbarOpen = true,
- embedded = false,
-}: ServerProps): React.ReactElement {
- const { t } = useTranslation();
- const { state: sidebarState } = useSidebar();
- const { addTab, tabs } = useTabs() as any;
- const [serverStatus, setServerStatus] = React.useState<"online" | "offline">(
- "offline",
- );
- const [metrics, setMetrics] = React.useState(null);
- const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig);
- const [isLoadingMetrics, setIsLoadingMetrics] = React.useState(false);
- const [isRefreshing, setIsRefreshing] = React.useState(false);
- const [showStatsUI, setShowStatsUI] = React.useState(true);
-
- React.useEffect(() => {
- setCurrentHostConfig(hostConfig);
- }, [hostConfig]);
-
- React.useEffect(() => {
- const fetchLatestHostConfig = async () => {
- if (hostConfig?.id) {
- try {
- const { getSSHHosts } = await import("@/ui/main-axios.ts");
- const hosts = await getSSHHosts();
- const updatedHost = hosts.find((h) => h.id === hostConfig.id);
- if (updatedHost) {
- setCurrentHostConfig(updatedHost);
- }
- } catch (error) {
- toast.error(t("serverStats.failedToFetchHostConfig"));
- }
- }
- };
-
- fetchLatestHostConfig();
-
- const handleHostsChanged = async () => {
- if (hostConfig?.id) {
- try {
- const { getSSHHosts } = await import("@/ui/main-axios.ts");
- const hosts = await getSSHHosts();
- const updatedHost = hosts.find((h) => h.id === hostConfig.id);
- if (updatedHost) {
- setCurrentHostConfig(updatedHost);
- }
- } catch (error) {
- toast.error(t("serverStats.failedToFetchHostConfig"));
- }
- }
- };
-
- window.addEventListener("ssh-hosts:changed", handleHostsChanged);
- return () =>
- window.removeEventListener("ssh-hosts:changed", handleHostsChanged);
- }, [hostConfig?.id]);
-
- React.useEffect(() => {
- let cancelled = false;
- let intervalId: number | undefined;
-
- const fetchStatus = async () => {
- try {
- const res = await getServerStatusById(currentHostConfig?.id);
- if (!cancelled) {
- setServerStatus(res?.status === "online" ? "online" : "offline");
- }
- } catch (error: any) {
- if (!cancelled) {
- if (error?.response?.status === 503) {
- setServerStatus("offline");
- } else if (error?.response?.status === 504) {
- setServerStatus("offline");
- } else if (error?.response?.status === 404) {
- setServerStatus("offline");
- } else {
- setServerStatus("offline");
- }
- toast.error(t("serverStats.failedToFetchStatus"));
- }
- }
- };
-
- const fetchMetrics = async () => {
- if (!currentHostConfig?.id) return;
- try {
- setIsLoadingMetrics(true);
- const data = await getServerMetricsById(currentHostConfig.id);
- if (!cancelled) {
- setMetrics(data);
- setShowStatsUI(true);
- }
- } catch (error) {
- if (!cancelled) {
- setMetrics(null);
- setShowStatsUI(false);
- toast.error(t("serverStats.failedToFetchMetrics"));
- }
- } finally {
- if (!cancelled) {
- setIsLoadingMetrics(false);
- }
- }
- };
-
- if (currentHostConfig?.id && isVisible) {
- fetchStatus();
- fetchMetrics();
- intervalId = window.setInterval(() => {
- fetchStatus();
- fetchMetrics();
- }, 30000);
- }
-
- return () => {
- cancelled = true;
- if (intervalId) window.clearInterval(intervalId);
- };
- }, [currentHostConfig?.id, isVisible]);
-
- const topMarginPx = isTopbarOpen ? 74 : 16;
- const leftMarginPx = sidebarState === "collapsed" ? 16 : 8;
- const bottomMarginPx = 8;
-
- const isFileManagerAlreadyOpen = React.useMemo(() => {
- if (!currentHostConfig) return false;
- return tabs.some(
- (tab: any) =>
- tab.type === "file_manager" &&
- tab.hostConfig?.id === currentHostConfig.id,
- );
- }, [tabs, currentHostConfig]);
-
- const wrapperStyle: React.CSSProperties = embedded
- ? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" }
- : {
- opacity: isVisible ? 1 : 0,
- marginLeft: leftMarginPx,
- marginRight: 17,
- marginTop: topMarginPx,
- marginBottom: bottomMarginPx,
- height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
- };
-
- const containerClass = embedded
- ? "h-full w-full text-white overflow-hidden bg-transparent"
- : "bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden";
-
- return (
-
-
-
-
-
-
- {currentHostConfig?.folder} / {title}
-
-
-
-
-
-
-
-
{
- if (currentHostConfig?.id) {
- try {
- setIsRefreshing(true);
- const res = await getServerStatusById(currentHostConfig.id);
- setServerStatus(
- res?.status === "online" ? "online" : "offline",
- );
- const data = await getServerMetricsById(
- currentHostConfig.id,
- );
- setMetrics(data);
- setShowStatsUI(true);
- } catch (error: any) {
- if (error?.response?.status === 503) {
- setServerStatus("offline");
- } else if (error?.response?.status === 504) {
- setServerStatus("offline");
- } else if (error?.response?.status === 404) {
- setServerStatus("offline");
- } else {
- setServerStatus("offline");
- }
- setMetrics(null);
- setShowStatsUI(false);
- } finally {
- setIsRefreshing(false);
- }
- }
- }}
- title={t("serverStats.refreshStatusAndMetrics")}
- >
- {isRefreshing ? (
-
-
- {t("serverStats.refreshing")}
-
- ) : (
- t("serverStats.refreshStatus")
- )}
-
- {currentHostConfig?.enableFileManager && (
-
{
- if (!currentHostConfig || isFileManagerAlreadyOpen) return;
- const titleBase =
- currentHostConfig?.name &&
- currentHostConfig.name.trim() !== ""
- ? currentHostConfig.name.trim()
- : `${currentHostConfig.username}@${currentHostConfig.ip}`;
- addTab({
- type: "file_manager",
- title: titleBase,
- hostConfig: currentHostConfig,
- });
- }}
- >
- {t("nav.fileManager")}
-
- )}
-
-
-
-
- {showStatsUI && (
-
- {isLoadingMetrics && !metrics ? (
-
-
-
-
- {t("serverStats.loadingMetrics")}
-
-
-
- ) : !metrics && serverStatus === "offline" ? (
-
-
-
-
- {t("serverStats.serverOffline")}
-
-
- {t("serverStats.cannotFetchMetrics")}
-
-
-
- ) : (
-
- {/* CPU Stats */}
-
-
-
-
- {t("serverStats.cpuUsage")}
-
-
-
-
-
-
- {(() => {
- const pct = metrics?.cpu?.percent;
- const cores = metrics?.cpu?.cores;
- const pctText =
- typeof pct === "number" ? `${pct}%` : "N/A";
- const coresText =
- typeof cores === "number"
- ? t("serverStats.cpuCores", { count: cores })
- : t("serverStats.naCpus");
- return `${pctText} ${t("serverStats.of")} ${coresText}`;
- })()}
-
-
-
-
-
-
- {metrics?.cpu?.load
- ? `Load: ${metrics.cpu.load[0].toFixed(2)}, ${metrics.cpu.load[1].toFixed(2)}, ${metrics.cpu.load[2].toFixed(2)}`
- : "Load: N/A"}
-
-
-
-
- {/* Memory Stats */}
-
-
-
-
- {t("serverStats.memoryUsage")}
-
-
-
-
-
-
- {(() => {
- const pct = metrics?.memory?.percent;
- const used = metrics?.memory?.usedGiB;
- const total = metrics?.memory?.totalGiB;
- const pctText =
- typeof pct === "number" ? `${pct}%` : "N/A";
- const usedText =
- typeof used === "number"
- ? `${used.toFixed(1)} GiB`
- : "N/A";
- const totalText =
- typeof total === "number"
- ? `${total.toFixed(1)} GiB`
- : "N/A";
- return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`;
- })()}
-
-
-
-
-
-
- {(() => {
- const used = metrics?.memory?.usedGiB;
- const total = metrics?.memory?.totalGiB;
- const free =
- typeof used === "number" && typeof total === "number"
- ? (total - used).toFixed(1)
- : "N/A";
- return `Free: ${free} GiB`;
- })()}
-
-
-
-
- {/* Disk Stats */}
-
-
-
-
- {t("serverStats.rootStorageSpace")}
-
-
-
-
-
-
- {(() => {
- const pct = metrics?.disk?.percent;
- const used = metrics?.disk?.usedHuman;
- const total = metrics?.disk?.totalHuman;
- const pctText =
- typeof pct === "number" ? `${pct}%` : "N/A";
- const usedText = used ?? "N/A";
- const totalText = total ?? "N/A";
- return `${pctText} (${usedText} ${t("serverStats.of")} ${totalText})`;
- })()}
-
-
-
-
-
-
- {(() => {
- const available = metrics?.disk?.availableHuman;
- return available
- ? `Available: ${available}`
- : "Available: N/A";
- })()}
-
-
-
-
- )}
-
- )}
-
- {/* SSH Tunnels */}
- {currentHostConfig?.tunnelConnections &&
- currentHostConfig.tunnelConnections.length > 0 && (
-
-
-
- )}
-
-
- {t("serverStats.feedbackMessage")}{" "}
-
- GitHub
-
- !
-
-
-
- );
-}
diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx
deleted file mode 100644
index 2de03a7d..00000000
--- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx
+++ /dev/null
@@ -1,802 +0,0 @@
-import {
- useEffect,
- useRef,
- useState,
- useImperativeHandle,
- forwardRef,
-} from "react";
-import { useXTerm } from "react-xtermjs";
-import { FitAddon } from "@xterm/addon-fit";
-import { ClipboardAddon } from "@xterm/addon-clipboard";
-import { Unicode11Addon } from "@xterm/addon-unicode11";
-import { WebLinksAddon } from "@xterm/addon-web-links";
-import { useTranslation } from "react-i18next";
-import { toast } from "sonner";
-import { getCookie, isElectron } from "@/ui/main-axios.ts";
-
-interface SSHTerminalProps {
- hostConfig: any;
- isVisible: boolean;
- title?: string;
- showTitle?: boolean;
- splitScreen?: boolean;
- onClose?: () => void;
- initialPath?: string;
- executeCommand?: string;
-}
-
-export const Terminal = forwardRef(function SSHTerminal(
- {
- hostConfig,
- isVisible,
- splitScreen = false,
- onClose,
- initialPath,
- executeCommand,
- },
- ref,
-) {
- if (typeof window !== "undefined" && !(window as any).testJWT) {
- (window as any).testJWT = () => {
- const jwt = getCookie("jwt");
- return jwt;
- };
- }
-
- const { t } = useTranslation();
- const { instance: terminal, ref: xtermRef } = useXTerm();
- const fitAddonRef = useRef(null);
- const webSocketRef = useRef(null);
- const resizeTimeout = useRef(null);
- const wasDisconnectedBySSH = useRef(false);
- const pingIntervalRef = useRef(null);
- const [visible, setVisible] = useState(false);
- const [isConnected, setIsConnected] = useState(false);
- const [isConnecting, setIsConnecting] = useState(false);
- const [connectionError, setConnectionError] = useState(null);
- const [isAuthenticated, setIsAuthenticated] = useState(false);
- const isVisibleRef = useRef(false);
- const reconnectTimeoutRef = useRef(null);
- const reconnectAttempts = useRef(0);
- const maxReconnectAttempts = 3;
- const isUnmountingRef = useRef(false);
- const shouldNotReconnectRef = useRef(false);
- const isReconnectingRef = useRef(false);
- const isConnectingRef = useRef(false);
- const connectionTimeoutRef = useRef(null);
-
- const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
- const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
- const notifyTimerRef = useRef(null);
- const DEBOUNCE_MS = 140;
-
- useEffect(() => {
- isVisibleRef.current = isVisible;
- }, [isVisible]);
-
- useEffect(() => {
- const checkAuth = () => {
- const jwtToken = getCookie("jwt");
- const isAuth = !!(jwtToken && jwtToken.trim() !== "");
-
- setIsAuthenticated((prev) => {
- if (prev !== isAuth) {
- return isAuth;
- }
- return prev;
- });
- };
-
- checkAuth();
-
- const authCheckInterval = setInterval(checkAuth, 5000);
-
- return () => clearInterval(authCheckInterval);
- }, []);
-
- function hardRefresh() {
- try {
- if (terminal && typeof (terminal as any).refresh === "function") {
- (terminal as any).refresh(0, terminal.rows - 1);
- }
- } catch (_) {}
- }
-
- function scheduleNotify(cols: number, rows: number) {
- if (!(cols > 0 && rows > 0)) return;
- pendingSizeRef.current = { cols, rows };
- if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
- notifyTimerRef.current = setTimeout(() => {
- const next = pendingSizeRef.current;
- const last = lastSentSizeRef.current;
- if (!next) return;
- if (last && last.cols === next.cols && last.rows === next.rows) return;
- if (webSocketRef.current?.readyState === WebSocket.OPEN) {
- webSocketRef.current.send(
- JSON.stringify({ type: "resize", data: next }),
- );
- lastSentSizeRef.current = next;
- }
- }, DEBOUNCE_MS);
- }
-
- useImperativeHandle(
- ref,
- () => ({
- disconnect: () => {
- isUnmountingRef.current = true;
- shouldNotReconnectRef.current = true;
- isReconnectingRef.current = false;
- if (pingIntervalRef.current) {
- clearInterval(pingIntervalRef.current);
- pingIntervalRef.current = null;
- }
- if (reconnectTimeoutRef.current) {
- clearTimeout(reconnectTimeoutRef.current);
- reconnectTimeoutRef.current = null;
- }
- if (connectionTimeoutRef.current) {
- clearTimeout(connectionTimeoutRef.current);
- connectionTimeoutRef.current = null;
- }
- webSocketRef.current?.close();
- setIsConnected(false);
- setIsConnecting(false);
- },
- fit: () => {
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- },
- sendInput: (data: string) => {
- if (webSocketRef.current?.readyState === 1) {
- webSocketRef.current.send(JSON.stringify({ type: "input", data }));
- }
- },
- notifyResize: () => {
- try {
- const cols = terminal?.cols ?? undefined;
- const rows = terminal?.rows ?? undefined;
- if (typeof cols === "number" && typeof rows === "number") {
- scheduleNotify(cols, rows);
- hardRefresh();
- }
- } catch (_) {}
- },
- refresh: () => hardRefresh(),
- }),
- [terminal],
- );
-
- function handleWindowResize() {
- if (!isVisibleRef.current) return;
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- }
-
- function getUseRightClickCopyPaste() {
- return getCookie("rightClickCopyPaste") === "true";
- }
-
- function attemptReconnection() {
- if (
- isUnmountingRef.current ||
- shouldNotReconnectRef.current ||
- isReconnectingRef.current ||
- isConnectingRef.current ||
- wasDisconnectedBySSH.current
- ) {
- return;
- }
-
- if (reconnectAttempts.current >= maxReconnectAttempts) {
- toast.error(t("terminal.maxReconnectAttemptsReached"));
- if (onClose) {
- onClose();
- }
- return;
- }
-
- isReconnectingRef.current = true;
-
- if (terminal) {
- terminal.clear();
- }
-
- reconnectAttempts.current++;
-
- toast.info(
- t("terminal.reconnecting", {
- attempt: reconnectAttempts.current,
- max: maxReconnectAttempts,
- }),
- );
-
- reconnectTimeoutRef.current = setTimeout(() => {
- if (
- isUnmountingRef.current ||
- shouldNotReconnectRef.current ||
- wasDisconnectedBySSH.current
- ) {
- isReconnectingRef.current = false;
- return;
- }
-
- if (reconnectAttempts.current > maxReconnectAttempts) {
- isReconnectingRef.current = false;
- return;
- }
-
- const jwtToken = getCookie("jwt");
- if (!jwtToken || jwtToken.trim() === "") {
- console.warn("Reconnection cancelled - no authentication token");
- isReconnectingRef.current = false;
- setConnectionError("Authentication required for reconnection");
- return;
- }
-
- if (terminal && hostConfig) {
- terminal.clear();
- const cols = terminal.cols;
- const rows = terminal.rows;
- connectToHost(cols, rows);
- }
-
- isReconnectingRef.current = false;
- }, 2000 * reconnectAttempts.current);
- }
-
- function connectToHost(cols: number, rows: number) {
- if (isConnectingRef.current) {
- return;
- }
-
- isConnectingRef.current = true;
-
- const isDev =
- process.env.NODE_ENV === "development" &&
- (window.location.port === "3000" ||
- window.location.port === "5173" ||
- window.location.port === "");
-
- const jwtToken = getCookie("jwt");
-
- if (!jwtToken || jwtToken.trim() === "") {
- console.error("No JWT token available for WebSocket connection");
- setIsConnected(false);
- setIsConnecting(false);
- setConnectionError("Authentication required");
- isConnectingRef.current = false;
- return;
- }
-
- const baseWsUrl = isDev
- ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:30002`
- : isElectron()
- ? (() => {
- const baseUrl =
- (window as any).configuredServerUrl || "http://127.0.0.1:30001";
- const wsProtocol = baseUrl.startsWith("https://")
- ? "wss://"
- : "ws://";
- const wsHost = baseUrl.replace(/^https?:\/\//, "");
- return `${wsProtocol}${wsHost}/ssh/websocket/`;
- })()
- : `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/ssh/websocket/`;
-
- if (
- webSocketRef.current &&
- webSocketRef.current.readyState !== WebSocket.CLOSED
- ) {
- webSocketRef.current.close();
- }
-
- if (pingIntervalRef.current) {
- clearInterval(pingIntervalRef.current);
- pingIntervalRef.current = null;
- }
- if (connectionTimeoutRef.current) {
- clearTimeout(connectionTimeoutRef.current);
- connectionTimeoutRef.current = null;
- }
-
- const wsUrl = `${baseWsUrl}?token=${encodeURIComponent(jwtToken)}`;
-
- const ws = new WebSocket(wsUrl);
- webSocketRef.current = ws;
- wasDisconnectedBySSH.current = false;
- setConnectionError(null);
- shouldNotReconnectRef.current = false;
- isReconnectingRef.current = false;
- setIsConnecting(true);
-
- setupWebSocketListeners(ws, cols, rows);
- }
-
- function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
- ws.addEventListener("open", () => {
- connectionTimeoutRef.current = setTimeout(() => {
- if (!isConnected) {
- if (terminal) {
- terminal.clear();
- }
- toast.error(t("terminal.connectionTimeout"));
- if (webSocketRef.current) {
- webSocketRef.current.close();
- }
- if (reconnectAttempts.current > 0) {
- attemptReconnection();
- }
- }
- }, 10000);
-
- ws.send(
- JSON.stringify({
- type: "connectToHost",
- data: { cols, rows, hostConfig, initialPath, executeCommand },
- }),
- );
- terminal.onData((data) => {
- ws.send(JSON.stringify({ type: "input", data }));
- });
-
- pingIntervalRef.current = setInterval(() => {
- if (ws.readyState === WebSocket.OPEN) {
- ws.send(JSON.stringify({ type: "ping" }));
- }
- }, 30000);
- });
-
- ws.addEventListener("message", (event) => {
- try {
- const msg = JSON.parse(event.data);
- if (msg.type === "data") {
- if (typeof msg.data === "string") {
- terminal.write(msg.data);
- } else {
- terminal.write(String(msg.data));
- }
- } else if (msg.type === "error") {
- const errorMessage = msg.message || t("terminal.unknownError");
-
- if (
- errorMessage.toLowerCase().includes("auth") ||
- errorMessage.toLowerCase().includes("password") ||
- errorMessage.toLowerCase().includes("permission") ||
- errorMessage.toLowerCase().includes("denied") ||
- errorMessage.toLowerCase().includes("invalid") ||
- errorMessage.toLowerCase().includes("failed") ||
- errorMessage.toLowerCase().includes("incorrect")
- ) {
- toast.error(t("terminal.authError", { message: errorMessage }));
- shouldNotReconnectRef.current = true;
- if (webSocketRef.current) {
- webSocketRef.current.close();
- }
- if (onClose) {
- onClose();
- }
- return;
- }
-
- if (
- errorMessage.toLowerCase().includes("connection") ||
- errorMessage.toLowerCase().includes("timeout") ||
- errorMessage.toLowerCase().includes("network")
- ) {
- toast.error(
- t("terminal.connectionError", { message: errorMessage }),
- );
- setIsConnected(false);
- if (terminal) {
- terminal.clear();
- }
- setIsConnecting(true);
- wasDisconnectedBySSH.current = false;
- attemptReconnection();
- return;
- }
-
- toast.error(t("terminal.error", { message: errorMessage }));
- } else if (msg.type === "connected") {
- setIsConnected(true);
- setIsConnecting(false);
- isConnectingRef.current = false;
- if (connectionTimeoutRef.current) {
- clearTimeout(connectionTimeoutRef.current);
- connectionTimeoutRef.current = null;
- }
- if (reconnectAttempts.current > 0) {
- toast.success(t("terminal.reconnected"));
- }
- reconnectAttempts.current = 0;
- isReconnectingRef.current = false;
- } else if (msg.type === "disconnected") {
- wasDisconnectedBySSH.current = true;
- setIsConnected(false);
- if (terminal) {
- terminal.clear();
- }
- setIsConnecting(false);
- if (onClose) {
- onClose();
- }
- }
- } catch (error) {
- toast.error(t("terminal.messageParseError"));
- }
- });
-
- ws.addEventListener("close", (event) => {
- setIsConnected(false);
- isConnectingRef.current = false;
- if (terminal) {
- terminal.clear();
- }
-
- if (event.code === 1008) {
- console.error("WebSocket authentication failed:", event.reason);
- setConnectionError("Authentication failed - please re-login");
- setIsConnecting(false);
- shouldNotReconnectRef.current = true;
-
- localStorage.removeItem("jwt");
-
- toast.error("Authentication failed. Please log in again.");
-
- return;
- }
-
- setIsConnecting(false);
- if (
- !wasDisconnectedBySSH.current &&
- !isUnmountingRef.current &&
- !shouldNotReconnectRef.current
- ) {
- wasDisconnectedBySSH.current = false;
- attemptReconnection();
- }
- });
-
- ws.addEventListener("error", (event) => {
- setIsConnected(false);
- isConnectingRef.current = false;
- setConnectionError(t("terminal.websocketError"));
- if (terminal) {
- terminal.clear();
- }
- setIsConnecting(false);
- if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
- wasDisconnectedBySSH.current = false;
- attemptReconnection();
- }
- });
- }
-
- async function writeTextToClipboard(text: string): Promise {
- try {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- await navigator.clipboard.writeText(text);
- return;
- }
- } catch (_) {}
- const textarea = document.createElement("textarea");
- textarea.value = text;
- textarea.style.position = "fixed";
- textarea.style.left = "-9999px";
- document.body.appendChild(textarea);
- textarea.focus();
- textarea.select();
- try {
- document.execCommand("copy");
- } finally {
- document.body.removeChild(textarea);
- }
- }
-
- async function readTextFromClipboard(): Promise {
- try {
- if (navigator.clipboard && navigator.clipboard.readText) {
- return await navigator.clipboard.readText();
- }
- } catch (_) {}
- return "";
- }
-
- useEffect(() => {
- if (!terminal || !xtermRef.current) return;
-
- terminal.options = {
- cursorBlink: true,
- cursorStyle: "bar",
- scrollback: 10000,
- fontSize: 14,
- fontFamily:
- '"Caskaydia Cove Nerd Font Mono", "SF Mono", Consolas, "Liberation Mono", monospace',
- theme: { background: "#18181b", foreground: "#f7f7f7" },
- allowTransparency: true,
- convertEol: true,
- windowsMode: false,
- macOptionIsMeta: false,
- macOptionClickForcesSelection: false,
- rightClickSelectsWord: false,
- fastScrollModifier: "alt",
- fastScrollSensitivity: 5,
- allowProposedApi: true,
- minimumContrastRatio: 1,
- letterSpacing: 0,
- lineHeight: 1.2,
- };
-
- const fitAddon = new FitAddon();
- const clipboardAddon = new ClipboardAddon();
- const unicode11Addon = new Unicode11Addon();
- const webLinksAddon = new WebLinksAddon();
-
- fitAddonRef.current = fitAddon;
- terminal.loadAddon(fitAddon);
- terminal.loadAddon(clipboardAddon);
- terminal.loadAddon(unicode11Addon);
- terminal.loadAddon(webLinksAddon);
-
- terminal.unicode.activeVersion = "11";
-
- terminal.open(xtermRef.current);
-
- const element = xtermRef.current;
- const handleContextMenu = async (e: MouseEvent) => {
- if (!getUseRightClickCopyPaste()) return;
- e.preventDefault();
- e.stopPropagation();
- try {
- if (terminal.hasSelection()) {
- const selection = terminal.getSelection();
- if (selection) {
- await writeTextToClipboard(selection);
- terminal.clearSelection();
- }
- } else {
- const pasteText = await readTextFromClipboard();
- if (pasteText) terminal.paste(pasteText);
- }
- } catch (_) {}
- };
- element?.addEventListener("contextmenu", handleContextMenu);
-
- const handleMacKeyboard = (e: KeyboardEvent) => {
- const isMacOS =
- navigator.platform.toUpperCase().indexOf("MAC") >= 0 ||
- navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
-
- if (!isMacOS) return;
-
- if (e.altKey && !e.metaKey && !e.ctrlKey) {
- const keyMappings: { [key: string]: string } = {
- "7": "|",
- "2": "€",
- "8": "[",
- "9": "]",
- l: "@",
- L: "@",
- Digit7: "|",
- Digit2: "€",
- Digit8: "[",
- Digit9: "]",
- KeyL: "@",
- };
-
- const char = keyMappings[e.key] || keyMappings[e.code];
- if (char) {
- e.preventDefault();
- e.stopPropagation();
-
- if (webSocketRef.current?.readyState === 1) {
- webSocketRef.current.send(
- JSON.stringify({ type: "input", data: char }),
- );
- }
- return false;
- }
- }
- };
-
- element?.addEventListener("keydown", handleMacKeyboard, true);
-
- const resizeObserver = new ResizeObserver(() => {
- if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
- resizeTimeout.current = setTimeout(() => {
- if (!isVisibleRef.current) return;
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- }, 150);
- });
-
- resizeObserver.observe(xtermRef.current);
-
- setVisible(true);
-
- return () => {
- isUnmountingRef.current = true;
- shouldNotReconnectRef.current = true;
- isReconnectingRef.current = false;
- setIsConnecting(false);
- resizeObserver.disconnect();
- element?.removeEventListener("contextmenu", handleContextMenu);
- element?.removeEventListener("keydown", handleMacKeyboard, true);
- if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
- if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
- if (reconnectTimeoutRef.current)
- clearTimeout(reconnectTimeoutRef.current);
- if (connectionTimeoutRef.current)
- clearTimeout(connectionTimeoutRef.current);
- if (pingIntervalRef.current) {
- clearInterval(pingIntervalRef.current);
- pingIntervalRef.current = null;
- }
- webSocketRef.current?.close();
- };
- }, [xtermRef, terminal]);
-
- useEffect(() => {
- if (!terminal || !hostConfig || !visible) return;
-
- if (isConnected || isConnecting) return;
-
- setIsConnecting(true);
-
- const readyFonts =
- (document as any).fonts?.ready instanceof Promise
- ? (document as any).fonts.ready
- : Promise.resolve();
-
- readyFonts.then(() => {
- setTimeout(() => {
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
-
- if (terminal && !splitScreen) {
- terminal.focus();
- }
-
- const jwtToken = getCookie("jwt");
-
- if (!jwtToken || jwtToken.trim() === "") {
- setIsConnected(false);
- setIsConnecting(false);
- setConnectionError("Authentication required");
- return;
- }
-
- const cols = terminal.cols;
- const rows = terminal.rows;
-
- connectToHost(cols, rows);
- }, 200);
- });
- }, [terminal, hostConfig, visible, isConnected, isConnecting, splitScreen]);
-
- useEffect(() => {
- if (isVisible && fitAddonRef.current) {
- setTimeout(() => {
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- if (terminal && !splitScreen) {
- terminal.focus();
- }
- }, 0);
-
- if (terminal && !splitScreen) {
- setTimeout(() => {
- terminal.focus();
- }, 100);
- }
- }
- }, [isVisible, splitScreen, terminal]);
-
- useEffect(() => {
- if (!fitAddonRef.current) return;
- setTimeout(() => {
- fitAddonRef.current?.fit();
- if (terminal) scheduleNotify(terminal.cols, terminal.rows);
- hardRefresh();
- if (terminal && !splitScreen && isVisible) {
- terminal.focus();
- }
- }, 0);
- }, [splitScreen, isVisible, terminal]);
-
- return (
-
-
{
- if (terminal && !splitScreen) {
- terminal.focus();
- }
- }}
- />
-
- {isConnecting && (
-
-
-
-
{t("terminal.connecting")}
-
-
- )}
-
- );
-});
-
-const style = document.createElement("style");
-style.innerHTML = `
-@font-face {
- font-family: 'Caskaydia Cove Nerd Font Mono';
- src: url('./fonts/CaskaydiaCoveNerdFontMono-Regular.ttf') format('truetype');
- font-weight: normal;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'Caskaydia Cove Nerd Font Mono';
- src: url('./fonts/CaskaydiaCoveNerdFontMono-Bold.ttf') format('truetype');
- font-weight: bold;
- font-style: normal;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'Caskaydia Cove Nerd Font Mono';
- src: url('./fonts/CaskaydiaCoveNerdFontMono-Italic.ttf') format('truetype');
- font-weight: normal;
- font-style: italic;
- font-display: swap;
-}
-
-@font-face {
- font-family: 'Caskaydia Cove Nerd Font Mono';
- src: url('./fonts/CaskaydiaCoveNerdFontMono-BoldItalic.ttf') format('truetype');
- font-weight: bold;
- font-style: italic;
- font-display: swap;
-}
-
-.xterm .xterm-viewport::-webkit-scrollbar {
- width: 8px;
- background: transparent;
-}
-.xterm .xterm-viewport::-webkit-scrollbar-thumb {
- background: rgba(180,180,180,0.7);
- border-radius: 4px;
-}
-.xterm .xterm-viewport::-webkit-scrollbar-thumb:hover {
- background: rgba(120,120,120,0.9);
-}
-.xterm .xterm-viewport {
- scrollbar-width: thin;
- scrollbar-color: rgba(180,180,180,0.7) transparent;
-}
-
-.xterm {
- font-feature-settings: "liga" 1, "calt" 1;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-.xterm .xterm-screen {
- font-family: 'Caskaydia Cove Nerd Font Mono', 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace !important;
- font-variant-ligatures: contextual;
-}
-
-.xterm .xterm-screen .xterm-char {
- font-feature-settings: "liga" 1, "calt" 1;
-}
-`;
-document.head.appendChild(style);
diff --git a/src/ui/Desktop/Homepage/Homepage.tsx b/src/ui/Desktop/Homepage/Homepage.tsx
deleted file mode 100644
index 2b156618..00000000
--- a/src/ui/Desktop/Homepage/Homepage.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { HomepageAuth } from "@/ui/Desktop/Homepage/HomepageAuth.tsx";
-import { HomepageUpdateLog } from "@/ui/Desktop/Homepage/HompageUpdateLog.tsx";
-import { HomepageAlertManager } from "@/ui/Desktop/Homepage/HomepageAlertManager.tsx";
-import { Button } from "@/components/ui/button.tsx";
-import { getUserInfo, getDatabaseHealth, getCookie } from "@/ui/main-axios.ts";
-import { useTranslation } from "react-i18next";
-
-interface HomepageProps {
- onSelectView: (view: string) => void;
- isAuthenticated: boolean;
- authLoading: boolean;
- onAuthSuccess: (authData: {
- isAdmin: boolean;
- username: string | null;
- userId: string | null;
- }) => void;
- isTopbarOpen: boolean;
-}
-
-export function Homepage({
- isAuthenticated,
- authLoading,
- onAuthSuccess,
- isTopbarOpen,
-}: HomepageProps): React.ReactElement {
- const [loggedIn, setLoggedIn] = useState(isAuthenticated);
- const [isAdmin, setIsAdmin] = useState(false);
- const [username, setUsername] = useState
(null);
- const [userId, setUserId] = useState(null);
- const [dbError, setDbError] = useState(null);
-
- const topMarginPx = isTopbarOpen ? 74 : 26;
- const leftMarginPx = 26;
- const bottomMarginPx = 8;
-
- useEffect(() => {
- setLoggedIn(isAuthenticated);
- }, [isAuthenticated]);
-
- useEffect(() => {
- if (isAuthenticated) {
- const jwt = getCookie("jwt");
- if (jwt) {
- getUserInfo()
- .then((meRes) => {
- setIsAdmin(!!meRes.is_admin);
- setUsername(meRes.username || null);
- setUserId(meRes.userId || null);
- setDbError(null);
- })
- .catch((err) => {
- setIsAdmin(false);
- setUsername(null);
- setUserId(null);
-
- const errorCode = err?.response?.data?.code;
- if (errorCode === "SESSION_EXPIRED") {
- console.warn("Session expired - please log in again");
- setDbError("Session expired - please log in again");
- } else {
- setDbError(null);
- }
- });
-
- getDatabaseHealth()
- .then(() => {
- setDbError(null);
- })
- .catch((err) => {
- if (err?.response?.data?.error?.includes("Database")) {
- setDbError(
- "Could not connect to the database. Please try again later.",
- );
- }
- });
- }
- }
- }, [isAuthenticated]);
-
- return (
- <>
- {!loggedIn ? (
-
-
-
- ) : (
-
-
-
-
-
-
-
- window.open("https://github.com/Termix-SSH/Termix", "_blank")
- }
- >
- GitHub
-
-
-
- window.open(
- "https://github.com/Termix-SSH/Termix/issues/new",
- "_blank",
- )
- }
- >
- Feedback
-
-
-
- window.open(
- "https://discord.com/invite/jVQGdvHDrf",
- "_blank",
- )
- }
- >
- Discord
-
-
-
- window.open("https://github.com/sponsors/LukeGus", "_blank")
- }
- >
- Donate
-
-
-
-
-
- )}
-
-
- >
- );
-}
diff --git a/src/ui/Desktop/Homepage/HomepageAuth.tsx b/src/ui/Desktop/Homepage/HomepageAuth.tsx
deleted file mode 100644
index 70fd6ec4..00000000
--- a/src/ui/Desktop/Homepage/HomepageAuth.tsx
+++ /dev/null
@@ -1,1082 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { cn } from "@/lib/utils.ts";
-import { Button } from "@/components/ui/button.tsx";
-import { Input } from "@/components/ui/input.tsx";
-import { PasswordInput } from "@/components/ui/password-input.tsx";
-import { Label } from "@/components/ui/label.tsx";
-import { useTranslation } from "react-i18next";
-import { LanguageSwitcher } from "@/ui/Desktop/User/LanguageSwitcher.tsx";
-import { toast } from "sonner";
-import {
- registerUser,
- loginUser,
- getUserInfo,
- getRegistrationAllowed,
- getOIDCConfig,
- getSetupRequired,
- initiatePasswordReset,
- verifyPasswordResetCode,
- completePasswordReset,
- getOIDCAuthorizeUrl,
- verifyTOTPLogin,
- setCookie,
- getCookie,
- getServerConfig,
- isElectron,
- logoutUser,
-} from "../../main-axios.ts";
-import { ServerConfig as ServerConfigComponent } from "@/ui/Desktop/Electron Only/ServerConfig.tsx";
-
-interface HomepageAuthProps extends React.ComponentProps<"div"> {
- setLoggedIn: (loggedIn: boolean) => void;
- setIsAdmin: (isAdmin: boolean) => void;
- setUsername: (username: string | null) => void;
- setUserId: (userId: string | null) => void;
- loggedIn: boolean;
- authLoading: boolean;
- dbError: string | null;
- setDbError: (error: string | null) => void;
- onAuthSuccess: (authData: {
- isAdmin: boolean;
- username: string | null;
- userId: string | null;
- }) => void;
-}
-
-export function HomepageAuth({
- className,
- setLoggedIn,
- setIsAdmin,
- setUsername,
- setUserId,
- loggedIn,
- authLoading,
- dbError,
- setDbError,
- onAuthSuccess,
- ...props
-}: HomepageAuthProps) {
- const { t } = useTranslation();
- const [tab, setTab] = useState<"login" | "signup" | "external" | "reset">(
- "login",
- );
- const [localUsername, setLocalUsername] = useState("");
- const [password, setPassword] = useState("");
- const [signupConfirmPassword, setSignupConfirmPassword] = useState("");
- const [loading, setLoading] = useState(false);
- const [oidcLoading, setOidcLoading] = useState(false);
- const [visibility, setVisibility] = useState({
- password: false,
- signupConfirm: false,
- resetNew: false,
- resetConfirm: false,
- });
- const toggleVisibility = (field: keyof typeof visibility) => {
- setVisibility((prev) => ({ ...prev, [field]: !prev[field] }));
- };
-
- const [error, setError] = useState(null);
- const [internalLoggedIn, setInternalLoggedIn] = useState(false);
- const [firstUser, setFirstUser] = useState(false);
- const [firstUserToastShown, setFirstUserToastShown] = useState(false);
- const [registrationAllowed, setRegistrationAllowed] = useState(true);
- const [oidcConfigured, setOidcConfigured] = useState(false);
-
- const [resetStep, setResetStep] = useState<
- "initiate" | "verify" | "newPassword"
- >("initiate");
- const [resetCode, setResetCode] = useState("");
- const [newPassword, setNewPassword] = useState("");
- const [confirmPassword, setConfirmPassword] = useState("");
- const [tempToken, setTempToken] = useState("");
- const [resetLoading, setResetLoading] = useState(false);
- const [resetSuccess, setResetSuccess] = useState(false);
-
- const [totpRequired, setTotpRequired] = useState(false);
- const [totpCode, setTotpCode] = useState("");
- const [totpTempToken, setTotpTempToken] = useState("");
- const [totpLoading, setTotpLoading] = useState(false);
-
- useEffect(() => {
- setInternalLoggedIn(loggedIn);
- }, [loggedIn]);
-
- useEffect(() => {
- const clearJWTOnLoad = async () => {
- try {
- await logoutUser();
- } catch (error) {}
- };
-
- clearJWTOnLoad();
- }, []);
-
- useEffect(() => {
- getRegistrationAllowed().then((res) => {
- setRegistrationAllowed(res.allowed);
- });
- }, []);
-
- useEffect(() => {
- getOIDCConfig()
- .then((response) => {
- if (response) {
- setOidcConfigured(true);
- } else {
- setOidcConfigured(false);
- }
- })
- .catch((error) => {
- if (error.response?.status === 404) {
- setOidcConfigured(false);
- } else {
- setOidcConfigured(false);
- }
- });
- }, []);
-
- useEffect(() => {
- setDbHealthChecking(true);
- getSetupRequired()
- .then((res) => {
- if (res.setup_required) {
- setFirstUser(true);
- setTab("signup");
- if (!firstUserToastShown) {
- toast.info(t("auth.firstUserMessage"));
- setFirstUserToastShown(true);
- }
- } else {
- setFirstUser(false);
- }
- setDbError(null);
- setDbConnectionFailed(false);
- })
- .catch(() => {
- setDbConnectionFailed(true);
- })
- .finally(() => {
- setDbHealthChecking(false);
- });
- }, [setDbError, firstUserToastShown]);
-
- useEffect(() => {
- if (!registrationAllowed && !internalLoggedIn) {
- toast.warning(t("messages.registrationDisabled"));
- }
- }, [registrationAllowed, internalLoggedIn, t]);
-
- async function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- setError(null);
- setLoading(true);
-
- if (!localUsername.trim()) {
- toast.error(t("errors.requiredField"));
- setLoading(false);
- return;
- }
-
- try {
- let res, meRes;
- if (tab === "login") {
- res = await loginUser(localUsername, password);
- } else {
- if (password !== signupConfirmPassword) {
- toast.error(t("errors.passwordMismatch"));
- setLoading(false);
- return;
- }
- if (password.length < 6) {
- toast.error(t("errors.minLength", { min: 6 }));
- setLoading(false);
- return;
- }
-
- await registerUser(localUsername, password);
- res = await loginUser(localUsername, password);
- }
-
- if (res.requires_totp) {
- setTotpRequired(true);
- setTotpTempToken(res.temp_token);
- setLoading(false);
- return;
- }
-
- if (!res || !res.success) {
- throw new Error(t("errors.loginFailed"));
- }
-
- [meRes] = await Promise.all([getUserInfo()]);
-
- setInternalLoggedIn(true);
- setLoggedIn(true);
- setIsAdmin(!!meRes.is_admin);
- setUsername(meRes.username || null);
- setUserId(meRes.userId || null);
- setDbError(null);
- onAuthSuccess({
- isAdmin: !!meRes.is_admin,
- username: meRes.username || null,
- userId: meRes.userId || null,
- });
- setInternalLoggedIn(true);
- if (tab === "signup") {
- setSignupConfirmPassword("");
- toast.success(t("messages.registrationSuccess"));
- } else {
- toast.success(t("messages.loginSuccess"));
- }
- setTotpRequired(false);
- setTotpCode("");
- setTotpTempToken("");
- } catch (err: any) {
- const errorMessage =
- err?.response?.data?.error || err?.message || t("errors.unknownError");
- toast.error(errorMessage);
- setInternalLoggedIn(false);
- setLoggedIn(false);
- setIsAdmin(false);
- setUsername(null);
- setUserId(null);
- if (err?.response?.data?.error?.includes("Database")) {
- setDbConnectionFailed(true);
- } else {
- setDbError(null);
- }
- } finally {
- setLoading(false);
- }
- }
-
- async function handleInitiatePasswordReset() {
- setError(null);
- setResetLoading(true);
- try {
- const result = await initiatePasswordReset(localUsername);
- setResetStep("verify");
- toast.success(t("messages.resetCodeSent"));
- } catch (err: any) {
- toast.error(
- err?.response?.data?.error ||
- err?.message ||
- t("errors.failedPasswordReset"),
- );
- } finally {
- setResetLoading(false);
- }
- }
-
- async function handleVerifyResetCode() {
- setError(null);
- setResetLoading(true);
- try {
- const response = await verifyPasswordResetCode(localUsername, resetCode);
- setTempToken(response.tempToken);
- setResetStep("newPassword");
- toast.success(t("messages.codeVerified"));
- } catch (err: any) {
- toast.error(err?.response?.data?.error || t("errors.failedVerifyCode"));
- } finally {
- setResetLoading(false);
- }
- }
-
- async function handleCompletePasswordReset() {
- setError(null);
- setResetLoading(true);
-
- if (newPassword !== confirmPassword) {
- toast.error(t("errors.passwordMismatch"));
- setResetLoading(false);
- return;
- }
-
- if (newPassword.length < 6) {
- toast.error(t("errors.minLength", { min: 6 }));
- setResetLoading(false);
- return;
- }
-
- try {
- await completePasswordReset(localUsername, tempToken, newPassword);
-
- setResetStep("initiate");
- setResetCode("");
- setNewPassword("");
- setConfirmPassword("");
- setTempToken("");
- setError(null);
-
- setResetSuccess(true);
- toast.success(t("messages.passwordResetSuccess"));
-
- setTab("login");
- resetPasswordState();
- } catch (err: any) {
- toast.error(
- err?.response?.data?.error || t("errors.failedCompleteReset"),
- );
- } finally {
- setResetLoading(false);
- }
- }
-
- function resetPasswordState() {
- setResetStep("initiate");
- setResetCode("");
- setNewPassword("");
- setConfirmPassword("");
- setTempToken("");
- setError(null);
- setResetSuccess(false);
- setSignupConfirmPassword("");
- }
-
- function clearFormFields() {
- setPassword("");
- setSignupConfirmPassword("");
- setError(null);
- }
-
- async function handleTOTPVerification() {
- if (totpCode.length !== 6) {
- toast.error(t("auth.enterCode"));
- return;
- }
-
- setError(null);
- setTotpLoading(true);
-
- try {
- const res = await verifyTOTPLogin(totpTempToken, totpCode);
-
- if (!res || !res.success) {
- throw new Error(t("errors.loginFailed"));
- }
-
- if (isElectron() && res.token) {
- localStorage.setItem("jwt", res.token);
- }
-
- setInternalLoggedIn(true);
- setLoggedIn(true);
- setIsAdmin(!!res.is_admin);
- setUsername(res.username || null);
- setUserId(res.userId || null);
- setDbError(null);
-
- setTimeout(() => {
- onAuthSuccess({
- isAdmin: !!res.is_admin,
- username: res.username || null,
- userId: res.userId || null,
- });
- }, 100);
-
- setInternalLoggedIn(true);
- setTotpRequired(false);
- setTotpCode("");
- setTotpTempToken("");
- toast.success(t("messages.loginSuccess"));
- } catch (err: any) {
- const errorCode = err?.response?.data?.code;
- const errorMessage =
- err?.response?.data?.error ||
- err?.message ||
- t("errors.invalidTotpCode");
-
- if (errorCode === "SESSION_EXPIRED") {
- setTotpRequired(false);
- setTotpCode("");
- setTotpTempToken("");
- setTab("login");
- toast.error(t("errors.sessionExpired"));
- } else {
- toast.error(errorMessage);
- }
- } finally {
- setTotpLoading(false);
- }
- }
-
- async function handleOIDCLogin() {
- setError(null);
- setOidcLoading(true);
- try {
- const authResponse = await getOIDCAuthorizeUrl();
- const { auth_url: authUrl } = authResponse;
-
- if (!authUrl || authUrl === "undefined") {
- throw new Error(t("errors.invalidAuthUrl"));
- }
-
- window.location.replace(authUrl);
- } catch (err: any) {
- const errorMessage =
- err?.response?.data?.error ||
- err?.message ||
- t("errors.failedOidcLogin");
- toast.error(errorMessage);
- setOidcLoading(false);
- }
- }
-
- useEffect(() => {
- const urlParams = new URLSearchParams(window.location.search);
- const success = urlParams.get("success");
- const token = urlParams.get("token");
- const error = urlParams.get("error");
-
- if (error) {
- toast.error(`${t("errors.oidcAuthFailed")}: ${error}`);
- setOidcLoading(false);
- window.history.replaceState({}, document.title, window.location.pathname);
- return;
- }
-
- if (success) {
- setOidcLoading(true);
- setError(null);
-
- getUserInfo()
- .then((meRes) => {
- setInternalLoggedIn(true);
- setLoggedIn(true);
- setIsAdmin(!!meRes.is_admin);
- setUsername(meRes.username || null);
- setUserId(meRes.userId || null);
- setDbError(null);
- onAuthSuccess({
- isAdmin: !!meRes.is_admin,
- username: meRes.username || null,
- userId: meRes.userId || null,
- });
- setInternalLoggedIn(true);
- window.history.replaceState(
- {},
- document.title,
- window.location.pathname,
- );
- })
- .catch((err) => {
- setInternalLoggedIn(false);
- setLoggedIn(false);
- setIsAdmin(false);
- setUsername(null);
- setUserId(null);
- window.history.replaceState(
- {},
- document.title,
- window.location.pathname,
- );
- })
- .finally(() => {
- setOidcLoading(false);
- });
- }
- }, []);
-
- const Spinner = (
-
-
-
-
- );
-
- const [showServerConfig, setShowServerConfig] = useState(
- null,
- );
- const [currentServerUrl, setCurrentServerUrl] = useState("");
- const [dbConnectionFailed, setDbConnectionFailed] = useState(false);
- const [dbHealthChecking, setDbHealthChecking] = useState(false);
-
- useEffect(() => {
- if (dbConnectionFailed) {
- toast.error(t("errors.databaseConnection"));
- }
- }, [dbConnectionFailed, t]);
-
- const retryDatabaseConnection = async () => {
- setDbHealthChecking(true);
- setDbConnectionFailed(false);
- try {
- const res = await getSetupRequired();
- if (res.setup_required) {
- setFirstUser(true);
- setTab("signup");
- } else {
- setFirstUser(false);
- }
- setDbError(null);
- toast.success(t("messages.databaseConnected"));
- } catch (error) {
- setDbConnectionFailed(true);
- } finally {
- setDbHealthChecking(false);
- }
- };
-
- useEffect(() => {
- const checkServerConfig = async () => {
- if (isElectron()) {
- try {
- const config = await getServerConfig();
- setCurrentServerUrl(config?.serverUrl || "");
- setShowServerConfig(!config || !config.serverUrl);
- } catch (error) {
- setShowServerConfig(true);
- }
- } else {
- setShowServerConfig(false);
- }
- };
-
- checkServerConfig();
- }, []);
-
- if (showServerConfig === null) {
- return (
-
- );
- }
-
- if (showServerConfig) {
- return (
-
- {
- window.location.reload();
- }}
- onCancel={() => {
- setShowServerConfig(false);
- }}
- isFirstTime={!currentServerUrl}
- />
-
- );
- }
-
- if (dbHealthChecking && !dbConnectionFailed) {
- return (
-
-
-
-
-
- {t("common.checkingDatabase")}
-
-
-
-
- );
- }
-
- if (dbConnectionFailed) {
- return (
-
-
-
- {t("errors.databaseConnection")}
-
-
- {t("messages.databaseConnectionFailed")}
-
-
-
-
- window.location.reload()}
- >
- {t("common.refresh")}
-
-
-
-
-
-
-
- {t("common.language")}
-
-
-
-
- {isElectron() && currentServerUrl && (
-
-
-
Server
-
- {currentServerUrl}
-
-
-
setShowServerConfig(true)}
- className="h-8 px-3"
- >
- Edit
-
-
- )}
-
-
- );
- }
-
- return (
-
- {totpRequired && (
-
-
-
- {t("auth.twoFactorAuth")}
-
-
{t("auth.enterCode")}
-
-
-
-
{t("auth.verifyCode")}
-
setTotpCode(e.target.value.replace(/\D/g, ""))}
- disabled={totpLoading}
- className="text-center text-2xl tracking-widest font-mono"
- autoComplete="one-time-code"
- />
-
- {t("auth.backupCode")}
-
-
-
-
- {totpLoading ? Spinner : t("auth.verifyCode")}
-
-
-
{
- setTotpRequired(false);
- setTotpCode("");
- setTotpTempToken("");
- setError(null);
- }}
- >
- {t("common.cancel")}
-
-
- )}
-
- {!loggedIn && !authLoading && !totpRequired && (
- <>
-
- {
- setTab("login");
- if (tab === "reset") resetPasswordState();
- if (tab === "signup") clearFormFields();
- }}
- aria-selected={tab === "login"}
- disabled={loading || firstUser}
- >
- {t("common.login")}
-
- {
- setTab("signup");
- if (tab === "reset") resetPasswordState();
- if (tab === "login") clearFormFields();
- }}
- aria-selected={tab === "signup"}
- disabled={loading || !registrationAllowed}
- >
- {t("common.register")}
-
- {oidcConfigured && (
- {
- setTab("external");
- if (tab === "reset") resetPasswordState();
- if (tab === "login" || tab === "signup") clearFormFields();
- }}
- aria-selected={tab === "external"}
- disabled={oidcLoading}
- >
- {t("auth.external")}
-
- )}
-
-
-
- {tab === "login"
- ? t("auth.loginTitle")
- : tab === "signup"
- ? t("auth.registerTitle")
- : tab === "external"
- ? t("auth.loginWithExternal")
- : t("auth.forgotPassword")}
-
-
-
- {tab === "external" || tab === "reset" ? (
-
- {tab === "external" && (
- <>
-
-
{t("auth.loginWithExternalDesc")}
-
- {(() => {
- if (isElectron()) {
- return (
-
-
- {t("auth.externalNotSupportedInElectron")}
-
-
- );
- } else {
- return (
-
- {oidcLoading ? Spinner : t("auth.loginWithExternal")}
-
- );
- }
- })()}
- >
- )}
- {tab === "reset" && (
- <>
- {resetStep === "initiate" && (
- <>
-
-
{t("auth.resetCodeDesc")}
-
-
-
-
- {t("common.username")}
-
- setLocalUsername(e.target.value)}
- disabled={resetLoading}
- />
-
-
- {resetLoading ? Spinner : t("auth.sendResetCode")}
-
-
- >
- )}
-
- {resetStep === "verify" && (
- <>
-
-
- {t("auth.enterResetCode")}{" "}
- {localUsername}
-
-
-
-
-
- {t("auth.resetCode")}
-
-
- setResetCode(e.target.value.replace(/\D/g, ""))
- }
- disabled={resetLoading}
- placeholder="000000"
- />
-
-
- {resetLoading ? Spinner : t("auth.verifyCodeButton")}
-
-
{
- setResetStep("initiate");
- setResetCode("");
- }}
- >
- {t("common.back")}
-
-
- >
- )}
-
- {resetStep === "newPassword" && !resetSuccess && (
- <>
-
-
- {t("auth.enterNewPassword")}{" "}
- {localUsername}
-
-
-
-
-
- {t("auth.newPassword")}
-
-
setNewPassword(e.target.value)}
- disabled={resetLoading}
- autoComplete="new-password"
- />
-
-
-
- {t("auth.confirmNewPassword")}
-
-
setConfirmPassword(e.target.value)}
- disabled={resetLoading}
- autoComplete="new-password"
- />
-
-
- {resetLoading
- ? Spinner
- : t("auth.resetPasswordButton")}
-
-
{
- setResetStep("verify");
- setNewPassword("");
- setConfirmPassword("");
- }}
- >
- {t("common.back")}
-
-
- >
- )}
- >
- )}
-
- ) : (
-
- )}
-
-
-
-
-
- {t("common.language")}
-
-
-
-
- {isElectron() && currentServerUrl && (
-
-
-
- Server
-
-
- {currentServerUrl}
-
-
-
setShowServerConfig(true)}
- className="h-8 px-3"
- >
- Edit
-
-
- )}
-
- >
- )}
-
- );
-}
diff --git a/src/ui/Desktop/Homepage/HompageUpdateLog.tsx b/src/ui/Desktop/Homepage/HompageUpdateLog.tsx
deleted file mode 100644
index 4367f621..00000000
--- a/src/ui/Desktop/Homepage/HompageUpdateLog.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import React, { useEffect, useState } from "react";
-import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
-import { Separator } from "@/components/ui/separator.tsx";
-import { getReleasesRSS, getVersionInfo } from "@/ui/main-axios.ts";
-import { useTranslation } from "react-i18next";
-
-interface HomepageUpdateLogProps extends React.ComponentProps<"div"> {
- loggedIn: boolean;
-}
-
-interface ReleaseItem {
- id: number;
- title: string;
- description: string;
- link: string;
- pubDate: string;
- version: string;
- isPrerelease: boolean;
- isDraft: boolean;
- assets: Array<{
- name: string;
- size: number;
- download_count: number;
- download_url: string;
- }>;
-}
-
-interface RSSResponse {
- feed: {
- title: string;
- description: string;
- link: string;
- updated: string;
- };
- items: ReleaseItem[];
- total_count: number;
- cached: boolean;
- cache_age?: number;
-}
-
-interface VersionResponse {
- status: "up_to_date" | "requires_update";
- version: string;
- latest_release: {
- name: string;
- published_at: string;
- html_url: string;
- };
- cached: boolean;
- cache_age?: number;
-}
-
-export function HomepageUpdateLog({ loggedIn }: HomepageUpdateLogProps) {
- const { t } = useTranslation();
- const [releases, setReleases] = useState(null);
- const [versionInfo, setVersionInfo] = useState(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- useEffect(() => {
- if (loggedIn) {
- setLoading(true);
- Promise.all([getReleasesRSS(100), getVersionInfo()])
- .then(([releasesRes, versionRes]) => {
- setReleases(releasesRes);
- setVersionInfo(versionRes);
- setError(null);
- })
- .catch((err) => {
- setError(t("common.failedToFetchUpdateInfo"));
- })
- .finally(() => setLoading(false));
- }
- }, [loggedIn]);
-
- if (!loggedIn) {
- return null;
- }
-
- const formatDescription = (description: string) => {
- const firstLine = description.split("\n")[0];
- return firstLine.replace(/[#*`]/g, "").replace(/\s+/g, " ").trim();
- };
-
- return (
-
-
-
- {t("common.updatesAndReleases")}
-
-
-
-
- {versionInfo && versionInfo.status === "requires_update" && (
-
-
- {t("common.updateAvailable")}
-
-
- {t("common.newVersionAvailable", {
- version: versionInfo.version,
- })}
-
-
- )}
-
-
- {versionInfo && versionInfo.status === "requires_update" && (
-
- )}
-
-
- {loading && (
-
- )}
-
- {error && (
-
-
- {t("common.error")}
-
-
- {error}
-
-
- )}
-
- {releases?.items.map((release) => (
-
window.open(release.link, "_blank")}
- >
-
-
- {release.title}
-
- {release.isPrerelease && (
-
- {t("common.preRelease")}
-
- )}
-
-
-
- {formatDescription(release.description)}
-
-
-
-