diff --git a/.editorconfig b/.editorconfig index e6478bbc..f8aef6cb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.{js,jsx,ts,tsx,json,css,scss,md,yml,yaml}] +[*.{js,cjs,mjs,jsx,ts,tsx,json,css,scss,md,yml,yaml}] indent_style = space indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 5350c239..14088725 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,8 @@ * text=auto eol=lf *.js text eol=lf +*.cjs text eol=lf +*.mjs text eol=lf *.jsx text eol=lf *.ts text eol=lf *.tsx text eol=lf @@ -29,3 +31,4 @@ *.woff2 binary *.ttf binary *.eot binary +*.icns binary diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d5352b14..1adbbb54 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -25,12 +25,12 @@ jobs: fetch-depth: 1 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Determine tags id: tags @@ -57,7 +57,7 @@ jobs: echo "ALL_TAGS=$(IFS=,; echo "${ALL_TAGS[*]}")" >> $GITHUB_ENV - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: lukegus @@ -65,18 +65,18 @@ jobs: - name: Login to Docker Hub (prod only) if: ${{ github.event.inputs.build_type == 'Production' }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: bugattiguy527 password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push multi-arch image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . file: ./docker/Dockerfile push: true - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 tags: ${{ env.ALL_TAGS }} build-args: | BUILDKIT_INLINE_CACHE=1 diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index b7ab26d2..acfb0b3e 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -40,7 +40,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies @@ -145,7 +145,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version-file: ".nvmrc" cache: "npm" - name: Install system dependencies for AppImage @@ -368,7 +368,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies @@ -895,7 +895,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index 9d8cd19d..04383210 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -15,7 +15,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version-file: ".nvmrc" cache: "npm" - name: Install dependencies diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 9e77e4a2..b65c35ba 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -18,12 +18,10 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version-file: ".nvmrc" - name: Install dependencies - run: | - rm -rf node_modules package-lock.json - npm install + run: npm ci - name: Run ESLint run: npx eslint . diff --git a/.gitignore b/.gitignore index c3d02880..08f97481 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,7 @@ dist dist-ssr *.local -.vscode/* -!.vscode/extensions.json +.vscode/ .idea .DS_Store *.suo @@ -20,12 +19,14 @@ dist-ssr *.njsproj *.sln *.sw? + /db/ /release/ /.claude/ /ssl/ -.env -/.mcp.json +/uploads/ /nul -/.vscode/ +.env +electron/build-info.cjs +/.mcp.json /CLAUDE.md diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..521a9f7c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/.nvmrc b/.nvmrc index 209e3ef4..a45fd52c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +24 diff --git a/Casks/termix.rb b/Casks/termix.rb index 475f2a2c..82d8a29a 100644 --- a/Casks/termix.rb +++ b/Casks/termix.rb @@ -1,5 +1,5 @@ cask "termix" do - version "2.1.0" + version "2.2.0" sha256 "a897532f4002ed23b8b8f57312d0e0636e4b44729d0c42dad0030d80f43419cd" url "https://github.com/Termix-SSH/Termix/releases/download/release-#{version}-tag/termix_macos_universal_dmg.dmg" diff --git a/README.md b/README.md index bb930675..117836d6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ free and self-hosted alternative to Termius available for all platforms. - **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. - **Remote Desktop Access** - RDP, VNC, and Telnet support over the browser with complete customization and split screening -- **SSH Tunnel Management** - Create and manage SSH tunnels with automatic reconnection and health monitoring and support for -l or -r connections +- **SSH Tunnel Management** - Create and manage server-to-server SSH tunnels with automatic reconnection, health monitoring, and local, remote, or dynamic SOCKS forwarding. Desktop client-to-server tunnel settings are stored locally per desktop install, optional C2S preset snapshots can be saved to the server, renamed, loaded, or deleted when you want to move a local tunnel configuration between clients. - **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 with sudo support. - **Docker Management** - Start, stop, pause, remove containers. View container stats. Control container using docker exec terminal. It was not made to replace Portainer or Dockge but rather to simply manage your containers compared to creating them. - **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 @@ -46,6 +46,7 @@ free and self-hosted alternative to Termius available for all platforms. - **RBAC** - Create roles and share hosts across users/roles - **User Authentication** - Secure user management with admin controls and OIDC (with access control) and 2FA (TOTP) support. View active user sessions across all platforms and revoke permissions. Link your OIDC/Local accounts together. - **Database Encryption** - Backend stored as encrypted SQLite database files. View [docs](https://docs.termix.site/security) for more. +- **API Keys** - Create user-scoped API keys with expiration dates to be used for automation/CI - **Data Export/Import** - Export and import SSH hosts, credentials, and file manager data - **Automatic SSL Setup** - Built-in SSL certificate generation and management with HTTPS redirects - **Modern UI** - Clean desktop/mobile-friendly interface built with React, Tailwind CSS, and Shadcn. Choose between many different UI themes including light, dark, Dracula, etc. Use URL routes to open any connection in full-screen. diff --git a/docker/Dockerfile b/docker/Dockerfile index c8c701aa..f766cace 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,14 @@ # Stage 1: Install dependencies -FROM node:22-slim AS deps +FROM node:24-slim AS deps WORKDIR /app RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ +COPY .npmrc ./ +COPY vendor ./vendor -RUN npm ci --ignore-scripts --force && \ +RUN npm ci --ignore-scripts && \ npm cache clean --force # Stage 2: Build frontend @@ -26,24 +28,26 @@ WORKDIR /app COPY . . -RUN npm rebuild better-sqlite3 --force +RUN npm rebuild better-sqlite3 RUN npm run build:backend # Stage 4: Production dependencies only -FROM node:22-slim AS production-deps +FROM node:24-slim AS production-deps WORKDIR /app RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ +COPY .npmrc ./ +COPY vendor ./vendor -RUN npm ci --only=production --ignore-scripts --force && \ - npm rebuild better-sqlite3 bcryptjs --force && \ +RUN npm ci --omit=dev --ignore-scripts && \ + npm rebuild better-sqlite3 bcryptjs && \ npm cache clean --force # Stage 5: Final optimized image -FROM node:22-slim +FROM node:24-slim WORKDIR /app ENV DATA_DIR=/app/data \ @@ -53,11 +57,9 @@ ENV DATA_DIR=/app/data \ RUN apt-get update && apt-get install -y nginx gettext-base openssl ca-certificates gosu wget && \ update-ca-certificates && \ rm -rf /var/lib/apt/lists/* && \ - mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /app/nginx/logs /app/nginx/cache /app/nginx/client_body && \ - chown -R node:node /app && \ - chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx && \ - touch /app/nginx/nginx.conf && \ - chown node:node /app/nginx/nginx.conf + mkdir -p /app/data /app/uploads /app/data/.opk /app/nginx /tmp/nginx && \ + chown -R node:node /app /tmp/nginx && \ + chmod 755 /app/data /app/uploads /app/data/.opk /app/nginx /tmp/nginx COPY docker/nginx.conf /app/nginx/nginx.conf.template COPY docker/nginx-https.conf /app/nginx/nginx-https.conf.template diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index d0c8aeb3..511bb338 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -7,14 +7,14 @@ PGID=${PGID:-1000} if [ "$(id -u)" = "0" ]; then if [ "$PUID" = "0" ]; then echo "Running as root (PUID=0, PGID=$PGID)" - chown -R root:root /app/data /app/uploads /app/nginx 2>/dev/null || true + chown -R root:root /app/data /app/uploads /tmp/nginx 2>/dev/null || true else echo "Setting up user permissions (PUID: $PUID, PGID: $PGID)..." groupmod -o -g "$PGID" node 2>/dev/null || true usermod -o -u "$PUID" node 2>/dev/null || true - chown -R node:node /app/data /app/uploads /app/nginx 2>/dev/null || true + chown -R node:node /app/data /app/uploads /tmp/nginx 2>/dev/null || true echo "User node is now UID: $PUID, GID: $PGID" @@ -38,7 +38,8 @@ else NGINX_CONF_SOURCE="/app/nginx/nginx.conf.template" fi -envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /app/nginx/nginx.conf +mkdir -p /tmp/nginx +envsubst '${PORT} ${SSL_PORT} ${SSL_CERT_PATH} ${SSL_KEY_PATH}' < $NGINX_CONF_SOURCE > /tmp/nginx/nginx.conf mkdir -p /app/data /app/uploads /app/data/.opk chmod 755 /app/data /app/uploads /app/data/.opk 2>/dev/null || true @@ -132,7 +133,7 @@ EOF fi echo "Starting nginx..." -nginx -c /app/nginx/nginx.conf +nginx -c /tmp/nginx/nginx.conf echo "Starting backend services..." cd /app diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf index 74dbae3e..907d03e3 100644 --- a/docker/nginx-https.conf +++ b/docker/nginx-https.conf @@ -1,7 +1,7 @@ worker_processes 1; master_process off; -pid /app/nginx/nginx.pid; -error_log /app/nginx/logs/error.log warn; +pid /tmp/nginx/nginx.pid; +error_log /tmp/nginx/error.log warn; events { worker_connections 1024; @@ -11,13 +11,13 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; - access_log /app/nginx/logs/access.log; + access_log /tmp/nginx/access.log; - client_body_temp_path /app/nginx/client_body; - proxy_temp_path /app/nginx/proxy_temp; - fastcgi_temp_path /app/nginx/fastcgi_temp; - uwsgi_temp_path /app/nginx/uwsgi_temp; - scgi_temp_path /app/nginx/scgi_temp; + client_body_temp_path /tmp/nginx/client_body; + proxy_temp_path /tmp/nginx/proxy_temp; + fastcgi_temp_path /tmp/nginx/fastcgi_temp; + uwsgi_temp_path /tmp/nginx/uwsgi_temp; + scgi_temp_path /tmp/nginx/scgi_temp; sendfile on; keepalive_timeout 65; @@ -65,16 +65,32 @@ http { add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; + location = /sw.js { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + + location = /manifest.json { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /app/html; expires 1y; - add_header Cache-Control "public, immutable"; + add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } location / { root /app/html; index index.html index.htm; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri $uri/ /index.html; } @@ -162,6 +178,15 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/c2s-tunnel-presets(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_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 ~ ^/terminal(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -337,6 +362,21 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location /ssh/tunnel/ { + proxy_pass http://127.0.0.1:30003; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_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_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_buffering off; + proxy_cache off; + } + location /host/file_manager/recent { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -455,6 +495,15 @@ http { proxy_read_timeout 60s; } + location ~ ^/(refresh|host-updated)$ { + proxy_pass http://127.0.0.1:30005; + proxy_http_version 1.1; + proxy_set_header Host $http_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 ~ ^/global-settings(/.*)?$ { proxy_pass http://127.0.0.1:30005; proxy_http_version 1.1; diff --git a/docker/nginx.conf b/docker/nginx.conf index 5a7eca7f..915b959c 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -1,7 +1,7 @@ worker_processes 1; master_process off; -pid /app/nginx/nginx.pid; -error_log /app/nginx/logs/error.log warn; +pid /tmp/nginx/nginx.pid; +error_log /tmp/nginx/error.log warn; events { worker_connections 1024; @@ -11,13 +11,13 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; - access_log /app/nginx/logs/access.log; + access_log /tmp/nginx/access.log; - client_body_temp_path /app/nginx/client_body; - proxy_temp_path /app/nginx/proxy_temp; - fastcgi_temp_path /app/nginx/fastcgi_temp; - uwsgi_temp_path /app/nginx/uwsgi_temp; - scgi_temp_path /app/nginx/scgi_temp; + client_body_temp_path /tmp/nginx/client_body; + proxy_temp_path /tmp/nginx/proxy_temp; + fastcgi_temp_path /tmp/nginx/fastcgi_temp; + uwsgi_temp_path /tmp/nginx/uwsgi_temp; + scgi_temp_path /tmp/nginx/scgi_temp; sendfile on; keepalive_timeout 65; @@ -54,16 +54,32 @@ http { add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; + location = /sw.js { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + + location = /manifest.json { + root /app/html; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + try_files $uri =404; + } + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /app/html; expires 1y; - add_header Cache-Control "public, immutable"; + add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } location / { root /app/html; index index.html index.htm; + expires off; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; try_files $uri $uri/ /index.html; } @@ -151,6 +167,15 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location ~ ^/c2s-tunnel-presets(/.*)?$ { + proxy_pass http://127.0.0.1:30001; + proxy_http_version 1.1; + proxy_set_header Host $http_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 ~ ^/terminal(/.*)?$ { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -326,6 +351,21 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } + location /ssh/tunnel/ { + proxy_pass http://127.0.0.1:30003; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_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_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_buffering off; + proxy_cache off; + } + location /host/file_manager/recent { proxy_pass http://127.0.0.1:30001; proxy_http_version 1.1; @@ -444,6 +484,15 @@ http { proxy_read_timeout 60s; } + location ~ ^/(refresh|host-updated)$ { + proxy_pass http://127.0.0.1:30005; + proxy_http_version 1.1; + proxy_set_header Host $http_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 ~ ^/global-settings(/.*)?$ { proxy_pass http://127.0.0.1:30005; proxy_http_version 1.1; diff --git a/electron-builder.json b/electron-builder.json index 9bbca395..094c0ef8 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -5,7 +5,8 @@ "directories": { "output": "release" }, - "asar": false, + "asar": true, + "asarUnpack": ["dist/backend/**/*", "node_modules/**/*.node"], "files": [ "dist/**/*", "electron/**/*", @@ -130,7 +131,6 @@ "entitlementsInherit": "build/entitlements.mas.inherit.plist", "hardenedRuntime": false, "gatekeeperAssess": false, - "asar": false, "type": "distribution", "category": "public.app-category.developer-tools", "artifactName": "termix_macos_${arch}_mas.${ext}", diff --git a/electron/main.cjs b/electron/main.cjs index 8fb19427..8966cd6c 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -5,17 +5,28 @@ const { ipcMain, dialog, Menu, + session, + safeStorage, Tray, + clipboard, } = require("electron"); const path = require("path"); const fs = require("fs"); const os = require("os"); const https = require("https"); const http = require("http"); +const net = require("net"); const { URL } = require("url"); const { fork } = require("child_process"); +const WebSocket = require("ws"); const logFile = path.join(app.getPath("userData"), "termix-main.log"); +const electronAuthCookiesPath = path.join( + app.getPath("userData"), + "electron-auth-cookies.json", +); +const electronAuthCookies = new Map(); + function logToFile(...args) { const timestamp = new Date().toISOString(); const msg = args @@ -30,6 +41,331 @@ function logToFile(...args) { console.log(...args); } +function getCookieOrigin(url) { + try { + const parsedUrl = new URL(url); + const protocol = + parsedUrl.protocol === "ws:" + ? "http:" + : parsedUrl.protocol === "wss:" + ? "https:" + : parsedUrl.protocol; + return `${protocol}//${parsedUrl.host}`; + } catch { + return null; + } +} + +function parseCookieTarget(url) { + try { + const parsedUrl = new URL(url); + if (parsedUrl.protocol === "ws:") { + parsedUrl.protocol = "http:"; + } else if (parsedUrl.protocol === "wss:") { + parsedUrl.protocol = "https:"; + } + return parsedUrl; + } catch { + return null; + } +} + +function getElectronAuthCookieKey(name, origin) { + return origin ? `${origin}|${name}` : null; +} + +function getSafeStorageAvailable() { + try { + return safeStorage.isEncryptionAvailable(); + } catch { + return false; + } +} + +function encodeElectronAuthCookieValue(value) { + return { + encrypted: true, + value: safeStorage.encryptString(value).toString("base64"), + }; +} + +function decodeElectronAuthCookieValue(record) { + if (!record.encrypted || !getSafeStorageAvailable()) { + return null; + } + + try { + return safeStorage.decryptString(Buffer.from(record.value, "base64")); + } catch (error) { + logToFile( + "Failed to decrypt persisted Electron auth cookie:", + error.message, + ); + return null; + } +} + +function isElectronAuthCookieExpired(cookie) { + return Number.isFinite(cookie.expiresAt) && cookie.expiresAt <= Date.now(); +} + +function saveElectronAuthCookiesToDisk() { + try { + if (!getSafeStorageAvailable()) { + if (fs.existsSync(electronAuthCookiesPath)) { + fs.rmSync(electronAuthCookiesPath, { force: true }); + } + return; + } + + const records = []; + + for (const [key, cookie] of electronAuthCookies.entries()) { + if (isElectronAuthCookieExpired(cookie)) { + electronAuthCookies.delete(key); + continue; + } + + records.push({ + key, + name: cookie.name, + origin: cookie.origin, + path: cookie.path, + expiresAt: cookie.expiresAt, + ...encodeElectronAuthCookieValue(cookie.value), + }); + } + + fs.writeFileSync( + electronAuthCookiesPath, + JSON.stringify({ version: 1, records }, null, 2), + ); + } catch (error) { + logToFile("Failed to persist Electron auth cookies:", error.message); + } +} + +function loadElectronAuthCookiesFromDisk() { + electronAuthCookies.clear(); + + try { + if (!getSafeStorageAvailable()) { + if (fs.existsSync(electronAuthCookiesPath)) { + fs.rmSync(electronAuthCookiesPath, { force: true }); + } + return; + } + + if (!fs.existsSync(electronAuthCookiesPath)) { + return; + } + + const data = JSON.parse(fs.readFileSync(electronAuthCookiesPath, "utf8")); + const records = Array.isArray(data.records) ? data.records : []; + + for (const record of records) { + if ( + !record || + typeof record.key !== "string" || + typeof record.name !== "string" || + typeof record.origin !== "string" + ) { + continue; + } + + const value = decodeElectronAuthCookieValue(record); + if (!value) { + continue; + } + + const cookie = { + name: record.name, + value, + origin: record.origin, + path: typeof record.path === "string" ? record.path : "/", + expiresAt: Number.isFinite(record.expiresAt) ? record.expiresAt : null, + }; + + if (!isElectronAuthCookieExpired(cookie)) { + electronAuthCookies.set(record.key, cookie); + } + } + + saveElectronAuthCookiesToDisk(); + } catch (error) { + logToFile("Failed to load persisted Electron auth cookies:", error.message); + } +} + +function clearPersistedElectronAuthCookies() { + electronAuthCookies.clear(); + try { + if (fs.existsSync(electronAuthCookiesPath)) { + fs.rmSync(electronAuthCookiesPath, { force: true }); + } + } catch (error) { + logToFile( + "Failed to clear persisted Electron auth cookies:", + error.message, + ); + } +} + +function parseSetCookieHeader(header) { + const [cookiePair, ...attributes] = String(header || "").split(";"); + const separatorIndex = cookiePair.indexOf("="); + if (separatorIndex <= 0) return null; + + const parsed = { + name: cookiePair.slice(0, separatorIndex).trim(), + value: cookiePair.slice(separatorIndex + 1).trim(), + path: "/", + maxAge: null, + expires: null, + }; + + for (const attribute of attributes) { + const [rawName, ...rawValueParts] = attribute.trim().split("="); + const attrName = rawName.toLowerCase(); + const attrValue = rawValueParts.join("="); + + if (attrName === "path" && attrValue) { + parsed.path = attrValue; + } else if (attrName === "max-age" && attrValue) { + const maxAge = Number(attrValue); + parsed.maxAge = Number.isFinite(maxAge) ? maxAge : null; + } else if (attrName === "expires" && attrValue) { + const expires = Date.parse(attrValue); + parsed.expires = Number.isFinite(expires) ? expires : null; + } + } + + return parsed; +} + +function rememberElectronAuthCookieFromHeader(url, header) { + const origin = getCookieOrigin(url); + if (!origin) return; + + const cookie = parseSetCookieHeader(header); + if (!cookie || cookie.name !== "jwt") return; + + const key = getElectronAuthCookieKey(cookie.name, origin); + if (!key) return; + + const expired = + cookie.maxAge === 0 || + (cookie.expires !== null && cookie.expires <= Date.now()); + + if (expired || !cookie.value) { + electronAuthCookies.delete(key); + saveElectronAuthCookiesToDisk(); + return; + } + + const expiresAt = + cookie.maxAge !== null ? Date.now() + cookie.maxAge * 1000 : cookie.expires; + + electronAuthCookies.set(key, { + name: cookie.name, + value: cookie.value, + origin, + path: cookie.path, + expiresAt, + }); + saveElectronAuthCookiesToDisk(); +} + +function getRememberedElectronAuthCookie(name, targetUrl) { + const target = parseCookieTarget(targetUrl); + if (!target) return null; + + const exactKey = getElectronAuthCookieKey(name, target.origin); + const exactCookie = exactKey ? electronAuthCookies.get(exactKey) : null; + if (exactCookie && !isElectronAuthCookieExpired(exactCookie)) { + return exactCookie; + } + + if (target.protocol !== "https:") { + return null; + } + + const httpOrigin = `http://${target.host}`; + const httpKey = getElectronAuthCookieKey(name, httpOrigin); + const httpCookie = httpKey ? electronAuthCookies.get(httpKey) : null; + return httpCookie && !isElectronAuthCookieExpired(httpCookie) + ? httpCookie + : null; +} + +function getHeaderName(headers, name) { + const lowerName = name.toLowerCase(); + return Object.keys(headers || {}).find( + (key) => key.toLowerCase() === lowerName, + ); +} + +function setCookieHeaderValue(requestHeaders, name, value) { + const headerName = getHeaderName(requestHeaders, "Cookie") || "Cookie"; + const existing = requestHeaders[headerName]; + const existingValue = Array.isArray(existing) + ? existing.join("; ") + : existing; + const nextCookie = `${name}=${value}`; + const otherCookies = String(existingValue || "") + .split(";") + .map((cookie) => cookie.trim()) + .filter((cookie) => cookie && !cookie.startsWith(`${name}=`)); + + requestHeaders[headerName] = + otherCookies.length > 0 + ? `${otherCookies.join("; ")}; ${nextCookie}` + : nextCookie; +} + +function parseSemver(version) { + const match = String(version || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/); + if (!match) return null; + + return [Number(match[1]), Number(match[2]), Number(match[3] || 0)]; +} + +function compareSemver(a, b) { + const parsedA = parseSemver(a); + const parsedB = parseSemver(b); + if (!parsedA || !parsedB) return null; + + for (let i = 0; i < 3; i += 1) { + if (parsedA[i] > parsedB[i]) return 1; + if (parsedA[i] < parsedB[i]) return -1; + } + + return 0; +} + +const INSECURE_MODE_VALUES = new Set(["true", "1", "yes"]); + +function isInsecureModeEnabled() { + return INSECURE_MODE_VALUES.has( + String(process.env.ENABLE_INSECURE_MODE || "") + .trim() + .toLowerCase(), + ); +} + +function getTlsVerificationOptions() { + return { + rejectUnauthorized: !isInsecureModeEnabled(), + }; +} + +function getWebSocketOptions(url, options = {}) { + return { + ...options, + ...(String(url).startsWith("wss:") ? getTlsVerificationOptions() : {}), + }; +} + function httpFetch(url, options = {}) { return new Promise((resolve, reject) => { const urlObj = new URL(url); @@ -40,16 +376,9 @@ function httpFetch(url, options = {}) { method: options.method || "GET", headers: options.headers || {}, timeout: options.timeout || 10000, + ...(isHttps ? getTlsVerificationOptions() : {}), }; - if (isHttps) { - requestOptions.rejectUnauthorized = false; - requestOptions.agent = new https.Agent({ - rejectUnauthorized: false, - checkServerIdentity: () => undefined, - }); - } - const req = client.request(url, requestOptions, (res) => { let data = ""; res.on("data", (chunk) => (data += chunk)); @@ -82,9 +411,14 @@ if (process.platform === "linux") { app.commandLine.appendSwitch("--enable-features=VaapiVideoDecoder"); } -app.commandLine.appendSwitch("--ignore-certificate-errors"); -app.commandLine.appendSwitch("--ignore-ssl-errors"); -app.commandLine.appendSwitch("--ignore-certificate-errors-spki-list"); +if (isInsecureModeEnabled()) { + logToFile( + "[security] ENABLE_INSECURE_MODE is enabled; TLS certificate validation is disabled.", + ); + 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"); let mainWindow = null; @@ -94,6 +428,137 @@ let isQuitting = false; const isDev = process.env.NODE_ENV === "development" || !app.isPackaged; const appRoot = isDev ? process.cwd() : path.join(__dirname, ".."); +const electronCacheBuildPath = path.join( + app.getPath("userData"), + "client-cache-build.json", +); +const termixSessionPartition = "persist:termix"; + +function getElectronBuildTimestamp() { + try { + const buildInfo = require("./build-info.cjs"); + if (Number.isInteger(buildInfo.buildTimestamp)) { + return buildInfo.buildTimestamp; + } + } catch { + // Development runs may not have generated build metadata yet. + } + + return 0; +} + +async function clearElectronClientCacheIfBuildChanged() { + const buildTimestamp = getElectronBuildTimestamp(); + let cacheTimestamp = 0; + + try { + if (fs.existsSync(electronCacheBuildPath)) { + const data = JSON.parse(fs.readFileSync(electronCacheBuildPath, "utf8")); + cacheTimestamp = Number.isInteger(data.buildTimestamp) + ? data.buildTimestamp + : 0; + } + } catch (error) { + logToFile( + "Failed to read Electron client cache build info:", + error.message, + ); + } + + if (cacheTimestamp === buildTimestamp) { + return; + } + + const clearStep = async (label, action) => { + try { + await action(); + } catch (error) { + logToFile(`Failed to clear Electron ${label}:`, error.message); + } + }; + + try { + const defaultSession = session.defaultSession; + await clearStep("HTTP cache", () => defaultSession.clearCache()); + await clearStep("code cache", () => + defaultSession.clearCodeCaches({ urls: [] }), + ); + await clearStep("auth cache", () => defaultSession.clearAuthCache()); + await clearStep("storage data", () => + defaultSession.clearStorageData({ + storages: [ + "appcache", + "cookies", + "filesystem", + "shadercache", + "websql", + "serviceworkers", + "cachestorage", + ], + }), + ); + + fs.writeFileSync( + electronCacheBuildPath, + JSON.stringify( + { + buildTimestamp, + appVersion: app.getVersion(), + updatedAt: new Date().toISOString(), + }, + null, + 2, + ), + ); + + logToFile("Electron client cache cleared for build change", { + from: cacheTimestamp, + to: buildTimestamp, + appVersion: app.getVersion(), + }); + } catch (error) { + logToFile("Failed to clear Electron client cache:", error.message); + } +} + +function getCookieRemovalUrl(cookie) { + const scheme = cookie.secure ? "https" : "http"; + const domain = cookie.domain?.startsWith(".") + ? cookie.domain.slice(1) + : cookie.domain || "localhost"; + return `${scheme}://${domain}${cookie.path || "/"}`; +} + +async function clearElectronJwtCookiesAtStartup() { + loadElectronAuthCookiesFromDisk(); + + const targetSessions = new Set([ + session.defaultSession, + session.fromPartition(termixSessionPartition), + ]); + + for (const targetSession of targetSessions) { + try { + const cookies = await targetSession.cookies.get({ name: "jwt" }); + await Promise.all( + cookies.map((cookie) => + targetSession.cookies.remove( + getCookieRemovalUrl(cookie), + cookie.name, + ), + ), + ); + + if (cookies.length > 0) { + logToFile("Cleared Electron JWT cookies from cookie store", { + count: cookies.length, + }); + } + } catch (error) { + logToFile("Failed to clear Electron JWT cookies:", error.message); + } + } +} function getBackendEntryPath() { if (isDev) { @@ -323,7 +788,7 @@ function createWindow() { contextIsolation: true, webSecurity: false, preload: path.join(__dirname, "preload.js"), - partition: "persist:termix", + partition: termixSessionPartition, allowRunningInsecureContent: true, webviewTag: true, offscreen: false, @@ -335,12 +800,13 @@ function createWindow() { (webContents, permission, callback) => { if ( permission === "clipboard-read" || + permission === "clipboard-write" || permission === "clipboard-sanitized-write" ) { callback(true); return; } - callback(true); + callback(false); }, ); @@ -357,6 +823,15 @@ function createWindow() { details.requestHeaders["User-Agent"] = customUserAgent; + const rememberedJwt = getRememberedElectronAuthCookie("jwt", details.url); + if (rememberedJwt) { + setCookieHeaderValue( + details.requestHeaders, + rememberedJwt.name, + rememberedJwt.value, + ); + } + callback({ requestHeaders: details.requestHeaders }); }, ); @@ -402,8 +877,17 @@ function createWindow() { } } - if (headers["set-cookie"]) { - headers["set-cookie"] = headers["set-cookie"].map((cookie) => { + const setCookieHeaderName = getHeaderName(headers, "Set-Cookie"); + if (setCookieHeaderName) { + const setCookieHeaders = Array.isArray(headers[setCookieHeaderName]) + ? headers[setCookieHeaderName] + : [headers[setCookieHeaderName]]; + + setCookieHeaders.forEach((cookie) => { + rememberElectronAuthCookieFromHeader(details.url, cookie); + }); + + headers[setCookieHeaderName] = setCookieHeaders.map((cookie) => { let modified = cookie.replace( /;\s*SameSite=Strict/gi, "; SameSite=None", @@ -548,11 +1032,17 @@ ipcMain.handle("check-electron-update", async () => { }; } - const isUpToDate = localVersion === remoteVersion; + const versionComparison = compareSemver(localVersion, remoteVersion); + const status = + versionComparison === null || versionComparison === 0 + ? "up_to_date" + : versionComparison > 0 + ? "beta" + : "requires_update"; const result = { success: true, - status: isUpToDate ? "up_to_date" : "requires_update", + status, localVersion: localVersion, remoteVersion: remoteVersion, latest_release: { @@ -621,6 +1111,1066 @@ ipcMain.handle("save-server-config", (event, config) => { } }); +function getC2STunnelConfigPath() { + return path.join(app.getPath("userData"), "c2s-tunnels.json"); +} + +ipcMain.handle("get-c2s-tunnel-config", () => { + try { + const configPath = getC2STunnelConfigPath(); + if (!fs.existsSync(configPath)) { + return []; + } + const configData = fs.readFileSync(configPath, "utf8"); + const parsed = JSON.parse(configData); + return Array.isArray(parsed) ? parsed : []; + } catch (error) { + console.error("Error reading C2S tunnel config:", error); + return []; + } +}); + +ipcMain.handle("save-c2s-tunnel-config", async (_event, config) => { + try { + if (!Array.isArray(config)) { + return { success: false, error: "C2S tunnel config must be an array" }; + } + const autoStartListeners = new Set(); + const autoStartRemoteListeners = new Set(); + for (const tunnel of config) { + if (!tunnel?.autoStart) continue; + const mode = tunnel.mode || tunnel.tunnelType || "local"; + if (mode === "remote") { + const sourceHostId = Number(tunnel.sourceHostId); + const sourcePort = Number(tunnel.sourcePort); + if ( + !Number.isInteger(sourceHostId) || + sourceHostId < 1 || + !Number.isInteger(sourcePort) || + sourcePort < 1 || + sourcePort > 65535 + ) { + return { + success: false, + error: "Invalid remote client tunnel endpoint or port", + }; + } + const listenerKey = `${sourceHostId}:${sourcePort}`; + if (autoStartRemoteListeners.has(listenerKey)) { + return { + success: false, + error: `Another auto-start client tunnel already uses remote ${listenerKey}`, + }; + } + autoStartRemoteListeners.add(listenerKey); + continue; + } + + const bindHost = tunnel.bindHost || "127.0.0.1"; + const sourcePort = Number(tunnel.sourcePort); + const listenerKey = `${bindHost}:${sourcePort}`; + if (autoStartListeners.has(listenerKey)) { + return { + success: false, + error: `Another auto-start client tunnel already uses ${listenerKey}`, + }; + } + autoStartListeners.add(listenerKey); + } + for (const listenerKey of autoStartListeners) { + const [bindHost, sourcePort] = listenerKey.split(":"); + const result = await checkLocalPortAvailable( + bindHost, + Number(sourcePort), + ); + const ownedByClientTunnel = Array.from(c2sTunnelRuntimes.values()).some( + (runtime) => + runtime.bindHost === bindHost && + runtime.sourcePort === Number(sourcePort), + ); + if (!result.available && !ownedByClientTunnel) { + return { + success: false, + error: `Cannot auto-start client tunnel on ${listenerKey}: ${result.error || "port is already in use"}`, + }; + } + } + const userDataPath = app.getPath("userData"); + if (!fs.existsSync(userDataPath)) { + fs.mkdirSync(userDataPath, { recursive: true }); + } + fs.writeFileSync(getC2STunnelConfigPath(), JSON.stringify(config, null, 2)); + return { success: true }; + } catch (error) { + console.error("Error saving C2S tunnel config:", error); + return { success: false, error: error.message }; + } +}); + +function checkLocalPortAvailable(host, port) { + return new Promise((resolve) => { + const server = net.createServer(); + server.once("error", (error) => { + resolve({ available: false, error: error.message }); + }); + server.once("listening", () => { + server.close(() => resolve({ available: true })); + }); + server.listen({ host, port }); + }); +} + +function checkTcpConnection(host, port) { + return new Promise((resolve) => { + const socket = net.createConnection({ host, port }); + const timer = setTimeout(() => { + socket.destroy(); + resolve({ success: false, error: "Connection timed out" }); + }, 5000); + + socket.once("connect", () => { + clearTimeout(timer); + socket.destroy(); + resolve({ success: true }); + }); + socket.once("error", (error) => { + clearTimeout(timer); + socket.destroy(); + resolve({ success: false, error: error.message }); + }); + }); +} + +const c2sTunnelRuntimes = new Map(); +const C2S_WS_HIGH_WATERMARK = 1024 * 1024; +const C2S_WS_LOW_WATERMARK = 256 * 1024; +const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024; + +function getServerConfigSync() { + try { + const configPath = path.join(app.getPath("userData"), "server-config.json"); + if (!fs.existsSync(configPath)) return null; + return JSON.parse(fs.readFileSync(configPath, "utf8")); + } catch { + return null; + } +} + +function getC2SRelayUrl() { + const config = getServerConfigSync(); + const serverUrl = + config?.serverUrl || (!isDev ? "http://127.0.0.1:30003" : null); + if (!serverUrl) { + throw new Error("No Termix server configured"); + } + + const base = serverUrl.replace(/\/$/, ""); + const relayHttpUrl = base.endsWith(":30003") + ? `${base}/ssh/tunnel/c2s/stream` + : `${base}/ssh/tunnel/c2s/stream`; + return relayHttpUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:"); +} + +async function getC2SRelayHeaders(relayUrl) { + if (!mainWindow?.webContents?.session) return {}; + + const cookieUrl = relayUrl + .replace(/^ws:/, "http:") + .replace(/^wss:/, "https:"); + const cookies = await mainWindow.webContents.session.cookies.get({ + url: cookieUrl, + name: "jwt", + }); + const jwt = cookies[0]?.value; + if (!jwt) return {}; + + return { + Cookie: `jwt=${encodeURIComponent(jwt)}`, + }; +} + +function getC2STunnelName(tunnel, index = 0) { + if (tunnel.name) return tunnel.name; + return [ + "c2s", + index, + tunnel.sourceHostId || 0, + tunnel.mode || tunnel.tunnelType || "local", + tunnel.bindHost || "127.0.0.1", + tunnel.sourcePort, + tunnel.endpointPort || 0, + ].join("::"); +} + +function getC2STunnelStatus(tunnelName) { + return ( + c2sTunnelRuntimes.get(tunnelName)?.status || { + connected: false, + status: "DISCONNECTED", + } + ); +} + +function getAllC2STunnelStatuses() { + const statuses = {}; + for (const [tunnelName] of c2sTunnelRuntimes.entries()) { + statuses[tunnelName] = getC2STunnelStatus(tunnelName); + } + return statuses; +} + +function emitC2STunnelStatuses() { + if (!mainWindow || mainWindow.isDestroyed()) return; + mainWindow.webContents.send("c2s-tunnel-statuses", getAllC2STunnelStatuses()); +} + +function setC2STunnelStatus(tunnelName, status) { + const runtime = c2sTunnelRuntimes.get(tunnelName); + if (runtime) { + runtime.status = status; + emitC2STunnelStatuses(); + } +} + +function setC2STunnelError(tunnelName, message) { + logToFile(`[c2s] ${tunnelName} failed:`, message); + setC2STunnelStatus(tunnelName, { + connected: false, + status: "ERROR", + reason: message, + }); +} + +function parseSocks5Target(buffer) { + if (buffer.length < 7 || buffer[0] !== 0x05 || buffer[1] !== 0x01) { + return null; + } + + const addressType = buffer[3]; + let offset = 4; + let host; + + if (addressType === 0x01) { + if (buffer.length < offset + 4 + 2) return null; + host = Array.from(buffer.subarray(offset, offset + 4)).join("."); + offset += 4; + } else if (addressType === 0x03) { + const length = buffer[offset]; + offset += 1; + if (buffer.length < offset + length + 2) return null; + host = buffer.subarray(offset, offset + length).toString("utf8"); + offset += length; + } else if (addressType === 0x04) { + if (buffer.length < offset + 16 + 2) return null; + const parts = []; + for (let i = 0; i < 16; i += 2) { + parts.push(buffer.readUInt16BE(offset + i).toString(16)); + } + host = parts.join(":"); + offset += 16; + } else { + throw new Error("Unsupported SOCKS5 address type"); + } + + const port = buffer.readUInt16BE(offset); + return { host, port, bytesRead: offset + 2 }; +} + +async function openC2SRelay( + tunnel, + targetHost, + targetPort, + socket, + initialData, +) { + const tunnelName = tunnel.name || getC2STunnelName(tunnel); + const relayUrl = getC2SRelayUrl(); + const headers = await getC2SRelayHeaders(relayUrl); + logToFile(`[c2s] opening relay for ${tunnelName}`, { + relayUrl, + targetHost, + targetPort, + }); + setC2STunnelStatus(tunnelName, { + connected: false, + status: "CONNECTING", + reason: `Opening relay to ${targetHost}:${targetPort}`, + }); + const ws = new WebSocket( + relayUrl, + getWebSocketOptions(relayUrl, { headers }), + ); + const pendingChunks = []; + let ready = false; + let closed = false; + + const cleanup = () => { + if (closed) return; + closed = true; + try { + socket.destroy(); + } catch { + // expected during shutdown + } + try { + ws.close(); + } catch { + // expected during shutdown + } + }; + + const sendChunk = (chunk) => { + if (ready && ws.readyState === WebSocket.OPEN) { + ws.send(chunk); + } else { + pendingChunks.push(chunk); + } + }; + + socket.on("data", sendChunk); + socket.on("close", cleanup); + socket.on("error", (error) => { + setC2STunnelError(tunnelName, error.message || "Local socket error"); + cleanup(); + }); + ws.on("close", cleanup); + ws.on("error", (error) => { + setC2STunnelError(tunnelName, error.message || "Relay connection failed"); + cleanup(); + }); + + ws.on("open", () => { + logToFile(`[c2s] relay connected for ${tunnelName}`); + ws.send( + JSON.stringify({ + type: "open", + tunnelConfig: tunnel, + targetHost, + targetPort, + }), + ); + }); + + ws.on("message", (data, isBinary) => { + if (isBinary) { + socket.write(Buffer.isBuffer(data) ? data : Buffer.from(data)); + return; + } + + try { + const message = JSON.parse(data.toString()); + if (message.type === "ready") { + ready = true; + logToFile(`[c2s] relay ready for ${tunnelName}`); + setC2STunnelStatus(tunnelName, { + connected: true, + status: "CONNECTED", + }); + if (initialData?.length) { + ws.send(initialData); + } + while (pendingChunks.length > 0) { + ws.send(pendingChunks.shift()); + } + } else if (message.type === "error") { + logToFile("[c2s] relay error:", message.error); + setC2STunnelError( + tunnelName, + message.error || "Relay rejected the client tunnel", + ); + cleanup(); + } + } catch (error) { + logToFile("[c2s] invalid relay message:", error.message); + setC2STunnelError(tunnelName, error.message || "Invalid relay response"); + cleanup(); + } + }); +} + +async function testC2SRelay(tunnel, targetHost, targetPort) { + const relayUrl = getC2SRelayUrl(); + const headers = await getC2SRelayHeaders(relayUrl); + const ws = new WebSocket( + relayUrl, + getWebSocketOptions(relayUrl, { headers }), + ); + + return new Promise((resolve) => { + let settled = false; + const settle = (result) => { + if (settled) return; + settled = true; + try { + ws.close(); + } catch { + // expected during shutdown + } + resolve(result); + }; + + const timer = setTimeout(() => { + settle({ success: false, error: "Tunnel test timed out" }); + }, 15000); + + ws.on("open", () => { + ws.send( + JSON.stringify({ + type: "test", + tunnelConfig: tunnel, + targetHost, + targetPort, + }), + ); + }); + ws.on("message", (data, isBinary) => { + if (isBinary) return; + + try { + const message = JSON.parse(data.toString()); + if (message.type === "ready") { + clearTimeout(timer); + settle({ success: true }); + } else if (message.type === "error") { + clearTimeout(timer); + settle({ + success: false, + error: message.error || "Tunnel test failed", + }); + } + } catch (error) { + clearTimeout(timer); + settle({ success: false, error: error.message }); + } + }); + ws.on("error", (error) => { + clearTimeout(timer); + settle({ success: false, error: error.message }); + }); + ws.on("close", () => { + clearTimeout(timer); + settle({ success: false, error: "Tunnel test connection closed" }); + }); + }); +} + +async function testC2STunnel(tunnel, index = 0) { + const mode = tunnel.mode || tunnel.tunnelType || "local"; + const testTunnel = { + ...tunnel, + name: `${getC2STunnelName(tunnel, index)}::test`, + mode, + }; + const bindHost = tunnel.bindHost || "127.0.0.1"; + const sourcePort = Number(tunnel.sourcePort); + const endpointPort = Number(tunnel.endpointPort); + + if (!tunnel.sourceHostId) { + return { success: false, error: "Endpoint SSH host is required" }; + } + + if (mode === "remote") { + const localTarget = await checkTcpConnection(bindHost, endpointPort); + if (!localTarget.success) { + return { + success: false, + error: `Local target ${bindHost}:${endpointPort} is not reachable: ${localTarget.error}`, + }; + } + + return testC2SRelay(testTunnel, undefined, undefined); + } + + if (!Number.isInteger(sourcePort) || sourcePort < 1 || sourcePort > 65535) { + return { success: false, error: "Invalid local port" }; + } + + const runtime = c2sTunnelRuntimes.get(getC2STunnelName(tunnel, index)); + if (!runtime) { + const availability = await checkLocalPortAvailable(bindHost, sourcePort); + if (!availability.available) { + return { + success: false, + error: `Local listener ${bindHost}:${sourcePort} is not available: ${availability.error}`, + }; + } + } + + if (mode === "dynamic") { + return testC2SRelay(testTunnel, undefined, undefined); + } + + if (!Number.isInteger(endpointPort) || endpointPort < 1) { + return { success: false, error: "Invalid remote port" }; + } + + return testC2SRelay( + testTunnel, + tunnel.targetHost || "127.0.0.1", + endpointPort, + ); +} + +function handleC2SDynamicConnection(tunnel, socket) { + const tunnelName = tunnel.name || getC2STunnelName(tunnel); + let buffer = Buffer.alloc(0); + let stage = "greeting"; + + const fail = (code = 0x01, message = "SOCKS5 request failed") => { + setC2STunnelError(tunnelName, message); + if (!socket.destroyed) { + socket.write(Buffer.from([0x05, code, 0x00, 0x01, 0, 0, 0, 0, 0, 0])); + socket.destroy(); + } + }; + + const onData = (chunk) => { + buffer = Buffer.concat([buffer, chunk]); + + try { + if (stage === "greeting") { + if (buffer.length < 2) return; + if (buffer[0] !== 0x05) { + fail(0x01, "Invalid SOCKS5 greeting"); + return; + } + const methodsLength = buffer[1]; + if (buffer.length < 2 + methodsLength) return; + socket.write(Buffer.from([0x05, 0x00])); + buffer = buffer.subarray(2 + methodsLength); + stage = "connect"; + } + + if (stage === "connect") { + const target = parseSocks5Target(buffer); + if (!target) return; + + stage = "piping"; + socket.off("data", onData); + const remainder = buffer.subarray(target.bytesRead); + socket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])); + openC2SRelay(tunnel, target.host, target.port, socket, remainder).catch( + (error) => { + logToFile("[c2s] dynamic relay failed:", error.message); + fail(0x05, error.message || "Dynamic relay failed"); + }, + ); + } + } catch (error) { + logToFile("[c2s] SOCKS5 parse failed:", error.message); + fail(0x01, error.message || "SOCKS5 parse failed"); + } + }; + + socket.on("data", onData); + socket.on("error", () => socket.destroy()); +} + +function handleC2SLocalConnection(tunnel, socket) { + const tunnelName = tunnel.name || getC2STunnelName(tunnel); + const targetHost = tunnel.targetHost || "127.0.0.1"; + const targetPort = Number(tunnel.endpointPort); + openC2SRelay(tunnel, targetHost, targetPort, socket).catch((error) => { + logToFile("[c2s] local relay failed:", error.message); + setC2STunnelError(tunnelName, error.message || "Local relay failed"); + socket.destroy(); + }); +} + +function pauseSourceForC2SWebSocket(ws, source) { + if (!source?.pause || !source?.resume) return; + if (ws.bufferedAmount <= C2S_WS_HIGH_WATERMARK) return; + + source.pause(); + const resumeTimer = setInterval(() => { + if ( + ws.readyState !== WebSocket.OPEN || + source.destroyed || + ws.bufferedAmount <= C2S_WS_LOW_WATERMARK + ) { + clearInterval(resumeTimer); + if (ws.readyState === WebSocket.OPEN && !source.destroyed) { + source.resume(); + } + } + }, 25); +} + +function sendC2SRemoteMessage(ws, message, source) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(message), (error) => { + if (error && source?.destroy) { + source.destroy(error); + } + }); + pauseSourceForC2SWebSocket(ws, source); + } +} + +function writeC2SRemoteChunk(target, chunk, ws, closeTarget) { + if (!target || target.destroyed) return; + + if (target.writableLength > C2S_STREAM_WRITE_LIMIT) { + closeTarget(); + return; + } + + const canContinue = target.write(chunk); + if (!canContinue && typeof ws.pause === "function") { + ws.pause(); + target.once("drain", () => { + if (ws.readyState === WebSocket.OPEN && typeof ws.resume === "function") { + ws.resume(); + } + }); + } +} + +async function startC2SRemoteTunnel(tunnel, index = 0) { + const tunnelName = getC2STunnelName(tunnel, index); + const localHost = tunnel.bindHost || "127.0.0.1"; + const localPort = Number(tunnel.endpointPort); + const remotePort = Number(tunnel.sourcePort); + + if (!tunnel.sourceHostId) { + return { success: false, error: "Endpoint SSH host is required" }; + } + if (!Number.isInteger(remotePort) || remotePort < 1 || remotePort > 65535) { + return { success: false, error: "Invalid remote port" }; + } + if (!Number.isInteger(localPort) || localPort < 1 || localPort > 65535) { + return { success: false, error: "Invalid local port" }; + } + + const localTarget = await checkTcpConnection(localHost, localPort); + if (!localTarget.success) { + return { + success: false, + error: `Local target ${localHost}:${localPort} is not reachable: ${localTarget.error}`, + }; + } + + const existing = c2sTunnelRuntimes.get(tunnelName); + if (existing) { + return { success: true, tunnelName }; + } + + for (const runtime of c2sTunnelRuntimes.values()) { + if ( + runtime.mode === "remote" && + runtime.sourceHostId === Number(tunnel.sourceHostId) && + runtime.sourcePort === remotePort + ) { + return { + success: false, + error: `Another client remote tunnel already uses ${remotePort} on this endpoint`, + }; + } + } + + const relayUrl = getC2SRelayUrl(); + const headers = await getC2SRelayHeaders(relayUrl); + const ws = new WebSocket( + relayUrl, + getWebSocketOptions(relayUrl, { headers }), + ); + const sockets = new Map(); + let closed = false; + + const cleanup = () => { + if (closed) return; + closed = true; + for (const socket of sockets.values()) { + socket.destroy(); + } + sockets.clear(); + try { + ws.close(); + } catch { + // expected during shutdown + } + }; + + c2sTunnelRuntimes.set(tunnelName, { + ws, + sockets, + mode: "remote", + sourceHostId: Number(tunnel.sourceHostId), + sourcePort: remotePort, + bindHost: localHost, + status: { connected: false, status: "CONNECTING" }, + close: cleanup, + }); + emitC2STunnelStatuses(); + + return new Promise((resolve) => { + let settled = false; + const settle = (result) => { + if (settled) return; + settled = true; + resolve(result); + }; + + ws.on("open", () => { + logToFile(`[c2s] opening remote tunnel ${tunnelName}`, { + relayUrl, + remotePort, + localHost, + localPort, + }); + ws.send( + JSON.stringify({ + type: "open", + tunnelConfig: { ...tunnel, name: tunnelName, mode: "remote" }, + }), + ); + }); + + ws.on("message", (data, isBinary) => { + if (isBinary) return; + + let message; + try { + message = JSON.parse(data.toString()); + } catch (error) { + setC2STunnelError(tunnelName, error.message || "Invalid relay message"); + cleanup(); + settle({ success: false, error: error.message }); + return; + } + + if (message.type === "ready") { + setC2STunnelStatus(tunnelName, { + connected: true, + status: "CONNECTED", + }); + settle({ success: true, tunnelName }); + return; + } + + if (message.type === "error") { + const error = message.error || "Relay rejected the client tunnel"; + setC2STunnelError(tunnelName, error); + cleanup(); + c2sTunnelRuntimes.delete(tunnelName); + emitC2STunnelStatuses(); + settle({ success: false, error }); + return; + } + + if (message.type === "connection" && message.streamId) { + const socket = net.createConnection( + { host: localHost, port: localPort }, + () => { + logToFile(`[c2s] remote stream ${message.streamId} connected`, { + tunnelName, + localHost, + localPort, + }); + }, + ); + sockets.set(message.streamId, socket); + socket.on("data", (chunk) => { + sendC2SRemoteMessage( + ws, + { + type: "data", + streamId: message.streamId, + data: chunk.toString("base64"), + }, + socket, + ); + }); + socket.on("close", () => { + sockets.delete(message.streamId); + sendC2SRemoteMessage(ws, { + type: "close", + streamId: message.streamId, + }); + }); + socket.on("error", (error) => { + logToFile(`[c2s] remote stream ${message.streamId} failed:`, { + tunnelName, + error: error.message, + }); + sockets.delete(message.streamId); + sendC2SRemoteMessage(ws, { + type: "close", + streamId: message.streamId, + error: error.message, + }); + }); + return; + } + + if (message.type === "data" && message.streamId && message.data) { + const socket = sockets.get(message.streamId); + writeC2SRemoteChunk( + socket, + Buffer.from(message.data, "base64"), + ws, + () => { + if (socket) { + sockets.delete(message.streamId); + socket.destroy(); + } + }, + ); + return; + } + + if (message.type === "close" && message.streamId) { + const socket = sockets.get(message.streamId); + if (socket) { + sockets.delete(message.streamId); + socket.destroy(); + } + } + }); + + ws.on("close", () => { + cleanup(); + c2sTunnelRuntimes.delete(tunnelName); + emitC2STunnelStatuses(); + settle({ success: false, error: "Remote tunnel relay closed" }); + }); + + ws.on("error", (error) => { + setC2STunnelError(tunnelName, error.message || "Relay connection failed"); + cleanup(); + c2sTunnelRuntimes.delete(tunnelName); + emitC2STunnelStatuses(); + settle({ success: false, error: error.message }); + }); + }); +} + +async function startC2STunnel(tunnel, index = 0) { + const mode = tunnel.mode || tunnel.tunnelType || "local"; + const tunnelName = getC2STunnelName(tunnel, index); + const bindHost = tunnel.bindHost || "127.0.0.1"; + const sourcePort = Number(tunnel.sourcePort); + logToFile(`[c2s] starting tunnel ${tunnelName}`, { + mode, + bindHost, + sourcePort, + sourceHostId: tunnel.sourceHostId, + endpointPort: tunnel.endpointPort, + }); + + if (mode === "remote") { + return startC2SRemoteTunnel(tunnel, index); + } + if (!tunnel.sourceHostId) { + return { success: false, error: "Endpoint SSH host is required" }; + } + if (!Number.isInteger(sourcePort) || sourcePort < 1 || sourcePort > 65535) { + return { success: false, error: "Invalid local port" }; + } + + const existing = c2sTunnelRuntimes.get(tunnelName); + if (existing) { + return { success: true, tunnelName }; + } + + for (const runtime of c2sTunnelRuntimes.values()) { + if ( + runtime.mode !== "remote" && + runtime.bindHost === bindHost && + runtime.sourcePort === sourcePort + ) { + return { + success: false, + error: `Another client tunnel already uses ${bindHost}:${sourcePort}`, + }; + } + } + + const availability = await checkLocalPortAvailable(bindHost, sourcePort); + if (!availability.available) { + return { + success: false, + error: availability.error || "Port is already in use", + }; + } + + const sockets = new Set(); + const server = net.createServer((socket) => { + sockets.add(socket); + socket.on("close", () => sockets.delete(socket)); + if (mode === "dynamic") { + handleC2SDynamicConnection({ ...tunnel, name: tunnelName, mode }, socket); + } else { + handleC2SLocalConnection({ ...tunnel, name: tunnelName, mode }, socket); + } + }); + + c2sTunnelRuntimes.set(tunnelName, { + server, + sockets, + bindHost, + sourcePort, + status: { connected: false, status: "CONNECTING" }, + }); + + return new Promise((resolve) => { + server.once("error", (error) => { + c2sTunnelRuntimes.delete(tunnelName); + logToFile(`[c2s] failed to listen for ${tunnelName}:`, error.message); + emitC2STunnelStatuses(); + resolve({ success: false, error: error.message }); + }); + server.listen({ host: bindHost, port: sourcePort }, () => { + logToFile( + `[c2s] listening for ${tunnelName} on ${bindHost}:${sourcePort}`, + ); + setC2STunnelStatus(tunnelName, { + connected: true, + status: "CONNECTED", + }); + resolve({ success: true, tunnelName }); + }); + }); +} + +async function stopC2STunnel(tunnelName) { + const runtime = c2sTunnelRuntimes.get(tunnelName); + if (!runtime) { + return { success: true }; + } + + setC2STunnelStatus(tunnelName, { + connected: false, + status: "DISCONNECTING", + }); + + return new Promise((resolve) => { + if (typeof runtime.close === "function") { + runtime.close(); + c2sTunnelRuntimes.delete(tunnelName); + emitC2STunnelStatuses(); + resolve({ success: true }); + return; + } + + for (const socket of runtime.sockets || []) { + socket.destroy(); + } + runtime.server?.close(() => { + c2sTunnelRuntimes.delete(tunnelName); + emitC2STunnelStatuses(); + resolve({ success: true }); + }); + }); +} + +function stopAllC2STunnels() { + for (const [tunnelName, runtime] of c2sTunnelRuntimes.entries()) { + try { + if (typeof runtime.close === "function") { + runtime.close(); + } else { + for (const socket of runtime.sockets || []) { + socket.destroy(); + } + runtime.server?.close(); + } + } catch (error) { + logToFile(`[c2s] failed to stop tunnel ${tunnelName}:`, error.message); + } + c2sTunnelRuntimes.delete(tunnelName); + } + emitC2STunnelStatuses(); +} + +async function startC2SAutoStartTunnels() { + const configPath = getC2STunnelConfigPath(); + if (!fs.existsSync(configPath)) { + return { success: true, started: 0, errors: [] }; + } + + const config = JSON.parse(fs.readFileSync(configPath, "utf8")); + const tunnels = Array.isArray(config) ? config : []; + const errors = []; + let started = 0; + + for (let index = 0; index < tunnels.length; index += 1) { + const tunnel = tunnels[index]; + if (!tunnel?.autoStart) continue; + const result = await startC2STunnel(tunnel, index); + if (result.success) { + started += 1; + } else { + errors.push(result.error || "Failed to start client tunnel"); + } + } + + return { success: errors.length === 0, started, errors }; +} + +ipcMain.handle("check-local-port-available", async (_event, host, port) => { + const sourcePort = Number(port); + if ( + !host || + !Number.isInteger(sourcePort) || + sourcePort < 1 || + sourcePort > 65535 + ) { + return { available: false, error: "Invalid local bind address or port" }; + } + return checkLocalPortAvailable(host, sourcePort); +}); + +ipcMain.handle("start-c2s-tunnel", async (_event, tunnel, index) => { + try { + return await startC2STunnel(tunnel, Number(index) || 0); + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle("test-c2s-tunnel", async (_event, tunnel, index) => { + try { + return await testC2STunnel(tunnel, Number(index) || 0); + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle("stop-c2s-tunnel", async (_event, tunnelName) => { + try { + return await stopC2STunnel(tunnelName); + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle("get-c2s-tunnel-statuses", () => { + return getAllC2STunnelStatuses(); +}); + +ipcMain.handle("start-c2s-autostart-tunnels", async () => { + try { + return await startC2SAutoStartTunnels(); + } catch (error) { + return { success: false, started: 0, errors: [error.message] }; + } +}); + +ipcMain.handle("get-c2s-tunnel-preset-default-name", () => { + const now = new Date(); + const date = now.toISOString().slice(0, 10); + const platform = + process.platform === "darwin" + ? "macOS" + : process.platform === "win32" + ? "Windows" + : "Linux"; + const release = os.release(); + const computerName = os.hostname(); + return `[${date}] ${computerName} (${platform} ${release})`; +}); + ipcMain.handle("get-setting", (event, key) => { try { const userDataPath = app.getPath("userData"); @@ -663,61 +2213,102 @@ ipcMain.handle("set-setting", (event, key, value) => { } }); -ipcMain.handle("get-iframe-jwt", async () => { - try { - if (!mainWindow) return null; - const frames = mainWindow.webContents.mainFrame.framesInSubtree; - logToFile(`[get-iframe-jwt] scanning ${frames.length} frames`); - for (const frame of frames) { - if (frame === mainWindow.webContents.mainFrame) continue; - try { - const token = await frame.executeJavaScript( - `(function() { - try { - const t = localStorage.getItem('jwt') || sessionStorage.getItem('jwt'); - return t || null; - } catch(e) { return null; } - })()`, - ); - logToFile( - `[get-iframe-jwt] frame url=${frame.url} token found=${!!token} length=${token?.length}`, - ); - if (token && token.length > 20) return token; - } catch (err) { - logToFile(`[get-iframe-jwt] frame exec error:`, err.message); - } - } - return null; - } catch (error) { - logToFile("[get-iframe-jwt] error:", error.message); - return null; - } -}); - -ipcMain.handle("get-session-cookie", async (_event, name) => { +ipcMain.handle("get-session-cookie", async (_event, name, targetUrl) => { try { const ses = mainWindow?.webContents?.session; if (!ses) return null; - const cookies = await ses.cookies.get({ name }); - return cookies.length > 0 ? cookies[0].value : null; + const cookies = await ses.cookies.get({ + name, + ...(targetUrl ? { url: targetUrl } : {}), + }); + const cookie = cookies.find((candidate) => + cookieMatchesUrl(candidate, targetUrl), + ); + return ( + cookie?.value || + getRememberedElectronAuthCookie(name, targetUrl)?.value || + null + ); } catch (error) { console.error("Failed to get session cookie:", error); - return null; + return getRememberedElectronAuthCookie(name, targetUrl)?.value || null; } }); +function cookieMatchesUrl(cookie, targetUrl) { + if (!targetUrl) return true; + + try { + const targetHost = new URL(targetUrl).hostname; + const cookieDomain = (cookie.domain || "").replace(/^\./, ""); + + return ( + cookieDomain === targetHost || + targetHost.endsWith(`.${cookieDomain}`) || + (!cookieDomain && targetHost === "localhost") + ); + } catch { + return true; + } +} + +ipcMain.handle( + "wait-session-cookie", + async (_event, name, targetUrl, previousValue, timeoutMs = 5000) => { + const ses = mainWindow?.webContents?.session; + if (!ses) return { success: false, error: "No Electron session" }; + + const existingCookies = await ses.cookies.get({ + name, + ...(targetUrl ? { url: targetUrl } : {}), + }); + const existingCookie = existingCookies.find((cookie) => + cookieMatchesUrl(cookie, targetUrl), + ); + if (existingCookie?.value && existingCookie.value !== previousValue) { + return { success: true, value: existingCookie.value }; + } + + const rememberedCookie = getRememberedElectronAuthCookie(name, targetUrl); + if (rememberedCookie?.value && rememberedCookie.value !== previousValue) { + return { success: true, value: rememberedCookie.value }; + } + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + ses.cookies.off("changed", onCookieChanged); + resolve({ success: false, error: "Timed out waiting for cookie" }); + }, timeoutMs); + + function onCookieChanged(_event, cookie, _cause, removed) { + if ( + removed || + cookie.name !== name || + !cookie.value || + cookie.value === previousValue || + !cookieMatchesUrl(cookie, targetUrl) + ) { + return; + } + + clearTimeout(timeout); + ses.cookies.off("changed", onCookieChanged); + resolve({ success: true, value: cookie.value }); + } + + ses.cookies.on("changed", onCookieChanged); + }); + }, +); + ipcMain.handle("clear-session-cookies", async () => { try { + clearPersistedElectronAuthCookies(); const ses = mainWindow?.webContents?.session; if (ses) { const cookies = await ses.cookies.get({}); for (const cookie of cookies) { - const scheme = cookie.secure ? "https" : "http"; - const domain = cookie.domain?.startsWith(".") - ? cookie.domain.slice(1) - : cookie.domain || "localhost"; - const url = `${scheme}://${domain}${cookie.path || "/"}`; - await ses.cookies.remove(url, cookie.name); + await ses.cookies.remove(getCookieRemovalUrl(cookie), cookie.name); } } } catch (error) { @@ -725,6 +2316,13 @@ ipcMain.handle("clear-session-cookies", async () => { } }); +ipcMain.handle("clipboard-write-text", (_event, text) => { + clipboard.writeText(typeof text === "string" ? text : String(text ?? "")); + return true; +}); + +ipcMain.handle("clipboard-read-text", () => clipboard.readText()); + ipcMain.handle("test-server-connection", async (event, serverUrl) => { try { const normalizedServerUrl = serverUrl.replace(/\/$/, ""); @@ -906,6 +2504,8 @@ app.whenReady().then(async () => { process.arch, ); createMenu(); + await clearElectronClientCacheIfBuildChanged(); + await clearElectronJwtCookiesAtStartup(); if (!isDev) { const result = await startBackendServer(); @@ -939,6 +2539,7 @@ app.on("before-quit", () => { app.on("will-quit", () => { console.log("App will quit..."); + stopAllC2STunnels(); stopBackendServer(); }); diff --git a/electron/preload.js b/electron/preload.js index c7670426..a19eb5c3 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -1,5 +1,4 @@ const { contextBridge, ipcRenderer } = require("electron"); -const { clipboard } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { getAppVersion: () => ipcRenderer.invoke("get-app-version"), @@ -10,15 +9,46 @@ contextBridge.exposeInMainWorld("electronAPI", { getSetting: (key) => ipcRenderer.invoke("get-setting", key), setSetting: (key, value) => ipcRenderer.invoke("set-setting", key, value), + getC2STunnelConfig: () => ipcRenderer.invoke("get-c2s-tunnel-config"), + saveC2STunnelConfig: (config) => + ipcRenderer.invoke("save-c2s-tunnel-config", config), + checkLocalPortAvailable: (host, port) => + ipcRenderer.invoke("check-local-port-available", host, port), + getC2STunnelPresetDefaultName: () => + ipcRenderer.invoke("get-c2s-tunnel-preset-default-name"), + startC2STunnel: (tunnel, index) => + ipcRenderer.invoke("start-c2s-tunnel", tunnel, index), + testC2STunnel: (tunnel, index) => + ipcRenderer.invoke("test-c2s-tunnel", tunnel, index), + stopC2STunnel: (tunnelName) => + ipcRenderer.invoke("stop-c2s-tunnel", tunnelName), + getC2STunnelStatuses: () => ipcRenderer.invoke("get-c2s-tunnel-statuses"), + onC2STunnelStatuses: (callback) => { + const listener = (_event, statuses) => callback(statuses); + ipcRenderer.on("c2s-tunnel-statuses", listener); + return () => ipcRenderer.removeListener("c2s-tunnel-statuses", listener); + }, + startC2SAutoStartTunnels: () => + ipcRenderer.invoke("start-c2s-autostart-tunnels"), clearSessionCookies: () => ipcRenderer.invoke("clear-session-cookies"), + getSessionCookie: (name, targetUrl) => + ipcRenderer.invoke("get-session-cookie", name, targetUrl), + waitForSessionCookie: (name, targetUrl, previousValue, timeoutMs) => + ipcRenderer.invoke( + "wait-session-cookie", + name, + targetUrl, + previousValue, + timeoutMs, + ), invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args), }); contextBridge.exposeInMainWorld("electronClipboard", { - writeText: (text) => clipboard.writeText(text), - readText: () => clipboard.readText(), + writeText: (text) => ipcRenderer.invoke("clipboard-write-text", text), + readText: () => ipcRenderer.invoke("clipboard-read-text"), }); window.IS_ELECTRON = true; diff --git a/eslint.config.js b/eslint.config.js index a7b8196c..a2cce5ee 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,7 @@ import js from "@eslint/js"; import globals from "globals"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; +import unusedImports from "eslint-plugin-unused-imports"; import tseslint from "typescript-eslint"; import { globalIgnores } from "eslint/config"; @@ -12,19 +13,36 @@ export default tseslint.config([ extends: [ js.configs.recommended, tseslint.configs.recommended, - reactHooks.configs["recommended-latest"], reactRefresh.configs.vite, ], + plugins: { + "react-hooks": reactHooks, + "unused-imports": unusedImports, + }, languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, rules: { - "@typescript-eslint/no-unused-vars": "warn", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-unused-expressions": "warn", "no-empty": "warn", "no-control-regex": "off", + "no-useless-assignment": "off", + "preserve-caught-error": "off", + "react-hooks/exhaustive-deps": "warn", + "react-hooks/rules-of-hooks": "error", "react-refresh/only-export-components": "warn", }, }, diff --git a/package-lock.json b/package-lock.json index aefa65e8..ae152a35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,91 +1,91 @@ { "name": "termix", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "termix", - "version": "2.1.0", + "version": "2.2.0", + "hasInstallScript": true, "dependencies": { - "axios": "^1.10.0", - "bcryptjs": "^3.0.2", - "better-sqlite3": "^12.2.0", - "body-parser": "^1.20.2", - "chalk": "^4.1.2", + "axios": "^1.15.2", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.9.0", + "body-parser": "^2.2.2", + "chalk": "^5.6.2", "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dotenv": "^17.2.0", - "drizzle-orm": "^0.44.3", - "express": "^5.1.0", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "express": "^5.2.1", "guacamole-lite": "^1.2.0", "https-proxy-agent": "^7.0.6", - "jose": "^5.2.3", + "jose": "^6.2.2", "js-yaml": "^4.1.1", - "jsonwebtoken": "^9.0.2", + "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", - "multer": "^2.0.2", - "nanoid": "^5.1.5", - "node-fetch": "^3.3.2", + "motion": "^12.38.0", + "multer": "^2.1.1", + "nanoid": "^5.1.9", "qrcode": "^1.5.4", + "react-is": "^19.2.5", "socks": "^2.8.7", "speakeasy": "^2.0.0", - "ssh2": "^1.16.0", - "ws": "^8.18.3" + "ssh2": "^1.17.0", + "undici": "^7.0.0", + "ws": "^8.20.0" }, "devDependencies": { - "@codemirror/autocomplete": "^6.18.7", - "@codemirror/commands": "^6.3.3", - "@codemirror/search": "^6.5.11", - "@codemirror/theme-one-dark": "^6.1.2", - "@codemirror/view": "^6.23.1", - "@commitlint/cli": "^20.1.0", - "@commitlint/config-conventional": "^20.0.0", - "@electron/notarize": "^2.5.0", - "@electron/rebuild": "^3.7.2", - "@eslint/js": "^9.34.0", - "@hookform/resolvers": "^5.1.1", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", + "@codemirror/search": "^6.7.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.41.1", + "@commitlint/cli": "^20.5.0", + "@commitlint/config-conventional": "^20.5.0", + "@deadendjs/swagger-jsdoc": "^8.1.2", + "@electron/notarize": "^3.1.1", + "@electron/rebuild": "^4.0.4", + "@eslint/js": "^9.0.0", + "@hookform/resolvers": "^5.2.2", "@monaco-editor/react": "^4.7.0", - "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-progress": "^1.1.7", - "@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-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@tailwindcss/vite": "^4.1.14", - "@types/bcryptjs": "^2.4.6", + "@tailwindcss/vite": "^4.2.4", "@types/better-sqlite3": "^7.6.13", - "@types/cookie-parser": "^1.4.9", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", - "@types/cytoscape": "^3.21.9", - "@types/express": "^5.0.3", + "@types/express": "^5.0.6", "@types/guacamole-common-js": "^1.5.5", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/jszip": "^3.4.0", - "@types/multer": "^2.0.0", - "@types/node": "^24.3.0", - "@types/qrcode": "^1.5.5", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@types/react-grid-layout": "^1.3.6", + "@types/multer": "^2.1.0", + "@types/node": "^24.12.2", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/speakeasy": "^2.0.10", "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", - "@uiw/codemirror-extensions-langs": "^4.24.1", - "@uiw/codemirror-theme-github": "^4.25.4", - "@uiw/react-codemirror": "^4.24.1", - "@vitejs/plugin-react": "^4.3.4", + "@uiw/codemirror-extensions-langs": "^4.25.9", + "@uiw/codemirror-theme-github": "^4.25.9", + "@uiw/react-codemirror": "^4.25.9", + "@vitejs/plugin-react": "^6.0.1", "@xterm/addon-clipboard": "^0.2.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", @@ -95,59 +95,67 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "concurrently": "^9.2.1", - "cytoscape": "^3.33.1", - "electron": "^38.0.0", - "electron-builder": "^26.0.12", - "eslint": "^9.34.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", + "cytoscape": "^3.33.2", + "electron": "^41.3.0", + "electron-builder": "^26.8.1", + "eslint": "^9.0.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^17.5.0", "guacamole-common-js": "^1.5.0", "husky": "^9.1.7", - "i18next": "^25.4.2", - "i18next-browser-languagedetector": "^8.2.0", - "lint-staged": "^16.2.3", - "lucide-react": "^0.525.0", + "i18next": "^26.0.8", + "i18next-browser-languagedetector": "^8.2.1", + "lint-staged": "^16.4.0", + "lucide-react": "^1.11.0", "next-themes": "^0.4.6", - "prettier": "3.6.2", - "react": "^19.1.0", + "prettier": "3.8.3", + "react": "^19.2.5", "react-cytoscapejs": "^2.0.0", - "react-dom": "^19.1.0", - "react-grid-layout": "^2.2.2", - "react-h5-audio-player": "^3.10.1", - "react-hook-form": "^7.60.0", - "react-i18next": "^15.7.3", - "react-icons": "^5.5.0", + "react-dom": "^19.2.5", + "react-grid-layout": "^2.2.3", + "react-h5-audio-player": "^3.10.2", + "react-hook-form": "^7.73.1", + "react-i18next": "^17.0.4", + "react-icons": "^5.6.0", "react-markdown": "^10.1.0", - "react-pdf": "^10.1.0", + "react-pdf": "^10.4.1", "react-photo-view": "^1.2.7", - "react-resizable-panels": "^3.0.3", - "react-simple-keyboard": "^3.8.120", - "react-syntax-highlighter": "^15.6.6", + "react-resizable-panels": "^4.10.0", + "react-simple-keyboard": "^3.8.196", + "react-syntax-highlighter": "^16.1.1", "react-xtermjs": "^1.0.10", - "recharts": "^3.2.1", + "recharts": "^3.8.1", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", - "swagger-jsdoc": "^6.2.8", - "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.14", - "typescript": "~5.9.2", - "typescript-eslint": "^8.40.0", - "vite": "^7.1.5", - "zod": "^4.0.5" + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.4", + "typescript": "~6.0.3", + "typescript-eslint": "^8.59.0", + "vite": "^8.0.10", + "zod": "^4.3.6" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=11" } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", - "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", "dev": true, "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.6", - "call-me-maybe": "^1.0.1", + "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" } }, "node_modules/@apidevtools/openapi-schemas": { @@ -168,31 +176,31 @@ "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz", + "integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==", "dev": true, "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "^9.0.6", - "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/json-schema-ref-parser": "14.0.1", + "@apidevtools/openapi-schemas": "^2.1.0", "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", - "call-me-maybe": "^1.0.1", - "z-schema": "^5.0.1" + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" }, "peerDependencies": { "openapi-types": ">=7" } }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -201,9 +209,9 @@ } }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -211,21 +219,21 @@ } }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "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", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -252,14 +260,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -269,13 +277,13 @@ } }, "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==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -306,29 +314,29 @@ } }, "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==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "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==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -337,16 +345,6 @@ "@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", @@ -378,27 +376,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "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==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -407,42 +405,10 @@ "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", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "dev": true, "license": "MIT", "engines": { @@ -450,33 +416,33 @@ } }, "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==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -484,9 +450,9 @@ } }, "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==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -498,9 +464,9 @@ } }, "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==", + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", "dev": true, "license": "MIT", "dependencies": { @@ -511,33 +477,18 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", - "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, - "node_modules/@codemirror/lang-angular": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", - "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@codemirror/lang-html": "^6.0.0", - "@codemirror/lang-javascript": "^6.1.2", - "@codemirror/language": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.3" - } - }, "node_modules/@codemirror/lang-cpp": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", @@ -607,9 +558,9 @@ } }, "node_modules/@codemirror/lang-javascript": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", - "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", "dev": true, "license": "MIT", "dependencies": { @@ -623,14 +574,17 @@ } }, "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==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.1.tgz", + "integrity": "sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==", "dev": true, "license": "MIT", "dependencies": { + "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.0" @@ -662,9 +616,9 @@ } }, "node_modules/@codemirror/lang-liquid": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz", - "integrity": "sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.2.tgz", + "integrity": "sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==", "dev": true, "license": "MIT", "dependencies": { @@ -806,9 +760,9 @@ } }, "node_modules/@codemirror/lang-yaml": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", - "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", + "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", "dev": true, "license": "MIT", "dependencies": { @@ -822,52 +776,20 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", - "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", + "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, - "node_modules/@codemirror/language-data": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz", - "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@codemirror/lang-angular": "^0.1.0", - "@codemirror/lang-cpp": "^6.0.0", - "@codemirror/lang-css": "^6.0.0", - "@codemirror/lang-go": "^6.0.0", - "@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", - "@codemirror/lang-markdown": "^6.0.0", - "@codemirror/lang-php": "^6.0.0", - "@codemirror/lang-python": "^6.0.0", - "@codemirror/lang-rust": "^6.0.0", - "@codemirror/lang-sass": "^6.0.0", - "@codemirror/lang-sql": "^6.0.0", - "@codemirror/lang-vue": "^0.1.1", - "@codemirror/lang-wast": "^6.0.0", - "@codemirror/lang-xml": "^6.0.0", - "@codemirror/lang-yaml": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/legacy-modes": "^6.4.0" - } - }, "node_modules/@codemirror/legacy-modes": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", @@ -879,9 +801,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.1.tgz", - "integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==", + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", "dev": true, "license": "MIT", "dependencies": { @@ -891,21 +813,21 @@ } }, "node_modules/@codemirror/search": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", - "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -926,30 +848,30 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.6", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", - "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "version": "6.41.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.1.tgz", + "integrity": "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg==", "dev": true, "license": "MIT", "dependencies": { - "@codemirror/state": "^6.5.0", + "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", + "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", "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", + "@commitlint/format": "^20.5.0", + "@commitlint/lint": "^20.5.0", + "@commitlint/load": "^20.5.0", + "@commitlint/read": "^20.5.0", + "@commitlint/types": "^20.5.0", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, @@ -961,27 +883,27 @@ } }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", + "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.0.0", - "conventional-changelog-conventionalcommits": "^7.0.2" + "@commitlint/types": "^20.5.0", + "conventional-changelog-conventionalcommits": "^9.2.0" }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", + "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.0.0", + "@commitlint/types": "^20.5.0", "ajv": "^8.11.0" }, "engines": { @@ -989,13 +911,13 @@ } }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", + "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.0.0", + "@commitlint/types": "^20.5.0", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", @@ -1017,40 +939,27 @@ } }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", + "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.0.0", - "chalk": "^5.3.0" + "@commitlint/types": "^20.5.0", + "picocolors": "^1.1.1" }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", + "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.0.0", + "@commitlint/types": "^20.5.0", "semver": "^7.6.0" }, "engines": { @@ -1058,60 +967,46 @@ } }, "node_modules/@commitlint/lint": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.0.0.tgz", - "integrity": "sha512-kWrX8SfWk4+4nCexfLaQT3f3EcNjJwJBsSZ5rMBw6JCd6OzXufFHgel2Curos4LKIxwec9WSvs2YUD87rXlxNQ==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz", + "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==", "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" + "@commitlint/is-ignored": "^20.5.0", + "@commitlint/parse": "^20.5.0", + "@commitlint/rules": "^20.5.0", + "@commitlint/types": "^20.5.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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", + "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^20.0.0", + "@commitlint/config-validator": "^20.5.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", + "@commitlint/resolve-extends": "^20.5.0", + "@commitlint/types": "^20.5.0", + "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "lodash.uniq": "^4.5.0" + "is-plain-obj": "^4.1.0", + "lodash.mergewith": "^4.6.2", + "picocolors": "^1.1.1" }, "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==", + "version": "20.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz", + "integrity": "sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==", "dev": true, "license": "MIT", "engines": { @@ -1119,30 +1014,30 @@ } }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-20.5.0.tgz", + "integrity": "sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.0.0", - "conventional-changelog-angular": "^7.0.0", - "conventional-commits-parser": "^5.0.0" + "@commitlint/types": "^20.5.0", + "conventional-changelog-angular": "^8.2.0", + "conventional-commits-parser": "^6.3.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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-20.5.0.tgz", + "integrity": "sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/top-level": "^20.0.0", - "@commitlint/types": "^20.0.0", - "git-raw-commits": "^4.0.0", + "@commitlint/top-level": "^20.4.3", + "@commitlint/types": "^20.5.0", + "git-raw-commits": "^5.0.0", "minimist": "^1.2.8", "tinyexec": "^1.0.0" }, @@ -1151,14 +1046,14 @@ } }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", + "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/config-validator": "^20.0.0", - "@commitlint/types": "^20.0.0", + "@commitlint/config-validator": "^20.5.0", + "@commitlint/types": "^20.5.0", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", @@ -1169,16 +1064,16 @@ } }, "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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz", + "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^20.0.0", - "@commitlint/message": "^20.0.0", + "@commitlint/ensure": "^20.5.0", + "@commitlint/message": "^20.4.3", "@commitlint/to-lines": "^20.0.0", - "@commitlint/types": "^20.0.0" + "@commitlint/types": "^20.5.0" }, "engines": { "node": ">=v18" @@ -1195,43 +1090,87 @@ } }, "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==", + "version": "20.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-20.4.3.tgz", + "integrity": "sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==", "dev": true, "license": "MIT", "dependencies": { - "find-up": "^7.0.0" + "escalade": "^3.2.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==", + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-20.5.0.tgz", + "integrity": "sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==", "dev": true, "license": "MIT", "dependencies": { - "@types/conventional-commits-parser": "^5.0.0", - "chalk": "^5.3.0" + "conventional-commits-parser": "^6.3.0", + "picocolors": "^1.1.1" }, "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==", + "node_modules/@conventional-changelog/git-client": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", + "integrity": "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "dependencies": { + "@simple-libs/child-process-utils": "^1.0.0", + "@simple-libs/stream-utils": "^1.2.0", + "semver": "^7.5.2" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.4.0" + }, + "peerDependenciesMeta": { + "conventional-commits-filter": { + "optional": true + }, + "conventional-commits-parser": { + "optional": true + } + } + }, + "node_modules/@deadendjs/swagger-jsdoc": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@deadendjs/swagger-jsdoc/-/swagger-jsdoc-8.1.2.tgz", + "integrity": "sha512-SbwXKaif1eicRLzK10RbmMjHd0GUbmi/izVlQaJJhpIAy3tfphbfgk6r+maWkqJeMSosmllIH91DVXLyFlavgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "^12.1.0", + "commander": "^14.0.0", + "doctrine": "3.0.0", + "glob": "^12.0.0", + "yaml": "2.0.0-4" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@deadendjs/swagger-jsdoc/node_modules/yaml": { + "version": "2.0.0-4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-4.tgz", + "integrity": "sha512-MoQoNhTFI400tkaeod+X0Vety1KD2L9dUa6pa1CVcyfcATjC/iDxoMLvqZ6U3D8c5KzxBrU2HnJH+PfaXOqI7w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" } }, "node_modules/@develar/schema-utils": { @@ -1253,9 +1192,9 @@ } }, "node_modules/@develar/schema-utils/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1287,21 +1226,49 @@ "license": "MIT" }, "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.2.0.tgz", + "integrity": "sha512-npW1NW5yy8EB9XY/vEw9sUdgmq0sJEhmSBb6bqyFOAw1CSkrhvAvO6QWlW8CdIMo8VN1lkdF345l/MeW0LrY0Q==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" + "commander": "^13.1.0", + "glob": "^13.0.2", + "minimatch": "^10.0.1" }, "bin": { - "asar": "bin/asar.js" + "asar": "bin/asar.mjs" }, "engines": { - "node": ">=10.12.0" + "node": ">=22.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron/asar/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@electron/fuses": { @@ -1319,155 +1286,98 @@ "electron-fuses": "dist/bin.js" } }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "node_modules/@electron/fuses/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/@electron/get/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@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", - "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", - "integrity": "sha512-4MSBTT8y07YUDqf69/vSh80Hh791epYqGtWHO3zSKhYFwQg+gx9wi1PqbqP6YqC4WMsNxZ5l9oDmnWdK5pfCKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^8.1.0", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.2.1", - "nopt": "^6.0.0", - "proc-log": "^2.0.1", - "semver": "^7.3.5", - "tar": "^6.2.1", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/@electron/node-gyp/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/node-gyp/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@electron/node-gyp/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "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": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "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": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "node_modules/@electron/fuses/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/get": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.0.0.tgz", + "integrity": "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", - "fs-extra": "^9.0.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@electron/get/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@electron/notarize": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz", + "integrity": "sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", "promise-retry": "^2.0.1" }, "engines": { - "node": ">= 10.0.0" + "node": ">= 22.12.0" } }, "node_modules/@electron/osx-sign": { @@ -1492,21 +1402,6 @@ "node": ">=12.0.0" } }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", @@ -1521,47 +1416,24 @@ } }, "node_modules/@electron/rebuild": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz", - "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", "dev": true, "license": "MIT", "dependencies": { - "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "node-abi": "^3.45.0", - "node-api-version": "^0.2.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" }, "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/@electron/rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" + "node": ">=22.12.0" } }, "node_modules/@electron/universal": { @@ -1583,6 +1455,13 @@ "node": ">=16.4" } }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@electron/universal/node_modules/brace-expansion": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", @@ -1624,491 +1503,44 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "optional": true, - "peer": true, "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2125,13 +1557,13 @@ } }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2148,20 +1580,51 @@ } }, "node_modules/@eslint/config-array": { - "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==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", @@ -2189,20 +1652,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -2213,9 +1676,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2229,6 +1692,24 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -2249,10 +1730,36 @@ "dev": true, "license": "MIT" }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.0.tgz", - "integrity": "sha512-BIhe0sW91JGPiaF1mOuPy5v8NflqfjIcDNpC+LbW9f609WVRX1rArrhi6Z2ymvrAry9jw+5POTj4t2t62o8Bmw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -2287,34 +1794,34 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -2322,16 +1829,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "dev": true, "license": "MIT" }, @@ -2349,29 +1849,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2424,77 +1938,13 @@ "license": "MIT" }, "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, "node_modules/@isaacs/fs-minipass": { @@ -2510,16 +1960,6 @@ "node": ">=18.0.0" } }, - "node_modules/@isaacs/fs-minipass/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2570,24 +2010,17 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "dev": true, - "license": "MIT" - }, "node_modules/@lezer/common": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", - "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", "dev": true, "license": "MIT" }, "node_modules/@lezer/cpp": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.3.tgz", - "integrity": "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.5.tgz", + "integrity": "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw==", "dev": true, "license": "MIT", "dependencies": { @@ -2597,9 +2030,9 @@ } }, "node_modules/@lezer/css": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", - "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz", + "integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==", "dev": true, "license": "MIT", "dependencies": { @@ -2631,9 +2064,9 @@ } }, "node_modules/@lezer/html": { - "version": "1.3.12", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz", - "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", "dev": true, "license": "MIT", "dependencies": { @@ -2679,9 +2112,9 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", - "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", "dev": true, "license": "MIT", "dependencies": { @@ -2689,13 +2122,13 @@ } }, "node_modules/@lezer/markdown": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.5.1.tgz", - "integrity": "sha512-F3ZFnIfNAOy/jPSk6Q0e3bs7e9grfK/n5zerkKoc5COH6Guy3Zb0vrJwXzdck79K16goBhYBRAvhf+ksqe0cMg==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", "dev": true, "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0", + "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, @@ -2760,9 +2193,9 @@ } }, "node_modules/@lezer/yaml": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", - "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", "dev": true, "license": "MIT", "dependencies": { @@ -2810,6 +2243,22 @@ "node": ">= 10.0.0" } }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", @@ -2818,9 +2267,9 @@ "license": "MIT" }, "node_modules/@monaco-editor/loader": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.6.1.tgz", - "integrity": "sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", "dev": true, "license": "MIT", "dependencies": { @@ -2843,9 +2292,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.81", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz", - "integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.99.tgz", + "integrity": "sha512-zN4eQlK3eBf7aJBcTHZilpBH3tDekBzPMIWC8r0s94Ecl73XfOyFi4w7yKFMRVUT0lvNQjtOL8YSrwqQj6mZFg==", "dev": true, "license": "MIT", "optional": true, @@ -2855,23 +2304,28 @@ "engines": { "node": ">= 10" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, "optionalDependencies": { - "@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" + "@napi-rs/canvas-android-arm64": "0.1.99", + "@napi-rs/canvas-darwin-arm64": "0.1.99", + "@napi-rs/canvas-darwin-x64": "0.1.99", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.99", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.99", + "@napi-rs/canvas-linux-arm64-musl": "0.1.99", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.99", + "@napi-rs/canvas-linux-x64-gnu": "0.1.99", + "@napi-rs/canvas-linux-x64-musl": "0.1.99", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.99", + "@napi-rs/canvas-win32-x64-msvc": "0.1.99" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.99.tgz", + "integrity": "sha512-9OCRt8VVxA17m32NWZKyNC2qamdaS/SC5CEOIQwFngRq0DIeVm4PDal+6Ljnhqm2whZiC63DNuKZ4xSp2nbj9w==", "cpu": [ "arm64" ], @@ -2883,12 +2337,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.99.tgz", + "integrity": "sha512-lupMDMy1+H38dhyCcLirOKKVUyzzlxi7j7rGPLI3vViMHOoPjcXO1b10ivy+ad+q6MiwHfoLjKTCoLke5ySOBg==", "cpu": [ "arm64" ], @@ -2900,12 +2358,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.99.tgz", + "integrity": "sha512-fdz02t4w8n6Ii/rYhWig6STb/zcTmCC/6YZTGmjoDeidDwn9Wf0ukQVynhCPEs29vqUc66wHZKsuIgMs9tycCg==", "cpu": [ "x64" ], @@ -2917,12 +2379,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.99.tgz", + "integrity": "sha512-w4FwVwlNo00ezeRhfY62IVIyt6G3u8wodkPtiqWc52BUHx+VDBUM2vkS3ogfANaLI7hnf3s6WK4LyZVUjBg1lA==", "cpu": [ "arm" ], @@ -2934,12 +2400,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.99.tgz", + "integrity": "sha512-8JvHeexKQ8c7g0q7YJ29NVQwnf1ePghP9ys9ZN0R0qzyqJQ9Uw6N9qnDINArlm3IYHexB7LjzArIfhQiqSDGvQ==", "cpu": [ "arm64" ], @@ -2951,12 +2421,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.99.tgz", + "integrity": "sha512-Z+6nyLdJXWzLPVxi4H6g9TJop4DwN3KSgHWto5JCbZV5/uKoVqcSynPs0tGlUHOoWI8S8tEvJspz51GQkvr07w==", "cpu": [ "arm64" ], @@ -2968,12 +2442,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.99.tgz", + "integrity": "sha512-jAnfOUv4IO1l8Levk5t85oVtEBOXLa07KnIUgWo1CDlPxiqpxS3uBfiE38Lvj/CQgHaNF6Nxk/SaemwLgsVJgw==", "cpu": [ "riscv64" ], @@ -2985,12 +2463,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.99.tgz", + "integrity": "sha512-mIkXw3fGmbYyFjSmfWEvty4jN+rwEOmv0+Dy9bRvvTzLYWCgm3RMgUEQVfAKFw96nIRFnyNZiK83KNQaVVFjng==", "cpu": [ "x64" ], @@ -3002,12 +2484,16 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.99.tgz", + "integrity": "sha512-f3Uz2P0RgrtBHISxZqr6yiYXJlTDyCVBumDacxo+4AmSg7z0HiqYZKGWC/gszq3fbPhyQUya1W2AEteKxT9Y6A==", "cpu": [ "x64" ], @@ -3019,12 +2505,37 @@ ], "engines": { "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.99.tgz", + "integrity": "sha512-XE6KUkfqRsCNejcoRMiMr3RaUeObxNf6y7dut3hrq2rn7PzfRTZgrjF1F/B2C7FcdgqY/vSHWpQeMuNz1vTNHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "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==", + "version": "0.1.99", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.99.tgz", + "integrity": "sha512-plMYGVbc/vmmPF9MtmHbwNk1rL1Aj53vQZt+Gnv1oZn6gmd9jEHHJ0n9Nd2nxa5sKH7TS5IjkCDM6289O0d6PQ==", "cpu": [ "x64" ], @@ -3036,123 +2547,39 @@ ], "engines": { "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" }, - "engines": { - "node": ">= 8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@radix-ui/number": { @@ -3605,13 +3032,37 @@ } }, "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.1.4" + }, + "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-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -3872,14 +3323,54 @@ } }, "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "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-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -4024,13 +3515,37 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.1.4" + }, + "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-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", @@ -4391,15 +3906,15 @@ "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==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", + "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" @@ -4417,6 +3932,17 @@ } } }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "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", @@ -4466,31 +3992,10 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "cpu": [ "arm64" ], @@ -4499,12 +4004,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "cpu": [ "arm64" ], @@ -4513,12 +4021,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "cpu": [ "x64" ], @@ -4527,26 +4038,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "cpu": [ "x64" ], @@ -4555,12 +4055,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "cpu": [ "arm" ], @@ -4569,26 +4072,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "cpu": [ "arm64" ], @@ -4597,12 +4089,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "cpu": [ "arm64" ], @@ -4611,40 +4106,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "cpu": [ "ppc64" ], @@ -4653,54 +4123,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "cpu": [ "s390x" ], @@ -4709,12 +4140,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "cpu": [ "x64" ], @@ -4723,12 +4157,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "cpu": [ "x64" ], @@ -4737,26 +4174,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "cpu": [ "arm64" ], @@ -4765,12 +4191,34 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "cpu": [ "arm64" ], @@ -4779,26 +4227,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -4807,39 +4244,51 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@simple-libs/child-process-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", + "integrity": "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "url": "https://ko-fi.com/dangreen" } }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, @@ -4850,63 +4299,50 @@ "dev": true, "license": "MIT" }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@tailwindcss/node": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", - "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.19", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.16" + "tailwindcss": "4.2.4" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", - "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@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" + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", "cpu": [ "arm64" ], @@ -4917,13 +4353,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", - "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", "cpu": [ "arm64" ], @@ -4934,13 +4370,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", "cpu": [ "x64" ], @@ -4951,13 +4387,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", "cpu": [ "x64" ], @@ -4968,13 +4404,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", "cpu": [ "arm" ], @@ -4985,13 +4421,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", "cpu": [ "arm64" ], @@ -5002,13 +4438,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", "cpu": [ "arm64" ], @@ -5019,13 +4455,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", "cpu": [ "x64" ], @@ -5036,13 +4472,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", "cpu": [ "x64" ], @@ -5053,13 +4489,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -5075,81 +4511,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.0.7", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.5.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", - "@tybys/wasm-util": "^0.10.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", - "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", "cpu": [ "arm64" ], @@ -5160,13 +4536,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "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==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", "cpu": [ "x64" ], @@ -5177,91 +4553,40 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz", - "integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.16", - "@tailwindcss/oxide": "4.1.16", - "tailwindcss": "4.1.16" + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "tailwindcss": "4.2.4" }, "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" + "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@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==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "tslib": "^2.4.0" } }, - "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", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -5278,19 +4603,6 @@ "@types/node": "*" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5301,16 +4613,6 @@ "@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.10", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", @@ -5331,13 +4633,6 @@ "@types/node": "*" } }, - "node_modules/@types/cytoscape": { - "version": "3.21.9", - "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.9.tgz", - "integrity": "sha512-JyrG4tllI6jvuISPjHK9j2Xv/LTbnLekLke5otGStjFluIyA9JjgnvgZrSBsp8cEDpiTjwgZUZwpPv8TSBcoLw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -5387,9 +4682,9 @@ } }, "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==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "dev": true, "license": "MIT", "dependencies": { @@ -5411,9 +4706,9 @@ "license": "MIT" }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "dev": true, "license": "MIT", "dependencies": { @@ -5438,21 +4733,21 @@ } }, "node_modules/@types/express": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", - "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^1" + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { - "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==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", "dependencies": { @@ -5489,13 +4784,6 @@ "@types/unist": "*" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -5528,26 +4816,6 @@ "@types/node": "*" } }, - "node_modules/@types/jszip": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz", - "integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "jszip": "*" - } - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -5558,13 +4826,6 @@ "@types/unist": "*" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -5573,9 +4834,9 @@ "license": "MIT" }, "node_modules/@types/multer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", - "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5583,10 +4844,10 @@ } }, "node_modules/@types/node": { - "version": "24.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", - "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", - "devOptional": true, + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -5604,6 +4865,13 @@ "xmlbuilder": ">=11.0.1" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/qrcode": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", @@ -5615,9 +4883,9 @@ } }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", "dev": true, "license": "MIT" }, @@ -5629,45 +4897,25 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, - "node_modules/@types/react-grid-layout": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.6.tgz", - "integrity": "sha512-Cw7+sb3yyjtmxwwJiXtEXcu5h4cgs+sCGkHwHXsFmPyV30bf14LeD/fa2LwQovuD2HWxCcjIdNhDlcYGj95qGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", @@ -5679,25 +4927,13 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -5738,15 +4974,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5791,21 +5018,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", - "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.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", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5815,9 +5041,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.2", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -5831,17 +5057,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", - "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5851,20 +5077,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "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==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5874,18 +5100,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "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==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5896,9 +5122,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "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==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", "dev": true, "license": "MIT", "engines": { @@ -5909,21 +5135,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", - "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5933,14 +5159,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", "dev": true, "license": "MIT", "engines": { @@ -5952,22 +5178,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "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==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", "dev": true, "license": "MIT", "dependencies": { - "@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", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5977,46 +5202,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6026,19 +5225,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "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==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6048,10 +5247,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.2.tgz", - "integrity": "sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==", + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz", + "integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==", "dev": true, "license": "MIT", "dependencies": { @@ -6077,14 +5289,34 @@ } }, "node_modules/@uiw/codemirror-extensions-langs": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.2.tgz", - "integrity": "sha512-fWS9fP52QJAFgXbsUl6vKMBqQ2PIT8z5TvX8BKBqPz/I+ayh6Fk+HzoeUtslUGPTu+UTPgB5m0qt3rTIDXWvng==", + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.9.tgz", + "integrity": "sha512-POcA4K5AQwjcHAebWTCRld4bUCMTGFdV8AFal1Pj6ymeKRqWc58cduryfjmAl8yfdTU0Kb3fF2bO08BeDzX7FQ==", "dev": true, "license": "MIT", "dependencies": { + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@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", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.0", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", "@codemirror/language": "^6.0.0", - "@codemirror/language-data": "^6.5.1", + "@codemirror/legacy-modes": "^6.0.0", "@replit/codemirror-lang-nix": "^6.0.1", "@replit/codemirror-lang-solidity": "^6.0.1", "@replit/codemirror-lang-svelte": "^6.0.0", @@ -6094,27 +5326,26 @@ "url": "https://jaywcjlove.github.io/#/sponsor" }, "peerDependencies": { - "@codemirror/language": ">=6.0.0", - "@codemirror/language-data": ">=6.0.0" + "@codemirror/language": ">=6.0.0" } }, "node_modules/@uiw/codemirror-theme-github": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.4.tgz", - "integrity": "sha512-M5zRT2vIpNsuKN0Lz+DwLnmhHW8Eddp1M9zC0hm3V+bvffmaSn/pUDey1eqGIv5xNNmjhqvDAz0a90xLYCzvSw==", + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.9.tgz", + "integrity": "sha512-AGpTamNiySKNzq3Jc7QjpwgQRVaHUaBtmOKiUDghYSfEGjsc5uW4NUW70sSU3BnkGv+lCTUnF3175KM24BWZbw==", "dev": true, "license": "MIT", "dependencies": { - "@uiw/codemirror-themes": "4.25.4" + "@uiw/codemirror-themes": "4.25.9" }, "funding": { "url": "https://jaywcjlove.github.io/#/sponsor" } }, "node_modules/@uiw/codemirror-themes": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.4.tgz", - "integrity": "sha512-2SLktItgcZC4p0+PfFusEbAHwbuAWe3bOOntCevVgHtrWGtGZX3IPv2k8IKZMgOXtAHyGKpJvT9/nspPn/uCQg==", + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.9.tgz", + "integrity": "sha512-DAHKb/L9ELwjY4nCf/MP/mIllHOn4GQe7RR4x8AMJuNeh9nGRRoo1uPxrxMmUL/bKqe6kDmDbIZ2AlhlqyIJuw==", "dev": true, "license": "MIT", "dependencies": { @@ -6132,9 +5363,9 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.2.tgz", - "integrity": "sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==", + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz", + "integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==", "dev": true, "license": "MIT", "dependencies": { @@ -6142,7 +5373,7 @@ "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.25.2", + "@uiw/codemirror-extensions-basic-setup": "4.25.9", "codemirror": "^6.0.0" }, "funding": { @@ -6166,24 +5397,29 @@ "license": "ISC" }, "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==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", "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", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@rolldown/pluginutils": "1.0.0-rc.7" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } } }, "node_modules/@xmldom/xmldom": { @@ -6251,11 +5487,14 @@ "license": "MIT" }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", "dev": true, - "license": "ISC" + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } }, "node_modules/accepts": { "version": "2.0.0", @@ -6271,9 +5510,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -6302,37 +5541,10 @@ "node": ">= 14" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -6346,10 +5558,25 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "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==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -6449,132 +5676,35 @@ "electron-builder-squirrel-windows": "26.8.1" } }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "node_modules/app-builder-lib/node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" }, "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" + "node": ">= 10.0.0" } }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/app-builder-lib/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": "^4.0.0", - "universalify": "^0.1.0" + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/rebuild": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", - "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "got": "^11.7.0", - "graceful-fs": "^4.2.11", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^11.2.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^7.5.6", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/app-builder-lib/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/app-builder-lib/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/app-builder-lib/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": ">=10" } }, "node_modules/app-builder-lib/node_modules/ci-info": { @@ -6606,21 +5736,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/app-builder-lib/node_modules/isexe": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", @@ -6631,75 +5746,6 @@ "node": ">=18" } }, - "node_modules/app-builder-lib/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/app-builder-lib/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/app-builder-lib/node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/app-builder-lib/node_modules/node-abi": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", - "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/app-builder-lib/node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/app-builder-lib/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -6716,16 +5762,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/app-builder-lib/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -6823,9 +5859,9 @@ } }, "node_modules/axios": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", - "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -6833,6 +5869,20 @@ "proxy-from-env": "^2.1.0" } }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -6845,11 +5895,105 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", + "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } }, "node_modules/base32.js": { "version": "0.0.1", @@ -6861,6 +6005,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -6878,13 +6023,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==", + "version": "2.10.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcrypt-pbkdf": { @@ -6897,18 +6045,18 @@ } }, "node_modules/bcryptjs": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", - "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", "license": "BSD-3-Clause", "bin": { "bcrypt": "bin/bcrypt" } }, "node_modules/better-sqlite3": { - "version": "12.4.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz", - "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -6916,7 +6064,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x" + "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, "node_modules/bindings": { @@ -6928,127 +6076,47 @@ "file-uri-to-path": "1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { - "node": ">= 0.8", - "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/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" + "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -7066,11 +6134,11 @@ ], "license": "MIT", "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" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -7083,6 +6151,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -7098,6 +6167,7 @@ } ], "license": "MIT", + "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -7126,9 +6196,9 @@ "license": "MIT" }, "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", "optional": true, "engines": { "node": ">=10.0.0" @@ -7173,19 +6243,34 @@ "node": ">=12.0.0" } }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/builder-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/builder-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/busboy": { @@ -7208,119 +6293,6 @@ "node": ">= 0.8" } }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -7377,9 +6349,9 @@ } }, "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==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "dev": true, "funding": [ { @@ -7409,16 +6381,12 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -7469,13 +6437,13 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/chromium-pickle-js": { @@ -7514,16 +6482,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -7540,19 +6498,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-truncate": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", @@ -7627,29 +6572,6 @@ "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", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -7754,13 +6676,13 @@ } }, "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=20" } }, "node_modules/compare-func": { @@ -7845,32 +6767,47 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/concurrently/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -7883,48 +6820,46 @@ } }, "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==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", + "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==", "dev": true, "license": "ISC", "dependencies": { "compare-func": "^2.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "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==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", + "integrity": "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==", "dev": true, "license": "ISC", "dependencies": { "compare-func": "^2.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "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==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", + "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==", "dev": true, "license": "MIT", "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" + "@simple-libs/stream-utils": "^1.2.0", + "meow": "^13.0.0" }, "bin": { - "conventional-commits-parser": "cli.mjs" + "conventional-commits-parser": "dist/cli/index.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/convert-source-map": { @@ -7969,9 +6904,9 @@ "license": "MIT" }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -7979,12 +6914,16 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8009,13 +6948,13 @@ } }, "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==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", + "integrity": "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==", "dev": true, "license": "MIT", "dependencies": { - "jiti": "^2.6.1" + "jiti": "2.6.1" }, "engines": { "node": ">=v18" @@ -8058,15 +6997,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8083,16 +7013,16 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", + "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", "dev": true, "license": "MIT", "engines": { @@ -8133,9 +7063,9 @@ } }, "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==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "dev": true, "license": "ISC", "engines": { @@ -8231,28 +7161,6 @@ "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/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -8287,9 +7195,9 @@ "license": "MIT" }, "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8315,18 +7223,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -8343,67 +7239,6 @@ "dev": true, "license": "MIT" }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8432,16 +7267,6 @@ "node": ">=6" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -8451,14 +7276,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -8497,6 +7314,37 @@ "p-limit": "^3.1.0 " } }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/dmg-builder": { "version": "26.8.1", "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", @@ -8514,21 +7362,6 @@ "dmg-license": "^1.0.11" } }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/dmg-builder/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -8570,9 +7403,9 @@ } }, "node_modules/dmg-license/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "optional": true, @@ -8608,17 +7441,6 @@ "node": ">=6.0.0" } }, - "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "dev": true, - "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -8633,9 +7455,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -8674,9 +7496,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.44.7", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz", - "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==", + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -8812,13 +7634,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -8851,15 +7666,15 @@ } }, "node_modules/electron": { - "version": "38.8.6", - "resolved": "https://registry.npmjs.org/electron/-/electron-38.8.6.tgz", - "integrity": "sha512-lyBhcVi9QYAZL6FO6r5twAWAjWnYomo3iVDvrb5SJZlq928BGemHOKG0tPIq41NOLaCu9f3XdEEjMkjQPjprRg==", + "version": "41.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-41.3.0.tgz", + "integrity": "sha512-2Q5aeocmFdeheZGDUTrAvSR3t+n0c3d104AJWWEnt7syJU0tE4VdibMYaPtQ47QuXSoUf0/xSsfUUvu/uSXIfg==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", + "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -8895,32 +7710,34 @@ "node": ">=14.0.0" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.8.1", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", - "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.8.1", - "builder-util": "26.8.1", - "electron-winstaller": "5.4.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/electron-builder/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-builder/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/electron-publish": { @@ -8940,105 +7757,43 @@ "mime": "^2.5.2" } }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/electron-publish/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/electron-publish/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "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==", + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", "dev": true, "license": "ISC" }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/electron-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", - "peer": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/electron/node_modules/@types/node": { - "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": { - "undici-types": "~6.21.0" - } - }, - "node_modules/electron/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -9054,31 +7809,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -9089,14 +7819,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -9188,9 +7918,9 @@ } }, "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==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.0.tgz", + "integrity": "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA==", "dev": true, "license": "MIT", "workspaces": [ @@ -9198,56 +7928,6 @@ "benchmarks" ] }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -9278,25 +7958,25 @@ } }, "node_modules/eslint": { - "version": "9.39.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.0.tgz", - "integrity": "sha512-iy2GE3MHrYTL5lrCtMZ0X1KLEKKUjmK0kzwcnefhR66txcEmXZD2YWgR5GNdcEwkNx3a0siYkSvl0vIC+Svjmg==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@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", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -9315,7 +7995,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -9338,26 +8018,49 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-refresh": { - "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==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=8.40" + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } } }, "node_modules/eslint-scope": { @@ -9377,23 +8080,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9407,21 +8097,52 @@ "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==", + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/json-schema-traverse": { @@ -9431,44 +8152,28 @@ "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==", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "has-flag": "^4.0.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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", - "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" } @@ -9491,10 +8196,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -9558,12 +8276,21 @@ } }, "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -9581,18 +8308,19 @@ "license": "Apache-2.0" }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -9622,30 +8350,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/express/node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -9655,76 +8359,6 @@ "node": ">=6.6.0" } }, - "node_modules/express/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/express/node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9778,35 +8412,11 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -9839,16 +8449,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fault": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", @@ -9873,27 +8473,22 @@ "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-entry-cache": { @@ -9925,6 +8520,13 @@ "minimatch": "^5.0.1" } }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/filelist/node_modules/brace-expansion": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", @@ -9948,23 +8550,10 @@ "node": ">=10" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -9975,22 +8564,25 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/find-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "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": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10100,18 +8692,6 @@ "node": ">=0.4.x" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10121,6 +8701,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -10130,48 +8737,21 @@ "node": ">= 0.8" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "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==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -10216,9 +8796,9 @@ } }, "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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true, "license": "MIT", "engines": { @@ -10292,21 +8872,20 @@ } }, "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==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-5.0.1.tgz", + "integrity": "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==", "dev": true, "license": "MIT", "dependencies": { - "dargs": "^8.0.0", - "meow": "^12.0.1", - "split2": "^4.0.0" + "@conventional-changelog/git-client": "^2.6.0", + "meow": "^13.0.0" }, "bin": { - "git-raw-commits": "cli.mjs" + "git-raw-commits": "src/cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/github-from-package": { @@ -10316,22 +8895,24 @@ "license": "MIT" }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz", + "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10350,25 +8931,6 @@ "node": ">=10.13.0" } }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -10386,9 +8948,9 @@ } }, "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", "dev": true, "license": "MIT", "engines": { @@ -10398,24 +8960,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -10428,32 +8972,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -10461,13 +8979,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/guacamole-common-js": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/guacamole-common-js/-/guacamole-common-js-1.5.0.tgz", @@ -10492,25 +9003,12 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -10539,9 +9037,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -10551,11 +9049,14 @@ } }, "node_modules/hast-util-parse-selector": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "dev": true, "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -10604,74 +9105,38 @@ } }, "node_modules/hastscript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "dev": true, "license": "MIT", "dependencies": { - "@types/hast": "^2.0.0", - "comma-separated-tokens": "^1.0.0", - "hast-util-parse-selector": "^2.0.0", - "property-information": "^5.0.0", - "space-separated-tokens": "^1.0.0" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/hastscript/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/hastscript/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, - "node_modules/hastscript/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hastscript/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "hermes-estree": "0.25.1" } }, "node_modules/highlight.js": { @@ -10745,36 +9210,24 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-proxy-agent": { @@ -10791,20 +9244,6 @@ "node": ">= 14" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -10818,16 +9257,6 @@ "node": ">= 14" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -10845,30 +9274,27 @@ } }, "node_modules/i18next": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz", - "integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==", + "version": "26.0.8", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.8.tgz", + "integrity": "sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==", "dev": true, "funding": [ { "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" + "url": "https://www.locize.com/i18next" }, { "type": "individual", "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" } ], "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6" - }, "peerDependencies": { - "typescript": "^5" + "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "typescript": { @@ -10877,9 +9303,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", - "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", "dev": true, "license": "MIT", "dependencies": { @@ -10905,21 +9331,26 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -10934,7 +9365,8 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "optional": true }, "node_modules/ignore": { "version": "5.3.2", @@ -11011,35 +9443,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, - "license": "ISC" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -11057,9 +9460,9 @@ } }, "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "dev": true, "license": "MIT" }, @@ -11074,9 +9477,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -11178,33 +9581,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "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", @@ -11234,32 +9610,6 @@ "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", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -11287,19 +9637,19 @@ "license": "ISC" }, "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jake": { @@ -11331,9 +9681,9 @@ } }, "node_modules/jose": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", - "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -11406,14 +9756,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -11428,9 +9770,9 @@ } }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -11440,40 +9782,13 @@ "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", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -11502,9 +9817,9 @@ } }, "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -11513,12 +9828,12 @@ } }, "node_modules/jws": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", - "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^1.4.2", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -11563,9 +9878,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -11579,23 +9894,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "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" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -11614,9 +9929,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -11635,9 +9950,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -11656,9 +9971,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -11677,9 +9992,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -11698,9 +10013,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -11719,9 +10034,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -11740,9 +10055,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -11761,9 +10076,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -11782,9 +10097,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -11803,9 +10118,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "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==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -11831,19 +10146,18 @@ "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==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.1", + "commander": "^14.0.3", "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "nano-spawn": "^2.0.0", - "pidtree": "^0.6.0", + "picomatch": "^4.0.3", "string-argv": "^0.3.2", - "yaml": "^2.8.1" + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -11855,16 +10169,6 @@ "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", @@ -11897,14 +10201,14 @@ } }, "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==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { "node": ">=20" @@ -11930,31 +10234,31 @@ } }, "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==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "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==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { "node": ">=20" @@ -11964,16 +10268,16 @@ } }, "node_modules/locate-path": { - "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==", + "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": "^6.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11993,14 +10297,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -12013,14 +10309,6 @@ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT" }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -12086,13 +10374,6 @@ "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", @@ -12100,23 +10381,6 @@ "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", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -12207,16 +10471,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -12243,9 +10497,9 @@ } }, "node_modules/lucide-react": { - "version": "0.525.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.525.0.tgz", - "integrity": "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz", + "integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==", "dev": true, "license": "ISC", "peerDependencies": { @@ -12282,96 +10536,6 @@ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" } }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dev": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -12383,34 +10547,6 @@ "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==", - "dev": true, - "license": "MIT", - "peer": true, - "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", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12451,9 +10587,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -12719,22 +10855,22 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16.10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -12770,16 +10906,6 @@ } } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -13371,20 +11497,6 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", @@ -13408,25 +11520,19 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-function": { @@ -13443,26 +11549,31 @@ } }, "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -13475,127 +11586,26 @@ } }, "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass/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", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "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", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" + "node": ">= 18" } }, "node_modules/mkdirp-classic": { @@ -13604,18 +11614,47 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/monaco-editor": { - "version": "0.55.1", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", - "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", - "dev": true, + "node_modules/motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", + "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", "license": "MIT", - "peer": true, "dependencies": { - "dompurify": "3.2.7", - "marked": "14.0.0" + "framer-motion": "^12.38.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13641,30 +11680,60 @@ "url": "https://opencollective.com/express" } }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/nan": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", "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", - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz", + "integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==", "funding": [ { "type": "github", @@ -13680,9 +11749,9 @@ } }, "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", "license": "MIT" }, "node_modules/natural-compare": { @@ -13713,15 +11782,16 @@ } }, "node_modules/node-abi": { - "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==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.3.5" + "semver": "^7.6.3" }, "engines": { - "node": ">=10" + "node": ">=22.12.0" } }, "node_modules/node-addon-api": { @@ -13742,436 +11812,88 @@ "semver": "^7.3.5" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", "semver": "^7.3.5", - "tar": "^7.4.3", + "tar": "^7.5.4", "tinyglobby": "^0.2.12", - "which": "^5.0.0" + "undici": "^6.25.0", + "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/node-gyp/node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/node-gyp/node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/node-gyp/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/node-gyp/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-gyp/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/node-gyp/node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/node-gyp/node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-gyp/node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/tar": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", - "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-gyp/node_modules/unique-filename": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=20" } }, - "node_modules/node-gyp/node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "node_modules/node-gyp/node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, + "license": "MIT", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=18.17" } }, "node_modules/node-gyp/node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^3.1.1" + "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": "^20.17.0 || >=22.9.0" } }, "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==", + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", "dev": true, "license": "MIT" }, "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "^1.0.0" + "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/object-assign": { @@ -14195,17 +11917,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -14243,14 +11954,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-types": { - "version": "12.1.3", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", - "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14269,113 +11972,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/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", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -14393,58 +11989,13 @@ } }, "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "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", "dependencies": { - "p-limit": "^4.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/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" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" + "p-limit": "^3.0.2" }, "engines": { "node": ">=10" @@ -14544,23 +12095,12 @@ } }, "node_modules/path-exists": { - "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, + "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": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/path-key": { @@ -14574,37 +12114,30 @@ } }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" } }, "node_modules/path-to-regexp": { @@ -14660,31 +12193,18 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "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/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -14710,9 +12230,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -14757,58 +12277,41 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "name": "@mmomtchev/prebuild-install", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mmomtchev/prebuild-install/-/prebuild-install-1.0.2.tgz", + "integrity": "sha512-0Vje0eg5XQa8Ta1jtQiWqnT1kS/P7OAAvFJ4VZG5EXT2gJmpmPA5SrsDrh++BqNjIxYy8O6aF4wZjJc/k4JQ6w==", "license": "MIT", "dependencies": { - "detect-libc": "^2.0.0", + "detect-libc": "^2.0.3", "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", + "github-from-package": "^0.0.0", + "minimist": "^1.2.8", "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", + "napi-build-utils": "^1.0.2", + "node-abi": "^3.63.0", "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", + "rc": "^1.2.8", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.6", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" }, + "engines": { + "node": ">=18" + } + }, + "node_modules/prebuild-install/node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, "engines": { "node": ">=10" } @@ -14824,9 +12327,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -14850,13 +12353,13 @@ } }, "node_modules/proc-log": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", - "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", "dev": true, "license": "ISC", "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/process-nextick-args": { @@ -14875,13 +12378,6 @@ "node": ">=0.4.0" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -14968,9 +12464,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -15076,15 +12572,6 @@ "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", @@ -15153,9 +12640,9 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -15167,40 +12654,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -15211,38 +12664,18 @@ } }, "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", + "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.10" } }, "node_modules/rc": { @@ -15266,19 +12699,10 @@ "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", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "dev": true, "license": "MIT", "engines": { @@ -15300,16 +12724,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.5" } }, "node_modules/react-draggable": { @@ -15328,9 +12752,9 @@ } }, "node_modules/react-grid-layout": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", - "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.3.tgz", + "integrity": "sha512-OAEJHBxmfuxQfVtZwRzmsokijGlBgzYIJ7MUlLk/VSa43SaGzu15w5D0P2RDrfX5EvP9POMbL6bFrai/huDzbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15338,7 +12762,7 @@ "fast-equals": "^4.0.3", "prop-types": "^15.8.1", "react-draggable": "^4.4.6", - "react-resizable": "^3.0.5", + "react-resizable": "^3.1.3", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { @@ -15347,24 +12771,27 @@ } }, "node_modules/react-h5-audio-player": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/react-h5-audio-player/-/react-h5-audio-player-3.10.1.tgz", - "integrity": "sha512-r6fSj9WXR6af1kxH5qQ/tawwDK4KrMfayiVCUettLYGX/KZ3BH8OGuaZP4O5KD0AxwsKAXtBv4kVQCWFzaIrUA==", + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/react-h5-audio-player/-/react-h5-audio-player-3.10.2.tgz", + "integrity": "sha512-2RlP1eF9V0KPZgBzaYM/hwIE1KxKV+E84gOkpaf0+3X1r56USO+V7AuCVa2FSTeWkQnrB03Osay99hR9FbSefQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.2", "@iconify/react": "^5" }, + "engines": { + "node": ">=18.0.0" + }, "peerDependencies": { "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-hook-form": { - "version": "7.66.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", - "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", + "version": "7.73.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.73.1.tgz", + "integrity": "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==", "dev": true, "license": "MIT", "engines": { @@ -15379,19 +12806,20 @@ } }, "node_modules/react-i18next": { - "version": "15.7.4", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", - "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.4.tgz", + "integrity": "sha512-hQipmK4EF0y6RO6tt6WuqnmWpWYEXmQUUzecmMBuNsIgYd3smXcG4GtYPWhvgxn0pqMOItKlEO8H24HCs5hc3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "html-parse-stringify": "^3.0.1" + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "i18next": ">= 23.4.0", + "i18next": ">= 26.0.1", "react": ">= 16.8.0", - "typescript": "^5" + "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "react-dom": { @@ -15406,9 +12834,9 @@ } }, "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -15416,12 +12844,10 @@ } }, "node_modules/react-is": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", - "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", - "dev": true, - "license": "MIT", - "peer": true + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT" }, "node_modules/react-markdown": { "version": "10.1.0", @@ -15452,9 +12878,9 @@ } }, "node_modules/react-pdf": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.2.0.tgz", - "integrity": "sha512-zk0DIL31oCh8cuQycM0SJKfwh4Onz0/Nwi6wTOjgtEjWGUY6eM+/vuzvOP3j70qtEULn7m1JtaeGzud1w5fY2Q==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.4.1.tgz", + "integrity": "sha512-kS/35staVCBqS29verTQJQZXw7RfsRCPO3fdJoW1KXylcv7A9dw6DZ3vJXC2w+bIBgLw5FN4pOFvKSQtkQhPfA==", "dev": true, "license": "MIT", "dependencies": { @@ -15516,20 +12942,10 @@ } } }, - "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", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -15591,20 +13007,20 @@ } }, "node_modules/react-resizable-panels": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz", - "integrity": "sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.10.0.tgz", + "integrity": "sha512-frjewRQt7TCv/vCH1pJfjZ7RxAhr5pKuqVQtVgzFq/vherxBFOWyC3xMbryx5Ti2wylViGUFc93Etg4rB3E0UA==", "dev": true, "license": "MIT", "peerDependencies": { - "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" } }, "node_modules/react-simple-keyboard": { - "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==", + "version": "3.8.196", + "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.196.tgz", + "integrity": "sha512-rvBGzgmNAvzqqwE6V1xVuwaAjwdDfzANA600EjW5hGIN4EQS1Zr7SXl+6Vn9++zdigUr468wbMhF4oGo+xpNKg==", "dev": true, "license": "MIT", "peerDependencies": { @@ -15636,18 +13052,21 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.6.6", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", - "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.3.1", + "@babel/runtime": "^7.28.4", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.30.0", - "refractor": "^3.6.0" + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" }, "peerDependencies": { "react": ">= 0.14.0" @@ -15698,13 +13117,16 @@ "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==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "dev": true, "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "@reduxjs/toolkit": "1.x.x || 2.x.x", + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", @@ -15743,131 +13165,22 @@ } }, "node_modules/refractor": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", "dev": true, "license": "MIT", "dependencies": { - "hastscript": "^6.0.0", - "parse-entities": "^2.0.0", - "prismjs": "~1.27.0" + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/refractor/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/refractor/node_modules/prismjs": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -15995,13 +13308,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -16012,19 +13318,6 @@ "node": ">=8" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -16052,17 +13345,6 @@ "node": ">= 4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -16070,87 +13352,47 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" } }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -16167,30 +13409,6 @@ "node": ">= 18" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -16255,9 +13473,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -16266,57 +13484,36 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" - } - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -16326,6 +13523,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/set-blocking": { @@ -16402,13 +13603,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -16565,34 +13766,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -16658,24 +13831,6 @@ "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/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", @@ -16693,19 +13848,6 @@ "nan": "^2.23.0" } }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", @@ -16740,6 +13882,17 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -16779,45 +13932,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/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", @@ -16855,13 +13969,13 @@ } }, "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==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -16870,41 +13984,13 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-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", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/style-mod": { @@ -16915,23 +14001,23 @@ "license": "MIT" }, "node_modules/style-to-js": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.18.tgz", - "integrity": "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", "dev": true, "license": "MIT", "dependencies": { - "style-to-object": "1.0.11" + "style-to-object": "1.0.14" } }, "node_modules/style-to-object": { - "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==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", "dev": true, "license": "MIT", "dependencies": { - "inline-style-parser": "0.2.4" + "inline-style-parser": "0.2.7" } }, "node_modules/sumchecker": { @@ -16948,97 +14034,25 @@ } }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/swagger-jsdoc": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", - "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "6.2.0", - "doctrine": "3.0.0", - "glob": "7.1.6", - "lodash.mergewith": "^4.6.2", - "swagger-parser": "^10.0.3", - "yaml": "2.0.0-1" - }, - "bin": { - "swagger-jsdoc": "bin/swagger-jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/swagger-jsdoc/node_modules/commander": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", - "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-jsdoc/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/swagger-jsdoc/node_modules/yaml": { - "version": "2.0.0-1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", - "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/swagger-parser": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", - "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/swagger-parser": "10.0.3" - }, - "engines": { - "node": ">=10" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/tailwind-merge": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", - "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "dev": true, "license": "MIT", "funding": { @@ -17047,16 +14061,16 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", - "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", "dev": true, "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -17068,101 +14082,65 @@ } }, "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==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "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" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", "license": "MIT", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "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" + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, - "license": "ISC" - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=6.0.0" + "node": ">=18" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" } }, "node_modules/temp-file": { @@ -17176,70 +14154,15 @@ "fs-extra": "^10.0.0" } }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" + "b4a": "^1.6.4" } }, - "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" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/temp/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/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/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -17268,21 +14191,24 @@ "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -17291,37 +14217,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -17342,19 +14237,6 @@ "tmp": "^0.2.0" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -17407,9 +14289,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -17423,7 +14305,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tunnel-agent": { @@ -17457,49 +14338,15 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { "node": ">= 0.6" @@ -17512,9 +14359,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -17526,16 +14373,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", - "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17545,29 +14392,25 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" } }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/unicorn-magic": { - "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" - } + "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", @@ -17589,32 +14432,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -17658,9 +14475,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", "dev": true, "license": "MIT", "dependencies": { @@ -17708,9 +14525,9 @@ } }, "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==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -17816,16 +14633,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/validator": { - "version": "13.15.26", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", - "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -17913,18 +14720,17 @@ } }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" @@ -17940,9 +14746,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -17955,15 +14762,18 @@ "@types/node": { "optional": true }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, "less": { "optional": true }, - "lightningcss": { - "optional": true - }, "sass": { "optional": true }, @@ -17987,37 +14797,6 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -18045,25 +14824,6 @@ "loose-envify": "^1.0.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -18114,48 +14874,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/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", @@ -18201,9 +14919,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -18231,16 +14949,6 @@ "node": ">=8.0" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -18327,48 +15035,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/z-schema": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", - "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "validator": "^13.7.0" - }, - "bin": { - "z-schema": "bin/z-schema" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "commander": "^9.4.1" - } - }, - "node_modules/z-schema/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 1b132d23..d098a454 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,20 @@ { "name": "termix", "private": true, - "version": "2.1.0", + "version": "2.2.0", "description": "A web-based server management platform with SSH terminal, tunneling, and file editing capabilities", "author": "Karmaa", "main": "electron/main.cjs", "type": "module", + "engines": { + "node": ">=22.12.0", + "npm": ">=11" + }, "scripts": { - "clean": "npx prettier . --write", "format": "prettier --write .", "format:check": "prettier --check .", + "postinstall": "node scripts/patch-app-builder-lib.cjs", + "prebuild": "node scripts/write-electron-build-info.cjs", "lint": "eslint .", "lint:fix": "eslint --fix .", "type-check": "tsc --noEmit", @@ -22,93 +27,94 @@ "generate:openapi": "tsc -p tsconfig.node.json && node ./dist/backend/backend/swagger.js", "preview": "vite preview", "electron:dev": "concurrently \"npm run dev\" \"powershell -c \\\"Start-Sleep -Seconds 5\\\" && electron .\"", + "electron:patch-builder": "node scripts/patch-app-builder-lib.cjs", "electron:rebuild": "electron-rebuild -f -w better-sqlite3", - "build:win-portable": "npm run build && npm run electron:rebuild && electron-builder --win --dir", - "build:win-installer": "npm run build && npm run electron:rebuild && electron-builder --win --publish=never", - "build:linux-portable": "npm run build && npm run electron:rebuild && electron-builder --linux --dir", - "build:linux-appimage": "npm run build && npm run electron:rebuild && electron-builder --linux AppImage", - "build:linux-targz": "npm run build && npm run electron:rebuild && electron-builder --linux tar.gz", - "build:mac": "npm run build && npm run electron:rebuild && electron-builder --mac --universal" + "build:win-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --dir", + "build:win-installer": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --win --publish=never", + "build:linux-portable": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux --dir", + "build:linux-appimage": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux AppImage", + "build:linux-targz": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --linux tar.gz", + "build:mac": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac --universal", + "build:mac-dev": "npm run build && npm run electron:rebuild && npm run electron:patch-builder && electron-builder --mac dir --publish=never" }, "dependencies": { - "axios": "^1.10.0", - "bcryptjs": "^3.0.2", - "better-sqlite3": "^12.2.0", - "body-parser": "^1.20.2", - "chalk": "^4.1.2", + "axios": "^1.15.2", + "bcryptjs": "^3.0.3", + "better-sqlite3": "^12.9.0", + "body-parser": "^2.2.2", + "chalk": "^5.6.2", "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dotenv": "^17.2.0", - "drizzle-orm": "^0.44.3", - "express": "^5.1.0", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "drizzle-orm": "^0.45.2", + "express": "^5.2.1", "guacamole-lite": "^1.2.0", "https-proxy-agent": "^7.0.6", + "jose": "^6.2.2", "js-yaml": "^4.1.1", - "jose": "^5.2.3", - "jsonwebtoken": "^9.0.2", + "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", - "multer": "^2.0.2", - "nanoid": "^5.1.5", - "node-fetch": "^3.3.2", + "motion": "^12.38.0", + "multer": "^2.1.1", + "nanoid": "^5.1.9", "qrcode": "^1.5.4", + "react-is": "^19.2.5", "socks": "^2.8.7", "speakeasy": "^2.0.0", - "ssh2": "^1.16.0", - "ws": "^8.18.3" + "ssh2": "^1.17.0", + "undici": "^7.0.0", + "ws": "^8.20.0" }, "devDependencies": { - "@codemirror/autocomplete": "^6.18.7", - "@codemirror/commands": "^6.3.3", - "@codemirror/search": "^6.5.11", - "@codemirror/theme-one-dark": "^6.1.2", - "@codemirror/view": "^6.23.1", - "@commitlint/cli": "^20.1.0", - "@commitlint/config-conventional": "^20.0.0", - "@electron/notarize": "^2.5.0", - "@electron/rebuild": "^3.7.2", - "@eslint/js": "^9.34.0", - "@hookform/resolvers": "^5.1.1", + "@codemirror/autocomplete": "^6.20.1", + "@codemirror/commands": "^6.10.3", + "@codemirror/search": "^6.7.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.41.1", + "@commitlint/cli": "^20.5.0", + "@commitlint/config-conventional": "^20.5.0", + "@deadendjs/swagger-jsdoc": "^8.1.2", + "@electron/notarize": "^3.1.1", + "@electron/rebuild": "^4.0.4", + "@eslint/js": "^9.0.0", + "@hookform/resolvers": "^5.2.2", "@monaco-editor/react": "^4.7.0", - "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-progress": "^1.1.7", - "@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-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", - "@radix-ui/react-switch": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@tailwindcss/vite": "^4.1.14", - "@types/bcryptjs": "^2.4.6", + "@tailwindcss/vite": "^4.2.4", "@types/better-sqlite3": "^7.6.13", - "@types/cookie-parser": "^1.4.9", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", - "@types/cytoscape": "^3.21.9", - "@types/express": "^5.0.3", + "@types/express": "^5.0.6", "@types/guacamole-common-js": "^1.5.5", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.10", - "@types/jszip": "^3.4.0", - "@types/multer": "^2.0.0", - "@types/node": "^24.3.0", - "@types/qrcode": "^1.5.5", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@types/react-grid-layout": "^1.3.6", + "@types/multer": "^2.1.0", + "@types/node": "^24.12.2", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/speakeasy": "^2.0.10", "@types/ssh2": "^1.15.5", "@types/ws": "^8.18.1", - "@uiw/codemirror-extensions-langs": "^4.24.1", - "@uiw/codemirror-theme-github": "^4.25.4", - "@uiw/react-codemirror": "^4.24.1", - "@vitejs/plugin-react": "^4.3.4", + "@uiw/codemirror-extensions-langs": "^4.25.9", + "@uiw/codemirror-theme-github": "^4.25.9", + "@uiw/react-codemirror": "^4.25.9", + "@vitejs/plugin-react": "^6.0.1", "@xterm/addon-clipboard": "^0.2.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-unicode11": "^0.8.0", @@ -118,53 +124,65 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "concurrently": "^9.2.1", - "cytoscape": "^3.33.1", - "electron": "^38.0.0", - "electron-builder": "^26.0.12", - "eslint": "^9.34.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^16.3.0", + "cytoscape": "^3.33.2", + "electron": "^41.3.0", + "electron-builder": "^26.8.1", + "eslint": "^9.0.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-unused-imports": "^4.4.1", + "globals": "^17.5.0", "guacamole-common-js": "^1.5.0", "husky": "^9.1.7", - "i18next": "^25.4.2", - "i18next-browser-languagedetector": "^8.2.0", - "lint-staged": "^16.2.3", - "lucide-react": "^0.525.0", + "i18next": "^26.0.8", + "i18next-browser-languagedetector": "^8.2.1", + "lint-staged": "^16.4.0", + "lucide-react": "^1.11.0", "next-themes": "^0.4.6", - "prettier": "3.6.2", - "react": "^19.1.0", + "prettier": "3.8.3", + "react": "^19.2.5", "react-cytoscapejs": "^2.0.0", - "react-dom": "^19.1.0", - "react-grid-layout": "^2.2.2", - "react-h5-audio-player": "^3.10.1", - "react-hook-form": "^7.60.0", - "react-i18next": "^15.7.3", - "react-icons": "^5.5.0", + "react-dom": "^19.2.5", + "react-grid-layout": "^2.2.3", + "react-h5-audio-player": "^3.10.2", + "react-hook-form": "^7.73.1", + "react-i18next": "^17.0.4", + "react-icons": "^5.6.0", "react-markdown": "^10.1.0", - "react-pdf": "^10.1.0", + "react-pdf": "^10.4.1", "react-photo-view": "^1.2.7", - "react-resizable-panels": "^3.0.3", - "react-simple-keyboard": "^3.8.120", - "react-syntax-highlighter": "^15.6.6", + "react-resizable-panels": "^4.10.0", + "react-simple-keyboard": "^3.8.196", + "react-syntax-highlighter": "^16.1.1", "react-xtermjs": "^1.0.10", - "recharts": "^3.2.1", + "recharts": "^3.8.1", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", - "swagger-jsdoc": "^6.2.8", - "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.14", - "typescript": "~5.9.2", - "typescript-eslint": "^8.40.0", - "vite": "^7.1.5", - "zod": "^4.0.5" + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.4", + "typescript": "~6.0.3", + "typescript-eslint": "^8.59.0", + "vite": "^8.0.10", + "zod": "^4.3.6" }, "lint-staged": { - "*.{js,jsx,ts,tsx}": [ + "*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{js,jsx}": [ "prettier --write" ], "*.{json,css,md}": [ "prettier --write" ] + }, + "overrides": { + "@electron/asar": "^4.2.0", + "@electron/get": "^5.0.0", + "dompurify": "^3.4.1", + "eslint-visitor-keys": "^4.2.1", + "prebuild-install": "npm:@mmomtchev/prebuild-install@1.0.2", + "rimraf": "file:vendor/rimraf-compat" } } diff --git a/public/sw.js b/public/sw.js index a15adc8d..a3fbfcb6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,8 +1,5 @@ -const CACHE_NAME = "termix-v1"; +const CACHE_NAME = "termix-static-v2"; const STATIC_ASSETS = [ - "/", - "/index.html", - "/manifest.json", "/favicon.ico", "/icons/48x48.png", "/icons/128x128.png", @@ -66,18 +63,11 @@ self.addEventListener("fetch", (event) => { } if (request.mode === "navigate") { - event.respondWith( - fetch(request).catch(() => { - return caches.match("/index.html"); - }), - ); + event.respondWith(fetch(request)); return; } - const isStaticAsset = STATIC_ASSETS.some((asset) => { - if (asset === "/") return url.pathname === "/"; - return url.pathname === asset || url.pathname.startsWith("/assets/"); - }); + const isStaticAsset = STATIC_ASSETS.some((asset) => url.pathname === asset); if (!isStaticAsset) { return; diff --git a/readme/README-AR.md b/readme/README-AR.md index 205818eb..e1df56c2 100644 --- a/readme/README-AR.md +++ b/readme/README-AR.md @@ -1,20 +1,7 @@ # إحصائيات المستودع

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-إذا كنت ترغب في ذلك، يمكنك دعم المشروع هنا!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # نظرة عامة

@@ -50,7 +34,7 @@ Termix هي منصة مفتوحة المصدر ومجانية للأبد وذا - **الوصول إلى طرفية SSH** - طرفية كاملة الميزات مع دعم تقسيم الشاشة (حتى 4 لوحات) مع نظام علامات تبويب شبيه بالمتصفح. يتضمن دعم تخصيص الطرفية بما في ذلك سمات الطرفية الشائعة والخطوط والمكونات الأخرى. - **الوصول إلى سطح المكتب البعيد** - دعم RDP و VNC و Telnet عبر المتصفح مع تخصيص كامل وتقسيم الشاشة. -- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH مع إعادة الاتصال التلقائي ومراقبة الحالة ودعم اتصالات -l أو -r. +- **إدارة أنفاق SSH** - إنشاء وإدارة أنفاق SSH بين الخوادم مع إعادة الاتصال التلقائي ومراقبة الحالة وإعادة التوجيه المحلي أو البعيد أو SOCKS الديناميكي. يتم تخزين إعدادات نفق العميل-المكتبي إلى السيرفر محلياً لكل تثبيت مكتبي؛ يمكن حفظ لقطات C2S الاختيارية على الخادم وإعادة تسميتها وتحميلها أو حذفها لنقل تكوين النفق المحلي بين العملاء. - **مدير الملفات عن بُعد** - إدارة الملفات مباشرة على الخوادم البعيدة مع دعم عرض وتحرير الكود والصور والصوت والفيديو. رفع وتنزيل وإعادة تسمية وحذف ونقل الملفات بسلاسة مع دعم sudo. - **إدارة Docker** - تشغيل وإيقاف وتعليق وحذف الحاويات. عرض إحصائيات الحاويات. التحكم في الحاوية باستخدام طرفية docker exec. لم يُصمم ليحل محل Portainer أو Dockge بل لإدارة حاوياتك ببساطة مقارنة بإنشائها. - **مدير مضيفات SSH** - حفظ وتنظيم وإدارة اتصالات SSH الخاصة بك باستخدام العلامات والمجلدات، وحفظ بيانات تسجيل الدخول القابلة لإعادة الاستخدام بسهولة مع إمكانية أتمتة نشر مفاتيح SSH. @@ -59,6 +43,7 @@ Termix هي منصة مفتوحة المصدر ومجانية للأبد وذا - **RBAC** - إنشاء الأدوار ومشاركة المضيفات عبر المستخدمين/الأدوار. - **مصادقة المستخدمين** - إدارة آمنة للمستخدمين مع ضوابط إدارية ودعم OIDC (مع التحكم في الوصول) و 2FA (TOTP). عرض جلسات المستخدمين النشطة عبر جميع المنصات وإلغاء الصلاحيات. ربط حسابات OIDC/المحلية معاً. - **تشفير قاعدة البيانات** - يُخزَّن الخادم الخلفي كملفات قاعدة بيانات SQLite مشفرة. اطلع على [الوثائق](https://docs.termix.site/security) لمزيد من المعلومات. +- **مفاتيح API** - إنشاء مفاتيح API محددة النطاق للمستخدم مع تواريخ انتهاء صلاحية للاستخدام في الأتمتة/CI. - **تصدير/استيراد البيانات** - تصدير واستيراد مضيفات SSH وبيانات الاعتماد وبيانات مدير الملفات. - **إعداد SSL تلقائي** - إنشاء وإدارة شهادات SSL مدمجة مع إعادة التوجيه إلى HTTPS. - **واجهة مستخدم حديثة** - واجهة نظيفة متوافقة مع سطح المكتب والهاتف المحمول مبنية بـ React و Tailwind CSS و Shadcn. الاختيار بين العديد من سمات واجهة المستخدم بما في ذلك الفاتح والداكن و Dracula وغيرها. استخدام مسارات URL لفتح أي اتصال في وضع ملء الشاشة. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-CN.md b/readme/README-CN.md index 53dbd30b..a1c05112 100644 --- a/readme/README-CN.md +++ b/readme/README-CN.md @@ -1,20 +1,7 @@ # 仓库统计

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

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

@@ -50,7 +34,7 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平 - **SSH 终端访问** - 功能齐全的终端,支持分屏(最多 4 个面板),并配有类似浏览器的标签系统。包括对自定义终端的支持,如常用的终端主题、字体和其他组件。 - **远程桌面访问** - 通过浏览器支持 RDP、VNC 和 Telnet,具有完整的自定义和分屏功能。 -- **SSH 隧道管理** - 创建和管理具有自动重连和健康监测功能的 SSH 隧道,支持 -l 或 -r 连接。 +- **SSH 隧道管理** - 创建和管理具有自动重连和健康监测功能的服务器间 SSH 隧道,支持本地、远程或动态 SOCKS 转发。桌面客户端到服务器的隧道设置按桌面安装本地存储,可选的 C2S 预设快照可保存到服务器、重命名、加载或删除,以便在客户端之间迁移本地隧道配置。 - **远程文件管理器** - 直接在远程服务器上管理文件,支持查看和编辑代码、图像、音频和视频。支持通过 sudo 无缝上传、下载、重命名、删除和移动文件。 - **Docker 管理** - 启动、停止、暂停、移除容器。查看容器统计信息。通过 docker exec 终端控制容器。它的初衷不是取代 Portainer 或 Dockge,而是为了比直接创建容器更简单地管理它们。 - **SSH 主机管理器** - 通过标签和文件夹保存、组织和管理您的 SSH 连接,轻松保存可重用的登录信息,并能自动化部署 SSH 密钥。 @@ -59,6 +43,7 @@ Termix 是一个开源、永久免费、自托管的一体化服务器管理平 - **RBAC** - 创建角色并在用户/角色之间共享主机。 - **用户认证** - 安全的用户管理,具有管理员控制、OIDC(带访问控制)和 2FA (TOTP) 支持。查看所有平台上的活动用户会话并撤销权限。将您的 OIDC/本地账户链接在一起。 - **数据库加密** - 后端存储为加密的 SQLite 数据库文件。查看[文档](https://docs.termix.site/security)了解更多。 +- **API 密钥** - 创建带有到期日期的用户范围 API 密钥,用于自动化/CI。 - **数据导出/导入** - 导出和导入 SSH 主机、凭据和文件管理器数据。 - **自动 SSL 设置** - 内置 SSL 证书生成和管理,支持 HTTPS 重定向。 - **现代 UI** - 使用 React、Tailwind CSS 和 Shadcn 构建的整洁的桌面/移动友好界面。有多种 UI 主题可选,包括浅色、深色、Dracula 等。使用 URL 路由全屏打开任何连接。 @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-DE.md b/readme/README-DE.md index cccbd6ed..4080f9d4 100644 --- a/readme/README-DE.md +++ b/readme/README-DE.md @@ -1,20 +1,7 @@ # Repo-Statistiken

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Wenn Sie möchten, können Sie das Projekt hier unterstützen!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Überblick

@@ -50,7 +34,7 @@ Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-S - **SSH-Terminalzugriff** - Voll ausgestattetes Terminal mit Split-Screen-Unterstützung (bis zu 4 Panels) mit einem browserähnlichen Tab-System. Enthält Unterstützung für die Anpassung des Terminals einschließlich gängiger Terminal-Themes, Schriftarten und anderer Komponenten. - **Remote-Desktop-Zugriff** - RDP-, VNC- und Telnet-Unterstützung über den Browser mit vollständiger Anpassung und Split-Screen. -- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie Unterstützung für -l oder -r Verbindungen. +- **SSH-Tunnelverwaltung** - Erstellen und verwalten Sie Server-zu-Server-SSH-Tunnel mit automatischer Wiederverbindung und Gesundheitsüberwachung sowie lokaler, entfernter oder dynamischer SOCKS-Weiterleitung. Desktop-Client-zu-Server-Tunneleinstellungen werden lokal pro Desktop-Installation gespeichert, optionale C2S-Preset-Snapshots können auf dem Server gespeichert, umbenannt, geladen oder gelöscht werden, um eine lokale Tunnelkonfiguration zwischen Clients zu übertragen. - **Remote-Dateimanager** - Verwalten Sie Dateien direkt auf Remote-Servern mit Unterstützung für das Anzeigen und Bearbeiten von Code, Bildern, Audio und Video. Laden Sie Dateien hoch, herunter, benennen Sie sie um, löschen oder verschieben Sie sie nahtlos mit Sudo-Unterstützung. - **Docker-Verwaltung** - Container starten, stoppen, pausieren, entfernen. Container-Statistiken anzeigen. Container über Docker-Exec-Terminal steuern. Es wurde nicht entwickelt, um Portainer oder Dockge zu ersetzen, sondern um Ihre Container einfach zu verwalten, anstatt sie zu erstellen. - **SSH-Host-Manager** - Speichern, organisieren und verwalten Sie Ihre SSH-Verbindungen mit Tags und Ordnern und speichern Sie einfach wiederverwendbare Anmeldeinformationen mit der Möglichkeit, die Bereitstellung von SSH-Schlüsseln zu automatisieren. @@ -59,6 +43,7 @@ Termix ist eine quelloffene, dauerhaft kostenlose, selbst gehostete All-in-One-S - **RBAC** - Rollen erstellen und Hosts über Benutzer/Rollen teilen. - **Benutzerauthentifizierung** - Sichere Benutzerverwaltung mit Admin-Kontrollen und OIDC- (mit Zugriffskontrolle) sowie 2FA (TOTP)-Unterstützung. Aktive Benutzersitzungen über alle Plattformen anzeigen und Berechtigungen widerrufen. OIDC-/Lokale Konten miteinander verknüpfen. - **Datenbankverschlüsselung** - Backend gespeichert als verschlüsselte SQLite-Datenbankdateien. Weitere Informationen in der [Dokumentation](https://docs.termix.site/security). +- **API-Schlüssel** - Erstellen Sie benutzerbezogene API-Schlüssel mit Ablaufdaten zur Verwendung für Automatisierung/CI. - **Datenexport/-import** - SSH-Hosts, Anmeldeinformationen und Dateimanager-Daten exportieren und importieren. - **Automatische SSL-Einrichtung** - Integrierte SSL-Zertifikatsgenerierung und -verwaltung mit HTTPS-Weiterleitungen. - **Moderne Benutzeroberfläche** - Saubere desktop-/mobilfreundliche Oberfläche, erstellt mit React, Tailwind CSS und Shadcn. Wählen Sie zwischen vielen verschiedenen UI-Themes einschließlich Hell, Dunkel, Dracula usw. Verwenden Sie URL-Routen, um jede Verbindung im Vollbildmodus zu öffnen. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-ES.md b/readme/README-ES.md index f0a9f029..70fbabe1 100644 --- a/readme/README-ES.md +++ b/readme/README-ES.md @@ -1,20 +1,7 @@ # Estadísticas del Repositorio

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Si lo desea, puede apoyar el proyecto aquí.\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Descripción General

@@ -50,7 +34,7 @@ Termix es una plataforma de gestión de servidores todo en uno, de código abier - **Acceso a Terminal SSH** - Terminal completo con soporte de pantalla dividida (hasta 4 paneles) con un sistema de pestañas similar al navegador. Incluye soporte para personalizar el terminal incluyendo temas comunes de terminal, fuentes y otros componentes. - **Acceso a Escritorio Remoto** - Soporte RDP, VNC y Telnet a través del navegador con personalización completa y pantalla dividida. -- **Gestión de Túneles SSH** - Cree y gestione túneles SSH con reconexión automática y monitoreo de estado, con soporte para conexiones -l o -r. +- **Gestión de Túneles SSH** - Cree y gestione túneles SSH de servidor a servidor con reconexión automática, monitoreo de estado y reenvío local, remoto o dinámico SOCKS. La configuración de túneles de cliente de escritorio a servidor se almacena localmente por instalación de escritorio; los snapshots de presets C2S opcionales pueden guardarse en el servidor, renombrarse, cargarse o eliminarse para mover una configuración de túnel local entre clientes. - **Gestor Remoto de Archivos** - Gestione archivos directamente en servidores remotos con soporte para visualizar y editar código, imágenes, audio y video. Suba, descargue, renombre, elimine y mueva archivos sin problemas con soporte sudo. - **Gestión de Docker** - Inicie, detenga, pause, elimine contenedores. Vea estadísticas de contenedores. Controle contenedores usando el terminal docker exec. No fue creado para reemplazar Portainer o Dockge, sino para simplemente gestionar sus contenedores en lugar de crearlos. - **Gestor de Hosts SSH** - Guarde, organice y gestione sus conexiones SSH con etiquetas y carpetas, y guarde fácilmente información de inicio de sesión reutilizable con la capacidad de automatizar el despliegue de claves SSH. @@ -59,6 +43,7 @@ Termix es una plataforma de gestión de servidores todo en uno, de código abier - **RBAC** - Cree roles y comparta hosts entre usuarios/roles. - **Autenticación de Usuarios** - Gestión segura de usuarios con controles de administrador y soporte para OIDC (con control de acceso) y 2FA (TOTP). Vea sesiones activas de usuarios en todas las plataformas y revoque permisos. Vincule sus cuentas OIDC/Locales entre sí. - **Cifrado de Base de Datos** - Backend almacenado como archivos de base de datos SQLite cifrados. Consulte la [documentación](https://docs.termix.site/security) para más información. +- **Claves API** - Cree claves API con ámbito de usuario y fechas de vencimiento para usar en automatización/CI. - **Exportación/Importación de Datos** - Exporte e importe hosts SSH, credenciales y datos del gestor de archivos. - **Configuración Automática de SSL** - Generación y gestión integrada de certificados SSL con redirecciones HTTPS. - **Interfaz Moderna** - Interfaz limpia compatible con escritorio/móvil construida con React, Tailwind CSS y Shadcn. Elija entre muchos temas de UI diferentes, incluyendo claro, oscuro, Dracula, etc. Use rutas URL para abrir cualquier conexión en pantalla completa. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-FR.md b/readme/README-FR.md index adfb6b1b..41cbf8c4 100644 --- a/readme/README-FR.md +++ b/readme/README-FR.md @@ -1,20 +1,7 @@ # Statistiques du dépôt

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Si vous le souhaitez, vous pouvez soutenir le projet ici !\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Présentation

@@ -50,7 +34,7 @@ Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jam - **Accès terminal SSH** - Terminal complet avec support d'écran partagé (jusqu'à 4 panneaux) et un système d'onglets inspiré des navigateurs. Inclut la personnalisation du terminal avec des thèmes courants, des polices et d'autres composants. - **Accès Bureau à Distance** - Support RDP, VNC et Telnet via navigateur avec personnalisation complète et écran partagé. -- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH avec reconnexion automatique et surveillance de l'état, avec support des connexions -l ou -r. +- **Gestion des tunnels SSH** - Créez et gérez des tunnels SSH de serveur à serveur avec reconnexion automatique, surveillance de l'état et transfert local, distant ou SOCKS dynamique. Les paramètres de tunnel client-bureau-vers-serveur sont stockés localement par installation bureau ; des instantanés de préréglages C2S optionnels peuvent être sauvegardés sur le serveur, renommés, chargés ou supprimés pour déplacer une configuration de tunnel locale entre clients. - **Gestionnaire de fichiers distant** - Gérez les fichiers directement sur les serveurs distants avec support de la visualisation et de l'édition de code, images, audio et vidéo. Téléversez, téléchargez, renommez, supprimez et déplacez des fichiers de manière fluide avec support sudo. - **Gestion Docker** - Démarrez, arrêtez, mettez en pause, supprimez des conteneurs. Consultez les statistiques des conteneurs. Contrôlez les conteneurs via le terminal docker exec. Non conçu pour remplacer Portainer ou Dockge, mais plutôt pour gérer simplement vos conteneurs plutôt que de les créer. - **Gestionnaire d'hôtes SSH** - Enregistrez, organisez et gérez vos connexions SSH avec des tags et des dossiers, et sauvegardez facilement les informations de connexion réutilisables tout en automatisant le déploiement des clés SSH. @@ -59,6 +43,7 @@ Termix est une plateforme de gestion de serveurs tout-en-un, open source, à jam - **RBAC** - Créez des rôles et partagez des hôtes entre utilisateurs/rôles. - **Authentification des utilisateurs** - Gestion sécurisée des utilisateurs avec contrôles administrateur et support OIDC (avec contrôle d'accès) et 2FA (TOTP). Visualisez les sessions utilisateur actives sur toutes les plateformes et révoquez les permissions. Liez vos comptes OIDC/locaux ensemble. - **Chiffrement de la base de données** - Le backend est stocké sous forme de fichiers de base de données SQLite chiffrés. Consultez la [documentation](https://docs.termix.site/security) pour plus de détails. +- **Clés API** - Créez des clés API à portée utilisateur avec des dates d'expiration pour une utilisation en automatisation/CI. - **Export/Import de données** - Exportez et importez les hôtes SSH, les identifiants et les données du gestionnaire de fichiers. - **Configuration SSL automatique** - Génération et gestion intégrées de certificats SSL avec redirections HTTPS. - **Interface moderne** - Interface épurée compatible desktop/mobile construite avec React, Tailwind CSS et Shadcn. Choisissez parmi de nombreux thèmes d'interface utilisateur, notamment clair, sombre, Dracula, etc. Utilisez les routes URL pour ouvrir n'importe quelle connexion en plein écran. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-HI.md b/readme/README-HI.md index 8cdb908e..e99eac33 100644 --- a/readme/README-HI.md +++ b/readme/README-HI.md @@ -1,20 +1,7 @@ # रिपॉजिटरी आँकड़े

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-यदि आप चाहें, तो आप यहाँ प्रोजेक्ट को सपोर्ट कर सकते हैं!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # अवलोकन

@@ -50,7 +34,7 @@ Termix एक ओपन-सोर्स, हमेशा के लिए मु - **SSH टर्मिनल एक्सेस** - ब्राउज़र जैसी टैब प्रणाली के साथ स्प्लिट-स्क्रीन सपोर्ट (4 पैनल तक) वाला पूर्ण-विशेषता वाला टर्मिनल। इसमें लोकप्रिय टर्मिनल थीम, फ़ॉन्ट और अन्य कंपोनेंट सहित टर्मिनल को कस्टमाइज़ करने का सपोर्ट शामिल है। - **रिमोट डेस्कटॉप एक्सेस** - ब्राउज़र पर RDP, VNC और Telnet सपोर्ट, पूर्ण कस्टमाइज़ेशन और स्प्लिट स्क्रीन के साथ। -- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन और हेल्थ मॉनिटरिंग के साथ SSH टनल बनाएँ और प्रबंधित करें, -l या -r कनेक्शन के सपोर्ट के साथ। +- **SSH टनल प्रबंधन** - ऑटोमैटिक रीकनेक्शन, हेल्थ मॉनिटरिंग और लोकल, रिमोट या डायनेमिक SOCKS फॉरवर्डिंग के साथ सर्वर-टु-सर्वर SSH टनल बनाएँ और प्रबंधित करें। डेस्कटॉप क्लाइंट-टु-सर्वर टनल सेटिंग्स प्रत्येक डेस्कटॉप इंस्टॉल में स्थानीय रूप से संग्रहीत होती हैं; वैकल्पिक C2S प्रीसेट स्नैपशॉट सर्वर पर सेव, रीनेम, लोड या डिलीट किए जा सकते हैं। - **रिमोट फ़ाइल मैनेजर** - कोड, इमेज, ऑडियो और वीडियो देखने और संपादित करने के सपोर्ट के साथ रिमोट सर्वर पर सीधे फ़ाइलें प्रबंधित करें। sudo सपोर्ट के साथ फ़ाइलें अपलोड, डाउनलोड, रीनेम, डिलीट और मूव करें। - **Docker प्रबंधन** - कंटेनर शुरू, बंद, पॉज़, हटाएँ। कंटेनर स्टैट्स देखें। docker exec टर्मिनल का उपयोग करके कंटेनर को नियंत्रित करें। इसे Portainer या Dockge की जगह लेने के लिए नहीं बनाया गया बल्कि कंटेनर बनाने की तुलना में उन्हें सरलता से प्रबंधित करने के लिए बनाया गया है। - **SSH होस्ट मैनेजर** - टैग और फ़ोल्डर के साथ अपने SSH कनेक्शन सहेजें, व्यवस्थित करें और प्रबंधित करें, और SSH कुंजियों की तैनाती को स्वचालित करने की क्षमता के साथ पुन: उपयोग योग्य लॉगिन जानकारी आसानी से सहेजें। @@ -59,6 +43,7 @@ Termix एक ओपन-सोर्स, हमेशा के लिए मु - **RBAC** - भूमिकाएँ बनाएँ और उपयोगकर्ताओं/भूमिकाओं में होस्ट साझा करें। - **उपयोगकर्ता प्रमाणीकरण** - व्यवस्थापक नियंत्रण और OIDC (एक्सेस कंट्रोल के साथ) और 2FA (TOTP) सपोर्ट के साथ सुरक्षित उपयोगकर्ता प्रबंधन। सभी प्लेटफ़ॉर्म पर सक्रिय उपयोगकर्ता सत्र देखें और अनुमतियाँ रद्द करें। अपने OIDC/स्थानीय खातों को एक साथ जोड़ें। - **डेटाबेस एन्क्रिप्शन** - बैकएंड एन्क्रिप्टेड SQLite डेटाबेस फ़ाइलों के रूप में संग्रहीत। अधिक जानकारी के लिए [डॉक्स](https://docs.termix.site/security) देखें। +- **API कुंजियाँ** - ऑटोमेशन/CI के लिए उपयोग हेतु समाप्ति तिथियों के साथ उपयोगकर्ता-स्कोप्ड API कुंजियाँ बनाएँ। - **डेटा एक्सपोर्ट/इम्पोर्ट** - SSH होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा एक्सपोर्ट और इम्पोर्ट करें। - **स्वचालित SSL सेटअप** - HTTPS रीडायरेक्ट के साथ बिल्ट-इन SSL सर्टिफ़िकेट जनरेशन और प्रबंधन। - **आधुनिक UI** - React, Tailwind CSS, और Shadcn से बना साफ़ डेस्कटॉप/मोबाइल-फ़्रेंडली इंटरफ़ेस। लाइट, डार्क, ड्रैकुला आदि सहित कई अलग-अलग UI थीम के बीच चुनें। किसी भी कनेक्शन को फ़ुल-स्क्रीन में खोलने के लिए URL रूट का उपयोग करें। @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-IT.md b/readme/README-IT.md index d87e1cac..b46ec0d1 100644 --- a/readme/README-IT.md +++ b/readme/README-IT.md @@ -1,20 +1,7 @@ # Statistiche Repo

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Se lo desideri, puoi supportare il progetto qui!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Panoramica

@@ -50,7 +34,7 @@ Termix è una piattaforma di gestione server tutto-in-uno, open-source, per semp - **Accesso Terminale SSH** - Terminale completo con supporto schermo diviso (fino a 4 pannelli) con un sistema di schede in stile browser. Include il supporto per la personalizzazione del terminale, inclusi temi, font e altri componenti comuni. - **Accesso Desktop Remoto** - Supporto RDP, VNC e Telnet tramite browser con personalizzazione completa e schermo diviso. -- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH con riconnessione automatica e monitoraggio dello stato, con supporto per connessioni -l o -r. +- **Gestione Tunnel SSH** - Crea e gestisci tunnel SSH da server a server con riconnessione automatica, monitoraggio dello stato e inoltro locale, remoto o SOCKS dinamico. Le impostazioni del tunnel da client desktop a server sono archiviate localmente per ogni installazione desktop; gli snapshot di preset C2S opzionali possono essere salvati sul server, rinominati, caricati o eliminati per spostare una configurazione di tunnel locale tra i client. - **Gestore File Remoto** - Gestisci i file direttamente sui server remoti con supporto per la visualizzazione e la modifica di codice, immagini, audio e video. Carica, scarica, rinomina, elimina e sposta file senza problemi con supporto sudo. - **Gestione Docker** - Avvia, ferma, metti in pausa, rimuovi container. Visualizza le statistiche dei container. Controlla i container tramite terminale docker exec. Non è stato creato per sostituire Portainer o Dockge, ma piuttosto per gestire semplicemente i tuoi container rispetto alla loro creazione. - **Gestore Host SSH** - Salva, organizza e gestisci le tue connessioni SSH con tag e cartelle, salva facilmente le informazioni di accesso riutilizzabili e automatizza il deployment delle chiavi SSH. @@ -59,6 +43,7 @@ Termix è una piattaforma di gestione server tutto-in-uno, open-source, per semp - **RBAC** - Crea ruoli e condividi host tra utenti/ruoli. - **Autenticazione Utente** - Gestione utenti sicura con controlli amministrativi e supporto OIDC (con controllo degli accessi) e 2FA (TOTP). Visualizza le sessioni utente attive su tutte le piattaforme e revoca i permessi. Collega i tuoi account OIDC/Locali tra loro. - **Crittografia Database** - Il backend è archiviato come file di database SQLite crittografati. Consulta la [documentazione](https://docs.termix.site/security) per maggiori informazioni. +- **Chiavi API** - Crea chiavi API con ambito utente e date di scadenza da utilizzare per automazione/CI. - **Esportazione/Importazione Dati** - Esporta e importa host SSH, credenziali e dati del gestore file. - **Configurazione SSL Automatica** - Generazione e gestione integrata dei certificati SSL con reindirizzamenti HTTPS. - **Interfaccia Moderna** - Interfaccia pulita e responsive per desktop/mobile costruita con React, Tailwind CSS e Shadcn. Scegli tra molti temi UI diversi, inclusi chiaro, scuro, Dracula, ecc. Usa i percorsi URL per aprire qualsiasi connessione a schermo intero. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-JA.md b/readme/README-JA.md index a9d8810e..83a89359 100644 --- a/readme/README-JA.md +++ b/readme/README-JA.md @@ -1,20 +1,7 @@ # リポジトリ統計

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-プロジェクトを支援していただける方はこちらからどうぞ!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # 概要

@@ -50,7 +34,7 @@ Termixは、オープンソースで永久無料のセルフホスト型オー - **SSHターミナルアクセス** - ブラウザ風タブシステムによる分割画面対応(最大4パネル)のフル機能ターミナル。一般的なターミナルテーマ、フォント、その他のコンポーネントを含むターミナルカスタマイズに対応しています。 - **リモートデスクトップアクセス** - ブラウザ上でRDP、VNC、Telnetをサポート、完全なカスタマイズと分割画面に対応しています。 -- **SSHトンネル管理** - 自動再接続とヘルスモニタリング機能を備えたSSHトンネルの作成・管理、-l または -r 接続に対応しています。 +- **SSHトンネル管理** - 自動再接続とヘルスモニタリング、ローカル・リモート・ダイナミックSOCKSフォワーディングを備えたサーバー間SSHトンネルの作成・管理が可能です。デスクトップクライアント対サーバーのトンネル設定はデスクトップインストールごとにローカルに保存され、オプションのC2Sプリセットスナップショットをサーバーに保存・名前変更・読み込み・削除してクライアント間でローカルトンネル設定を移動できます。 - **リモートファイルマネージャー** - コード、画像、音声、動画の表示・編集に対応し、リモートサーバー上のファイルを直接管理できます。sudo対応でファイルのアップロード、ダウンロード、名前変更、削除、移動をシームレスに実行できます。 - **Docker管理** - コンテナの起動、停止、一時停止、削除。コンテナの統計情報を表示。docker execターミナルでコンテナを操作。PortainerやDockgeの代替ではなく、コンテナの作成よりも簡易的な管理を目的としています。 - **SSHホストマネージャー** - タグやフォルダでSSH接続を保存、整理、管理し、再利用可能なログイン情報を簡単に保存しながらSSHキーのデプロイを自動化できます。 @@ -59,6 +43,7 @@ Termixは、オープンソースで永久無料のセルフホスト型オー - **RBAC** - ロールを作成し、ユーザー/ロール間でホストを共有できます。 - **ユーザー認証** - 管理者コントロールとOIDC(アクセス制御付き)および2FA(TOTP)対応による安全なユーザー管理。すべてのプラットフォームでアクティブなユーザーセッションを表示し、権限を取り消し可能。OIDC/ローカルアカウントの連携が可能です。 - **データベース暗号化** - バックエンドは暗号化されたSQLiteデータベースファイルとして保存されます。詳細は[ドキュメント](https://docs.termix.site/security)をご覧ください。 +- **APIキー** - 自動化/CI用に有効期限付きのユーザースコープAPIキーを作成できます。 - **データのエクスポート/インポート** - SSHホスト、認証情報、ファイルマネージャーデータのエクスポートとインポートが可能です。 - **自動SSL設定** - HTTPSリダイレクト付きの組み込みSSL証明書生成・管理が可能です。 - **モダンUI** - React、Tailwind CSS、Shadcnで構築された、デスクトップ/モバイル対応のクリーンなインターフェース。ライト、ダーク、Draculaなど、多くの異なるUIテーマから選択可能。URLルートで任意の接続をフルスクリーンで開くことができます。 @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-KO.md b/readme/README-KO.md index 5b6a9975..7f45ffcd 100644 --- a/readme/README-KO.md +++ b/readme/README-KO.md @@ -1,20 +1,7 @@ # 리포지토리 통계

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-프로젝트를 후원하고 싶으시다면 여기에서 지원해 주세요!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # 개요

@@ -50,7 +34,7 @@ Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 - **SSH 터미널 접속** - 브라우저 스타일 탭 시스템과 분할 화면 지원(최대 4개 패널)을 갖춘 완전한 기능의 터미널. 일반 터미널 테마, 글꼴 및 기타 구성 요소를 포함한 터미널 사용자 정의 지원. - **원격 데스크톱 접속** - 완전한 사용자 정의와 분할 화면을 지원하는 브라우저 기반 RDP, VNC, Telnet 지원. -- **SSH 터널 관리** - 자동 재연결 및 상태 모니터링 기능을 갖춘 SSH 터널 생성 및 관리, -l 또는 -r 연결 지원. +- **SSH 터널 관리** - 자동 재연결, 상태 모니터링, 로컬·원격·동적 SOCKS 포워딩을 지원하는 서버 간 SSH 터널 생성 및 관리. 데스크톱 클라이언트-서버 터널 설정은 데스크톱 설치별로 로컬에 저장되며, 선택적 C2S 사전 설정 스냅샷을 서버에 저장·이름 변경·불러오기·삭제하여 클라이언트 간 로컬 터널 구성을 이동할 수 있습니다. - **원격 파일 관리자** - 코드, 이미지, 오디오, 비디오의 보기 및 편집을 지원하여 원격 서버에서 파일을 직접 관리. sudo 지원으로 파일 업로드, 다운로드, 이름 변경, 삭제, 이동을 원활하게 수행. - **Docker 관리** - 컨테이너 시작, 중지, 일시 정지, 제거. 컨테이너 통계 보기. docker exec 터미널로 컨테이너 제어. Portainer나 Dockge를 대체하기 위한 것이 아니라 컨테이너 생성보다는 간편한 관리를 목적으로 합니다. - **SSH 호스트 관리자** - 태그와 폴더로 SSH 연결을 저장, 정리, 관리하고, 재사용 가능한 로그인 정보를 쉽게 저장하면서 SSH 키 배포를 자동화. @@ -59,6 +43,7 @@ Termix는 오픈 소스이며 영구 무료인 셀프 호스팅 올인원 서버 - **RBAC** - 역할을 생성하고 사용자/역할 간에 호스트 공유. - **사용자 인증** - 관리자 제어와 OIDC(액세스 제어 포함) 및 2FA(TOTP) 지원을 통한 안전한 사용자 관리. 모든 플랫폼에서 활성 사용자 세션을 보고 권한을 취소 가능. OIDC/로컬 계정 연동. - **데이터베이스 암호화** - 백엔드가 암호화된 SQLite 데이터베이스 파일로 저장됨. 자세한 내용은 [문서](https://docs.termix.site/security)를 참조하세요. +- **API 키** - 자동화/CI에 사용할 만료일이 있는 사용자 범위 API 키 생성. - **데이터 내보내기/가져오기** - SSH 호스트, 자격 증명, 파일 관리자 데이터의 내보내기 및 가져오기. - **자동 SSL 설정** - HTTPS 리디렉션을 포함한 내장 SSL 인증서 생성 및 관리. - **모던 UI** - React, Tailwind CSS, Shadcn으로 구축된 깔끔한 데스크톱/모바일 친화적 인터페이스. 라이트, 다크, 드라큘라 등 다양한 UI 테마 선택 가능. URL 라우트를 사용하여 모든 연결을 전체 화면으로 열기 가능. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: diff --git a/readme/README-PT.md b/readme/README-PT.md index 2432a13e..3af83736 100644 --- a/readme/README-PT.md +++ b/readme/README-PT.md @@ -1,20 +1,7 @@ # Estatísticas do Repositório

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Se desejar, você pode apoiar o projeto aqui!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Visão Geral

@@ -50,7 +34,7 @@ Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código a - **Acesso ao Terminal SSH** - Terminal completo com suporte a tela dividida (até 4 painéis) com um sistema de abas similar ao navegador. Inclui suporte para personalização do terminal incluindo temas comuns de terminal, fontes e outros componentes. - **Acesso à Área de Trabalho Remota** - Suporte a RDP, VNC e Telnet pelo navegador com personalização completa e tela dividida -- **Gerenciamento de Túneis SSH** - Crie e gerencie túneis SSH com reconexão automática e monitoramento de saúde, com suporte para conexões -l ou -r +- **Gerenciamento de Túneis SSH** - Crie e gerencie túneis SSH de servidor para servidor com reconexão automática, monitoramento de saúde e encaminhamento local, remoto ou SOCKS dinâmico. As configurações de túnel de cliente desktop para servidor são armazenadas localmente por instalação de desktop; snapshots de predefinições C2S opcionais podem ser salvos no servidor, renomeados, carregados ou excluídos para mover uma configuração de túnel local entre clientes. - **Gerenciador Remoto de Arquivos** - Gerencie arquivos diretamente em servidores remotos com suporte para visualizar e editar código, imagens, áudio e vídeo. Faça upload, download, renomeie, exclua e mova arquivos facilmente com suporte sudo. - **Gerenciamento de Docker** - Inicie, pare, pause, remova contêineres. Visualize estatísticas de contêineres. Controle contêineres usando o terminal Docker Exec. Não foi feito para substituir Portainer ou Dockge, mas sim para simplesmente gerenciar seus contêineres em vez de criá-los. - **Gerenciador de Hosts SSH** - Salve, organize e gerencie suas conexões SSH com tags e pastas, e salve facilmente informações de login reutilizáveis com a capacidade de automatizar a implantação de chaves SSH @@ -59,6 +43,7 @@ Termix é uma plataforma de gerenciamento de servidores tudo-em-um, de código a - **RBAC** - Crie funções e compartilhe hosts entre usuários/funções - **Autenticação de Usuários** - Gerenciamento seguro de usuários com controles de administrador e suporte para OIDC (com controle de acesso) e 2FA (TOTP). Visualize sessões ativas de usuários em todas as plataformas e revogue permissões. Vincule suas contas OIDC/Locais entre si. - **Criptografia de Banco de Dados** - Backend armazenado como arquivos de banco de dados SQLite criptografados. Consulte a [documentação](https://docs.termix.site/security) para mais informações. +- **Chaves de API** - Crie chaves de API com escopo de usuário e datas de expiração para uso em automação/CI. - **Exportação/Importação de Dados** - Exporte e importe hosts SSH, credenciais e dados do gerenciador de arquivos - **Configuração Automática de SSL** - Geração e gerenciamento integrado de certificados SSL com redirecionamentos HTTPS - **Interface Moderna** - Interface limpa compatível com desktop/mobile construída com React, Tailwind CSS e Shadcn. Escolha entre muitos temas de interface diferentes, incluindo claro, escuro, Drácula, etc. Use rotas de URL para abrir qualquer conexão em tela cheia. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: @@ -181,7 +166,7 @@ Por favor, seja o mais detalhado possível no seu relato, preferencialmente escr [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 + Termix Demo 1 Termix Demo 2

@@ -202,12 +187,12 @@ Por favor, seja o mais detalhado possível no seu relato, preferencialmente escr

Termix Demo 9 - Termix Demo 10 + Termix Demo 10

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

Alguns vídeos e imagens podem estar desatualizados ou podem não mostrar perfeitamente as funcionalidades. diff --git a/readme/README-RU.md b/readme/README-RU.md index 78b43e5a..b846a52e 100644 --- a/readme/README-RU.md +++ b/readme/README-RU.md @@ -1,20 +1,7 @@ # Статистика репозитория

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Если хотите, вы можете поддержать проект здесь!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Обзор

@@ -50,7 +34,7 @@ Termix — это платформа для управления сервера - **Доступ к SSH-терминалу** — Полнофункциональный терминал с поддержкой разделения экрана (до 4 панелей) и системой вкладок, как в браузере. Включает поддержку настройки терминала, включая популярные темы, шрифты и другие компоненты. - **Доступ к удалённому рабочему столу** — Поддержка RDP, VNC и Telnet через браузер с полной настройкой и разделением экрана -- **Управление SSH-туннелями** — Создание и управление SSH-туннелями с автоматическим переподключением и мониторингом состояния, с поддержкой соединений -l и -r +- **Управление SSH-туннелями** — Создание и управление межсерверными SSH-туннелями с автоматическим переподключением, мониторингом состояния и локальной, удалённой или динамической SOCKS-переадресацией. Настройки туннелей «десктопный клиент — сервер» хранятся локально для каждой установки; опциональные снимки C2S-пресетов можно сохранять на сервере, переименовывать, загружать или удалять для переноса конфигурации между клиентами. - **Удалённый файловый менеджер** — Управление файлами непосредственно на удалённых серверах с поддержкой просмотра и редактирования кода, изображений, аудио и видео. Загрузка, скачивание, переименование, удаление и перемещение файлов с поддержкой sudo. - **Управление Docker** — Запуск, остановка, приостановка, удаление контейнеров. Просмотр статистики контейнеров. Управление контейнером через терминал docker exec. Не предназначен для замены Portainer или Dockge, а скорее для простого управления контейнерами по сравнению с их созданием. - **Менеджер SSH-хостов** — Сохранение, организация и управление SSH-подключениями с помощью тегов и папок, с возможностью сохранения данных для повторного входа и автоматизации развёртывания SSH-ключей @@ -59,6 +43,7 @@ Termix — это платформа для управления сервера - **RBAC** — Создание ролей и предоставление общего доступа к хостам для пользователей/ролей - **Аутентификация пользователей** — Безопасное управление пользователями с административным контролем и поддержкой OIDC (с контролем доступа) и 2FA (TOTP). Просмотр активных сессий пользователей на всех платформах и отзыв прав доступа. Связывание аккаунтов OIDC/локальных аккаунтов. - **Шифрование базы данных** — Бэкенд хранится в виде зашифрованных файлов базы данных SQLite. Подробнее в [документации](https://docs.termix.site/security) +- **API-ключи** — Создание API-ключей с областью видимости пользователя и сроками действия для использования в автоматизации/CI. - **Экспорт/импорт данных** — Экспорт и импорт SSH-хостов, учётных данных и данных файлового менеджера - **Автоматическая настройка SSL** — Встроенная генерация и управление SSL-сертификатами с перенаправлением на HTTPS - **Современный интерфейс** — Чистый интерфейс для десктопа и мобильных устройств, построенный на React, Tailwind CSS и Shadcn. Выбор между множеством различных тем интерфейса, включая светлую, тёмную, Dracula и т. д. Использование URL-маршрутов для открытия любого подключения в полноэкранном режиме. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: @@ -181,7 +166,7 @@ networks: [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 + Termix Demo 1 Termix Demo 2

@@ -202,12 +187,12 @@ networks:

Termix Demo 9 - Termix Demo 10 + Termix Demo 10

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

Некоторые видео и изображения могут быть устаревшими или не полностью отражать функциональность. diff --git a/readme/README-TR.md b/readme/README-TR.md index 206a841a..33a0d6cf 100644 --- a/readme/README-TR.md +++ b/readme/README-TR.md @@ -1,20 +1,7 @@ # Repo İstatistikleri

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Projeyi desteklemek isterseniz, buradan destek olabilirsiniz!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Genel Bakış

@@ -50,7 +34,7 @@ Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındıra - **SSH Terminal Erişimi** - Tarayıcı benzeri sekme sistemiyle bölünmüş ekran desteğine sahip (4 panele kadar) tam özellikli terminal. Yaygın terminal temaları, yazı tipleri ve diğer bileşenler dahil olmak üzere terminal özelleştirme desteği içerir. - **Uzak Masaüstü Erişimi** - Tam özelleştirme ve bölünmüş ekran ile tarayıcı üzerinden RDP, VNC ve Telnet desteği -- **SSH Tünel Yönetimi** - Otomatik yeniden bağlanma ve sağlık izleme ile SSH tünelleri oluşturun ve yönetin, -l veya -r bağlantıları desteğiyle +- **SSH Tünel Yönetimi** - Otomatik yeniden bağlanma, sağlık izleme ve yerel, uzak veya dinamik SOCKS yönlendirme desteğiyle sunucular arası SSH tünelleri oluşturun ve yönetin. Masaüstü istemci-sunucu tünel ayarları her masaüstü kurulumu için yerel olarak depolanır; isteğe bağlı C2S hazır ayar anlık görüntüleri sunucuya kaydedilebilir, yeniden adlandırılabilir, yüklenebilir veya silinebilir. - **Uzak Dosya Yöneticisi** - Uzak sunuculardaki dosyaları doğrudan yönetin; kod, görüntü, ses ve video görüntüleme ve düzenleme desteğiyle. Sudo desteğiyle dosyaları sorunsuzca yükleyin, indirin, yeniden adlandırın, silin ve taşıyın. - **Docker Yönetimi** - Konteynerleri başlatın, durdurun, duraklatın, kaldırın. Konteyner istatistiklerini görüntüleyin. Docker exec terminali kullanarak konteyneri kontrol edin. Portainer veya Dockge'nin yerini almak için değil, konteynerlerinizi oluşturmak yerine basitçe yönetmek için tasarlanmıştır. - **SSH Ana Bilgisayar Yöneticisi** - SSH bağlantılarınızı etiketler ve klasörlerle kaydedin, düzenleyin ve yönetin; yeniden kullanılabilir giriş bilgilerini kolayca kaydedin ve SSH anahtarlarının dağıtımını otomatikleştirin @@ -59,6 +43,7 @@ Termix, açık kaynaklı, sonsuza kadar ücretsiz, kendi sunucunuzda barındıra - **RBAC** - Roller oluşturun ve ana bilgisayarları kullanıcılar/roller arasında paylaşın - **Kullanıcı Kimlik Doğrulama** - Yönetici kontrolleri, OIDC (erişim kontrollü) ve 2FA (TOTP) desteğiyle güvenli kullanıcı yönetimi. Tüm platformlardaki aktif kullanıcı oturumlarını görüntüleyin ve izinleri iptal edin. OIDC/Yerel hesaplarınızı birbirine bağlayın. - **Veritabanı Şifreleme** - Arka uç, şifrelenmiş SQLite veritabanı dosyaları olarak depolanır. Daha fazla bilgi için [belgelere](https://docs.termix.site/security) bakın. +- **API Anahtarları** - Otomasyon/CI için kullanılmak üzere son kullanma tarihleriyle kullanıcı kapsamlı API anahtarları oluşturun. - **Veri Dışa/İçe Aktarma** - SSH ana bilgisayarlarını, kimlik bilgilerini ve dosya yöneticisi verilerini dışa ve içe aktarın - **Otomatik SSL Kurulumu** - HTTPS yönlendirmeleriyle yerleşik SSL sertifika oluşturma ve yönetimi - **Modern Arayüz** - React, Tailwind CSS ve Shadcn ile oluşturulmuş temiz masaüstü/mobil uyumlu arayüz. Işık, karanlık, Dracula vb. dahil olmak üzere birçok farklı UI teması arasından seçim yapın. Herhangi bir bağlantıyı tam ekranda açmak için URL yollarını kullanın. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: @@ -181,7 +166,7 @@ Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 + Termix Demo 1 Termix Demo 2

@@ -202,12 +187,12 @@ Lütfen sorununuzu mümkün olduğunca ayrıntılı yazın, tercihen İngilizce

Termix Demo 9 - Termix Demo 10 + Termix Demo 10

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

Bazı videolar ve görseller güncel olmayabilir veya özellikleri tam olarak yansıtmayabilir. diff --git a/readme/README-VI.md b/readme/README-VI.md index bf721d80..3ce7512c 100644 --- a/readme/README-VI.md +++ b/readme/README-VI.md @@ -1,20 +1,7 @@ # Thống Kê Repo

- English English · - 中文 中文 · - 日本語 日本語 · - 한국어 한국어 · - Français Français · - Deutsch Deutsch · - Español Español · - Português Português · - Русский Русский · - العربية العربية · - हिन्दी हिन्दी · - Türkçe Türkçe · - Tiếng Việt Tiếng Việt · - Italiano Italiano +🇺🇸 English · 🇨🇳 中文 · 🇯🇵 日本語 · 🇰🇷 한국어 · 🇫🇷 Français · 🇩🇪 Deutsch · 🇪🇸 Español · 🇧🇷 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇹🇷 Türkçe · 🇻🇳 Tiếng Việt · 🇮🇹 Italiano

![GitHub Repo stars](https://img.shields.io/github/stars/Termix-SSH/Termix?style=flat&label=Stars) @@ -34,9 +21,6 @@ Termix Banner

-Nếu bạn muốn, bạn có thể hỗ trợ dự án tại đây!\ -[![GitHub Sponsor](https://img.shields.io/badge/Sponsor-LukeGus-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/sponsors/LukeGus) - # Tổng Quan

@@ -50,7 +34,7 @@ Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồ - **Truy Cập Terminal SSH** - Terminal đầy đủ tính năng với hỗ trợ chia màn hình (lên đến 4 bảng) với hệ thống tab kiểu trình duyệt. Bao gồm hỗ trợ tùy chỉnh terminal bao gồm các chủ đề terminal phổ biến, phông chữ và các thành phần khác. - **Truy Cập Màn Hình Từ Xa** - Hỗ trợ RDP, VNC và Telnet qua trình duyệt với đầy đủ tùy chỉnh và chia màn hình -- **Quản Lý Đường Hầm SSH** - Tạo và quản lý đường hầm SSH với tự động kết nối lại và giám sát sức khỏe, hỗ trợ kết nối -l hoặc -r +- **Quản Lý Đường Hầm SSH** - Tạo và quản lý đường hầm SSH giữa các máy chủ với tự động kết nối lại, giám sát sức khỏe và chuyển tiếp cục bộ, từ xa hoặc SOCKS động. Cài đặt đường hầm từ máy khách desktop đến máy chủ được lưu trữ cục bộ cho mỗi bản cài đặt desktop; các snapshot C2S preset tùy chọn có thể được lưu trên máy chủ, đổi tên, tải hoặc xóa để di chuyển cấu hình đường hầm cục bộ giữa các máy khách. - **Trình Quản Lý Tệp Từ Xa** - Quản lý tệp trực tiếp trên máy chủ từ xa với hỗ trợ xem và chỉnh sửa mã, hình ảnh, âm thanh và video. Tải lên, tải xuống, đổi tên, xóa và di chuyển tệp liền mạch với hỗ trợ sudo. - **Quản Lý Docker** - Khởi động, dừng, tạm dừng, xóa container. Xem thống kê container. Điều khiển container bằng terminal docker exec. Không được tạo ra để thay thế Portainer hay Dockge mà đơn giản là để quản lý container của bạn thay vì tạo mới chúng. - **Trình Quản Lý Máy Chủ SSH** - Lưu, sắp xếp và quản lý các kết nối SSH của bạn với thẻ và thư mục, dễ dàng lưu thông tin đăng nhập có thể tái sử dụng đồng thời có thể tự động hóa việc triển khai khóa SSH @@ -59,6 +43,7 @@ Termix là nền tảng quản lý máy chủ tất cả trong một, mã nguồ - **RBAC** - Tạo vai trò và chia sẻ máy chủ giữa người dùng/vai trò - **Xác Thực Người Dùng** - Quản lý người dùng an toàn với quyền quản trị và hỗ trợ OIDC (có kiểm soát truy cập) và 2FA (TOTP). Xem phiên hoạt động của người dùng trên tất cả các nền tảng và thu hồi quyền. Liên kết tài khoản OIDC/Nội bộ của bạn với nhau. - **Mã Hóa Cơ Sở Dữ Liệu** - Backend được lưu trữ dưới dạng tệp cơ sở dữ liệu SQLite được mã hóa. Xem [tài liệu](https://docs.termix.site/security) để biết thêm. +- **Khóa API** - Tạo khóa API theo phạm vi người dùng với ngày hết hạn để sử dụng cho tự động hóa/CI. - **Xuất/Nhập Dữ Liệu** - Xuất và nhập máy chủ SSH, thông tin xác thực và dữ liệu trình quản lý tệp - **Thiết Lập SSL Tự Động** - Tạo và quản lý chứng chỉ SSL tích hợp với chuyển hướng HTTPS - **Giao Diện Hiện Đại** - Giao diện sạch sẽ, thân thiện với máy tính/di động được xây dựng bằng React, Tailwind CSS và Shadcn. Chọn giữa nhiều chủ đề UI khác nhau bao gồm sáng, tối, Dracula, v.v. Sử dụng đường dẫn URL để mở bất kỳ kết nối nào ở chế độ toàn màn hình. @@ -122,7 +107,7 @@ services: - termix-net guacd: - image: guacamole/guacd:latest + image: guacamole/guacd:1.6.0 container_name: guacd restart: unless-stopped ports: @@ -181,7 +166,7 @@ Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết b [![YouTube](../repo-images/YouTube.jpg)](https://www.youtube.com/@TermixSSH/videos)

- Termix Demo 1 + Termix Demo 1 Termix Demo 2

@@ -202,12 +187,12 @@ Vui lòng mô tả vấn đề càng chi tiết càng tốt, ưu tiên viết b

Termix Demo 9 - Termix Demo 10 + Termix Demo 10

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

Một số video và hình ảnh có thể đã lỗi thời hoặc không thể hiện chính xác hoàn toàn các tính năng. diff --git a/scripts/patch-app-builder-lib.cjs b/scripts/patch-app-builder-lib.cjs new file mode 100644 index 00000000..66c6c12e --- /dev/null +++ b/scripts/patch-app-builder-lib.cjs @@ -0,0 +1,197 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const collectorPath = path.join( + __dirname, + "..", + "node_modules", + "app-builder-lib", + "out", + "node-module-collector", + "nodeModulesCollector.js", +); + +const appFileCopierPath = path.join( + __dirname, + "..", + "node_modules", + "app-builder-lib", + "out", + "util", + "appFileCopier.js", +); + +const moduleManagerPath = path.join( + __dirname, + "..", + "node_modules", + "app-builder-lib", + "out", + "node-module-collector", + "moduleManager.js", +); + +function patchFile(filePath, replacements) { + if (!fs.existsSync(filePath)) { + return; + } + + let source = fs.readFileSync(filePath, "utf8"); + let changed = false; + + for (const { original, patched, name, alreadyPatched = [] } of replacements) { + if ( + source.includes(patched) || + alreadyPatched.some((marker) => source.includes(marker)) + ) { + continue; + } + + if (!source.includes(original)) { + console.warn( + `app-builder-lib patch "${name}" was not applied; expected source was not found.`, + ); + continue; + } + + source = source.replace(original, patched); + changed = true; + } + + if (changed) { + fs.writeFileSync(filePath, source); + } +} + +patchFile(collectorPath, [ + { + name: "node module collector spawn shell", + original: ` shell: true, // \`true\`\` is now required: https://github.com/electron-userland/electron-builder/issues/9488`, + patched: ` shell: false, // Avoid Node DEP0190; .cmd files are wrapped through cmd.exe above.`, + }, + { + name: "node module collector output flush", + alreadyPatched: [ + ` outStream.end(); + }`, + ], + original: ` outStream.close(); + // https://github.com/npm/npm/issues/17624 + const shouldIgnore = code === 1 && "npm" === execName.toLowerCase() && args.includes("list"); + if (shouldIgnore) { + builder_util_1.log.debug(null, "\`npm list\` returned non-zero exit code, but it MIGHT be expected (https://github.com/npm/npm/issues/17624). Check stderr for details."); + } + if (stderr.length > 0) { + builder_util_1.log.debug({ stderr }, "note: there was node module collector output on stderr"); + this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_COLLECTOR_OUTPUT].push(stderr); + } + const shouldResolve = code === 0 || shouldIgnore; + return shouldResolve ? resolve() : reject(new Error(\`Node module collector process exited with code \${code}:\\n\${stderr}\`));`, + patched: ` const finish = () => { + // https://github.com/npm/npm/issues/17624 + const shouldIgnore = code === 1 && "npm" === execName.toLowerCase() && args.includes("list"); + if (shouldIgnore) { + builder_util_1.log.debug(null, "\`npm list\` returned non-zero exit code, but it MIGHT be expected (https://github.com/npm/npm/issues/17624). Check stderr for details."); + } + if (stderr.length > 0) { + builder_util_1.log.debug({ stderr }, "note: there was node module collector output on stderr"); + this.cache.logSummary[moduleManager_1.LogMessageByKey.PKG_COLLECTOR_OUTPUT].push(stderr); + } + const shouldResolve = code === 0 || shouldIgnore; + return shouldResolve ? resolve() : reject(new Error(\`Node module collector process exited with code \${code}:\\n\${stderr}\`)); + }; + if (outStream.writableFinished) { + finish(); + } + else { + outStream.once("finish", finish); + outStream.end(); + }`, + }, + { + name: "node module collector finish guard", + original: ` if (outStream.writableFinished || outStream.closed) {`, + patched: ` if (outStream.writableFinished) {`, + }, +]); + +patchFile(moduleManagerPath, [ + { + name: "node module collector npm alias resolution", + original: ` semverSatisfies(found, range) { + if ((0, builder_util_1.isEmptyOrSpaces)(range) || range === "*") {`, + patched: ` semverSatisfies(found, range, packageNameMatches = true) { + if (!packageNameMatches) { + return true; + } + if ((0, builder_util_1.isEmptyOrSpaces)(range) || range === "*") {`, + }, + { + name: "node module collector direct alias match", + original: ` if (json && this.semverSatisfies(json.version, requiredRange)) { + return { packageDir: path.dirname(direct), packageJson: json }; + }`, + patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) { + return { packageDir: path.dirname(direct), packageJson: json }; + }`, + }, + { + name: "node module collector alias match", + original: ` if (json && this.semverSatisfies(json.version, requiredRange)) { + return { packageDir: path.dirname(candidate), packageJson: json }; + }`, + patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) { + return { packageDir: path.dirname(candidate), packageJson: json }; + }`, + }, + { + name: "node module collector scoped alias match", + original: ` if (json && this.semverSatisfies(json.version, requiredRange)) { + return { packageDir: path.dirname(candidatePkgJson), packageJson: json }; + }`, + patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) { + return { packageDir: path.dirname(candidatePkgJson), packageJson: json }; + }`, + }, + { + name: "node module collector nested alias match", + original: ` if (json && this.semverSatisfies(json.version, requiredRange)) { + return { packageDir: path.dirname(candidatePkgJson), packageJson: json }; + }`, + patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) { + return { packageDir: path.dirname(candidatePkgJson), packageJson: json }; + }`, + }, + { + name: "node module collector nested direct alias match", + original: ` if (json && this.semverSatisfies(json.version, requiredRange)) { + return { packageDir: path.dirname(candidateDirect), packageJson: json }; + }`, + patched: ` if (json && this.semverSatisfies(json.version, requiredRange, json.name === pkgName)) { + return { packageDir: path.dirname(candidateDirect), packageJson: json }; + }`, + }, +]); + +patchFile(appFileCopierPath, [ + { + name: "node module collector fallback", + original: ` const collector = (0, node_module_collector_1.getCollectorByPackageManager)(pm, dir, tempDirManager); + deps = await collector.getNodeModules({ packageName: packager.metadata.name }); + if (deps.nodeModules.length > 0) {`, + patched: ` const collector = (0, node_module_collector_1.getCollectorByPackageManager)(pm, dir, tempDirManager); + try { + deps = await collector.getNodeModules({ packageName: packager.metadata.name }); + } + catch (error) { + const isLastSearchDirectory = searchDirectories.indexOf(dir) >= searchDirectories.length - 1; + const isLastPackageManager = pmApproaches.indexOf(pm) >= pmApproaches.length - 1; + if (isLastSearchDirectory && isLastPackageManager) { + throw error; + } + builder_util_1.log.warn({ pm, searchDir: dir, error: error instanceof Error ? error.message : String(error) }, "node modules collection failed, trying fallback"); + continue; + } + if (deps.nodeModules.length > 0) {`, + }, +]); diff --git a/scripts/write-electron-build-info.cjs b/scripts/write-electron-build-info.cjs new file mode 100644 index 00000000..bd20ad04 --- /dev/null +++ b/scripts/write-electron-build-info.cjs @@ -0,0 +1,24 @@ +const fs = require("fs"); +const path = require("path"); + +const outputPath = path.join(__dirname, "..", "electron", "build-info.cjs"); +const rawBuildTimestamp = + process.env.TERMIX_BUILD_TIMESTAMP || process.env.BUILD_TIMESTAMP; +const parsedBuildTimestamp = rawBuildTimestamp + ? Number(rawBuildTimestamp) + : NaN; +const buildTimestamp = Number.isInteger(parsedBuildTimestamp) + ? parsedBuildTimestamp + : Math.floor(Date.now() / 1000); +const buildInfo = { + buildTimestamp, + generatedAt: new Date().toISOString(), +}; + +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync( + outputPath, + `module.exports = ${JSON.stringify(buildInfo, null, 2)};\n`, +); + +console.log(`Wrote Electron build info: ${buildTimestamp}`); diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 59920818..03140c1f 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -8,12 +8,12 @@ import hostRoutes from "./routes/host.js"; import alertRoutes from "./routes/alerts.js"; import credentialsRoutes from "./routes/credentials.js"; import snippetsRoutes from "./routes/snippets.js"; +import c2sTunnelPresetRoutes from "./routes/c2s-tunnel-presets.js"; import terminalRoutes from "./routes/terminal.js"; import guacamoleRoutes from "../guacamole/routes.js"; import networkTopologyRoutes from "./routes/network-topology.js"; import rbacRoutes from "./routes/rbac.js"; import { createCorsMiddleware } from "../utils/cors-config.js"; -import fetch from "node-fetch"; import fs from "fs"; import path from "path"; import os from "os"; @@ -118,6 +118,31 @@ class GitHubCache { const githubCache = new GitHubCache(); +function parseSemver( + version: string | undefined, +): [number, number, number] | null { + const match = String(version || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/); + if (!match) return null; + + return [Number(match[1]), Number(match[2]), Number(match[3] || 0)]; +} + +function compareSemver( + a: string | undefined, + b: string | undefined, +): number | null { + const parsedA = parseSemver(a); + const parsedB = parseSemver(b); + if (!parsedA || !parsedB) return null; + + for (let i = 0; i < 3; i += 1) { + if (parsedA[i] > parsedB[i]) return 1; + if (parsedA[i] < parsedB[i]) return -1; + } + + return 0; +} + const GITHUB_API_BASE = "https://api.github.com"; const REPO_OWNER = "Termix-SSH"; const REPO_NAME = "Termix"; @@ -143,7 +168,7 @@ async function fetchGitHubAPI( "User-Agent": "TermixUpdateChecker/1.0", "X-GitHub-Api-Version": "2022-11-28", }, - agent: getProxyAgent(url), + dispatcher: getProxyAgent(url), }); if (!response.ok) { @@ -299,12 +324,19 @@ app.get("/version", authenticateJWT, async (req, res) => { return res.status(401).send("Remote Version Not Found"); } - const isUpToDate = localVersion === remoteVersion; + const versionComparison = compareSemver(localVersion, remoteVersion); + const status = + versionComparison === null || versionComparison === 0 + ? "up_to_date" + : versionComparison > 0 + ? "beta" + : "requires_update"; const response = { - status: isUpToDate ? "up_to_date" : "requires_update", + status, localVersion: localVersion, version: remoteVersion, + remoteVersion: remoteVersion, latest_release: { tag_name: releaseData.data.tag_name, name: releaseData.data.name, @@ -624,7 +656,9 @@ app.post("/database/export", authenticateJWT, async (req, res) => { operation: "export_temp_dir_error", tempDir, }); - throw new Error(`Failed to create temp directory: ${dirError.message}`); + throw new Error(`Failed to create temp directory: ${dirError.message}`, { + cause: dirError, + }); } const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); @@ -1161,7 +1195,7 @@ app.post( mimetype: req.file.mimetype, }); - let userDataKey = DataCrypto.getUserDataKey(userId); + const userDataKey = DataCrypto.getUserDataKey(userId); if (!userDataKey) { throw new Error("User data not unlocked"); } @@ -1719,6 +1753,7 @@ app.use("/host", hostRoutes); app.use("/alerts", alertRoutes); app.use("/credentials", credentialsRoutes); app.use("/snippets", snippetsRoutes); +app.use("/c2s-tunnel-presets", c2sTunnelPresetRoutes); app.use("/terminal", terminalRoutes); app.use("/guacamole", guacamoleRoutes); app.use("/network-topology", networkTopologyRoutes); @@ -1738,10 +1773,38 @@ if (frontendDist) { databaseLogger.info(`Serving frontend from: ${frontendDist}`, { operation: "static_files", }); - app.use(express.static(frontendDist)); + app.use( + express.static(frontendDist, { + setHeaders: (res, filePath) => { + const relativePath = path + .relative(frontendDist, filePath) + .replaceAll(path.sep, "/"); + + if (relativePath.startsWith("assets/")) { + res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); + return; + } + + if ( + relativePath === "index.html" || + relativePath === "sw.js" || + relativePath === "manifest.json" + ) { + res.setHeader( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0", + ); + } + }, + }), + ); app.use((req, res, next) => { if (req.method === "GET" && req.accepts("html")) { + res.setHeader( + "Cache-Control", + "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0", + ); res.sendFile(path.join(frontendDist, "index.html")); } else { next(); @@ -1750,13 +1813,13 @@ if (frontendDist) { } app.use( - // eslint-disable-next-line @typescript-eslint/no-unused-vars ( err: unknown, req: express.Request, res: express.Response, _next: express.NextFunction, ) => { + void _next; apiLogger.error("Unhandled error in request", err, { operation: "error_handler", method: req.method, diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index 34d99252..5acce909 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -118,6 +118,7 @@ async function initializeDatabaseAsync(): Promise { throw new Error( `Database decryption failed: ${error instanceof Error ? error.message : "Unknown error"}. This prevents data loss.`, + { cause: error }, ); } } else { @@ -317,6 +318,18 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); + CREATE TABLE IF NOT EXISTS c2s_tunnel_presets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + platform TEXT, + computer_name TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS ssh_folders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, @@ -424,10 +437,31 @@ async function initializeCompleteDatabase(): Promise { FOREIGN KEY (access_id) REFERENCES host_access (id) ON DELETE SET NULL ); + CREATE TABLE IF NOT EXISTS api_keys ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + token_hash TEXT NOT NULL, + token_prefix TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TEXT, + last_used_at TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + `); try { - sqlite.prepare("DELETE FROM sessions").run(); + const result = sqlite + .prepare("DELETE FROM sessions WHERE expires_at <= ?") + .run(new Date().toISOString()); + if (result.changes > 0) { + databaseLogger.info("Expired sessions cleaned up on startup", { + operation: "db_init_session_cleanup", + deletedSessions: result.changes, + }); + } } catch (e) { databaseLogger.warn("Could not clear expired sessions on startup", { operation: "db_init_session_cleanup_failed", @@ -803,6 +837,31 @@ const migrateSchema = () => { } } + try { + sqlite.prepare("SELECT id FROM c2s_tunnel_presets LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS c2s_tunnel_presets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + config TEXT NOT NULL, + platform TEXT, + computer_name TEXT, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create c2s_tunnel_presets table", { + operation: "schema_migration", + error: createError, + }); + } + } + try { sqlite .prepare("SELECT id FROM sessions LIMIT 1") @@ -1175,6 +1234,32 @@ const migrateSchema = () => { } } + try { + sqlite.prepare("SELECT id FROM api_keys LIMIT 1").get(); + } catch { + try { + sqlite.exec(` + CREATE TABLE IF NOT EXISTS api_keys ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + token_hash TEXT NOT NULL, + token_prefix TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + expires_at TEXT, + last_used_at TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE + ); + `); + } catch (createError) { + databaseLogger.warn("Failed to create api_keys table", { + operation: "schema_migration", + error: createError, + }); + } + } + try { const existingRoles = sqlite.prepare("SELECT name, is_system FROM roles").all() as Array<{ name: string; is_system: number }>; diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 0bc3ae6f..0e0d89d9 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -298,6 +298,23 @@ export const snippetFolders = sqliteTable("snippet_folders", { .default(sql`CURRENT_TIMESTAMP`), }); +export const c2sTunnelPresets = sqliteTable("c2s_tunnel_presets", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + config: text("config").notNull(), + platform: text("platform"), + computerName: text("computer_name"), + createdAt: text("created_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), + updatedAt: text("updated_at") + .notNull() + .default(sql`CURRENT_TIMESTAMP`), +}); + export const snippetAccess = sqliteTable("snippet_access", { id: integer("id").primaryKey({ autoIncrement: true }), snippetId: integer("snippet_id") @@ -572,3 +589,17 @@ export const opksshTokens = sqliteTable("opkssh_tokens", { expiresAt: text("expires_at").notNull(), lastUsed: text("last_used"), }); + +export const apiKeys = sqliteTable("api_keys", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + name: text("name").notNull(), + tokenHash: text("token_hash").notNull(), + tokenPrefix: text("token_prefix").notNull(), + createdAt: text("created_at").notNull().default(sql`CURRENT_TIMESTAMP`), + expiresAt: text("expires_at"), + lastUsedAt: text("last_used_at"), + isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), +}); diff --git a/src/backend/database/routes/alerts.ts b/src/backend/database/routes/alerts.ts index fe9d8dbb..aa66ec57 100644 --- a/src/backend/database/routes/alerts.ts +++ b/src/backend/database/routes/alerts.ts @@ -7,7 +7,6 @@ import express from "express"; import { db } from "../db/index.js"; import { dismissedAlerts } from "../db/schema.js"; import { eq, and } from "drizzle-orm"; -import fetch from "node-fetch"; import { authLogger } from "../../utils/logger.js"; import { AuthManager } from "../../utils/auth-manager.js"; import { getProxyAgent } from "../../utils/proxy-agent.js"; @@ -61,7 +60,7 @@ async function fetchAlertsFromGitHub(): Promise { Accept: "application/json", "User-Agent": "TermixAlertChecker/1.0", }, - agent: getProxyAgent(url), + dispatcher: getProxyAgent(url), }); if (!response.ok) { diff --git a/src/backend/database/routes/c2s-tunnel-presets.ts b/src/backend/database/routes/c2s-tunnel-presets.ts new file mode 100644 index 00000000..7bca59e4 --- /dev/null +++ b/src/backend/database/routes/c2s-tunnel-presets.ts @@ -0,0 +1,247 @@ +import type { + AuthenticatedRequest, + TunnelConnection, +} from "../../../types/index.js"; +import express from "express"; +import { db } from "../db/index.js"; +import { c2sTunnelPresets } from "../db/schema.js"; +import { and, asc, eq, sql } from "drizzle-orm"; +import type { Request, Response } from "express"; +import { authLogger, databaseLogger } from "../../utils/logger.js"; +import { AuthManager } from "../../utils/auth-manager.js"; + +const router = express.Router(); + +const authManager = AuthManager.getInstance(); +const authenticateJWT = authManager.createAuthMiddleware(); +const requireDataAccess = authManager.createDataAccessMiddleware(); + +function isNonEmptyString(val: unknown): val is string { + return typeof val === "string" && val.trim().length > 0; +} + +function parsePreset(row: typeof c2sTunnelPresets.$inferSelect) { + return { + ...row, + config: JSON.parse(row.config) as TunnelConnection[], + }; +} + +function validateConfig(config: unknown): config is TunnelConnection[] { + if (!Array.isArray(config)) return false; + return config.every((item) => { + if (!item || typeof item !== "object") return false; + const tunnel = item as Partial; + const mode = tunnel.mode || tunnel.tunnelType; + return ( + tunnel.scope === "c2s" && + (mode === "local" || mode === "remote" || mode === "dynamic") && + typeof tunnel.sourcePort === "number" && + tunnel.sourcePort >= 1 && + tunnel.sourcePort <= 65535 && + (mode === "dynamic" || + (typeof tunnel.endpointPort === "number" && + tunnel.endpointPort >= 1 && + tunnel.endpointPort <= 65535)) + ); + }); +} + +router.get( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + if (!isNonEmptyString(userId)) { + return res.status(400).json({ error: "Invalid userId" }); + } + + try { + const result = await db + .select() + .from(c2sTunnelPresets) + .where(eq(c2sTunnelPresets.userId, userId)) + .orderBy(asc(c2sTunnelPresets.name)); + res.json(result.map(parsePreset)); + } catch (error) { + authLogger.error("Failed to fetch C2S tunnel presets", error); + res.status(500).json({ error: "Failed to fetch C2S tunnel presets" }); + } + }, +); + +router.post( + "/", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const { name, config, platform, computerName } = req.body; + + if (!isNonEmptyString(userId) || !isNonEmptyString(name)) { + return res.status(400).json({ error: "Preset name is required" }); + } + if (!validateConfig(config)) { + return res + .status(400) + .json({ error: "Invalid C2S tunnel configuration" }); + } + + const trimmedName = name.trim(); + try { + const existing = await db + .select() + .from(c2sTunnelPresets) + .where( + and( + eq(c2sTunnelPresets.userId, userId), + eq(c2sTunnelPresets.name, trimmedName), + ), + ); + if (existing.length > 0) { + return res.status(409).json({ error: "Preset name already exists" }); + } + + const result = await db + .insert(c2sTunnelPresets) + .values({ + userId, + name: trimmedName, + config: JSON.stringify(config), + platform: platform?.trim() || null, + computerName: computerName?.trim() || null, + }) + .returning(); + + databaseLogger.info("C2S tunnel preset created", { + operation: "c2s_tunnel_preset_create", + userId, + presetId: result[0].id, + }); + res.status(201).json(parsePreset(result[0])); + } catch (error) { + authLogger.error("Failed to create C2S tunnel preset", error); + res.status(500).json({ error: "Failed to create C2S tunnel preset" }); + } + }, +); + +router.put( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = Number(req.params.id); + const { name, config, platform, computerName } = req.body; + + if (!isNonEmptyString(userId) || !Number.isInteger(id)) { + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const existing = await db + .select() + .from(c2sTunnelPresets) + .where( + and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)), + ); + if (existing.length === 0) { + return res.status(404).json({ error: "Preset not found" }); + } + + const updateFields: Record = { + updatedAt: sql`CURRENT_TIMESTAMP`, + }; + + if (name !== undefined) { + if (!isNonEmptyString(name)) { + return res.status(400).json({ error: "Preset name is required" }); + } + const trimmedName = name.trim(); + const duplicate = await db + .select() + .from(c2sTunnelPresets) + .where( + and( + eq(c2sTunnelPresets.userId, userId), + eq(c2sTunnelPresets.name, trimmedName), + ), + ); + if (duplicate.some((preset) => preset.id !== id)) { + return res.status(409).json({ error: "Preset name already exists" }); + } + updateFields.name = trimmedName; + } + + if (config !== undefined) { + if (!validateConfig(config)) { + return res + .status(400) + .json({ error: "Invalid C2S tunnel configuration" }); + } + updateFields.config = JSON.stringify(config); + } + if (platform !== undefined) + updateFields.platform = platform?.trim() || null; + if (computerName !== undefined) + updateFields.computerName = computerName?.trim() || null; + + await db + .update(c2sTunnelPresets) + .set(updateFields) + .where( + and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)), + ); + + const updated = await db + .select() + .from(c2sTunnelPresets) + .where(eq(c2sTunnelPresets.id, id)); + res.json(parsePreset(updated[0])); + } catch (error) { + authLogger.error("Failed to update C2S tunnel preset", error); + res.status(500).json({ error: "Failed to update C2S tunnel preset" }); + } + }, +); + +router.delete( + "/:id", + authenticateJWT, + requireDataAccess, + async (req: Request, res: Response) => { + const userId = (req as AuthenticatedRequest).userId; + const id = Number(req.params.id); + + if (!isNonEmptyString(userId) || !Number.isInteger(id)) { + return res.status(400).json({ error: "Invalid request" }); + } + + try { + const existing = await db + .select() + .from(c2sTunnelPresets) + .where( + and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)), + ); + if (existing.length === 0) { + return res.status(404).json({ error: "Preset not found" }); + } + + await db + .delete(c2sTunnelPresets) + .where( + and(eq(c2sTunnelPresets.id, id), eq(c2sTunnelPresets.userId, userId)), + ); + + res.json({ success: true }); + } catch (error) { + authLogger.error("Failed to delete C2S tunnel preset", error); + res.status(500).json({ error: "Failed to delete C2S tunnel preset" }); + } + }, +); + +export default router; diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index 2b06b25a..ef078467 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -27,6 +27,7 @@ import { inArray, } from "drizzle-orm"; import type { Request, Response } from "express"; +import axios from "axios"; import multer from "multer"; import { sshLogger, databaseLogger } from "../../utils/logger.js"; import { SimpleDBOps } from "../../utils/simple-db-ops.js"; @@ -42,6 +43,32 @@ const router = express.Router(); const upload = multer({ storage: multer.memoryStorage() }); +function notifyStatsHostUpdated( + hostId: number, + headers: Pick, + operation: string, +): void { + axios + .post( + "http://localhost:30005/host-updated", + { hostId }, + { + headers: { + Authorization: headers.authorization || "", + Cookie: headers.cookie || "", + }, + timeout: 5000, + }, + ) + .catch((err) => { + sshLogger.warn("Failed to notify stats server of host update", { + operation, + hostId, + error: err instanceof Error ? err.message : String(err), + }); + }); +} + function isNonEmptyString(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } @@ -581,29 +608,12 @@ router.post( name, }); - try { - const axios = (await import("axios")).default; - const statsPort = 30005; - await axios.post( - `http://localhost:${statsPort}/host-updated`, - { hostId: createdHost.id }, - { - headers: { - Authorization: req.headers.authorization || "", - Cookie: req.headers.cookie || "", - }, - timeout: 5000, - }, - ); - } catch (err) { - sshLogger.warn("Failed to notify stats server of new host", { - operation: "host_create", - hostId: createdHost.id as number, - error: err instanceof Error ? err.message : String(err), - }); - } - res.json(resolvedHost); + notifyStatsHostUpdated( + createdHost.id as number, + req.headers, + "host_create", + ); } catch (err) { sshLogger.error("Failed to save SSH host to database", err, { operation: "host_create", @@ -1189,29 +1199,8 @@ router.put( hostId: parseInt(hostId), }); - try { - const axios = (await import("axios")).default; - const statsPort = 30005; - await axios.post( - `http://localhost:${statsPort}/host-updated`, - { hostId: parseInt(hostId) }, - { - headers: { - Authorization: req.headers.authorization || "", - Cookie: req.headers.cookie || "", - }, - timeout: 5000, - }, - ); - } catch (err) { - sshLogger.warn("Failed to notify stats server of host update", { - operation: "host_update", - hostId: parseInt(hostId), - error: err instanceof Error ? err.message : String(err), - }); - } - res.json(resolvedHost); + notifyStatsHostUpdated(parseInt(hostId), req.headers, "host_update"); } catch (err) { sshLogger.error("Failed to update SSH host in database", err, { operation: "host_update", @@ -3319,7 +3308,7 @@ router.patch( .update(hosts) .set({ statsConfig: JSON.stringify(merged) }) .where(and(eq(hosts.id, host.id), eq(hosts.userId, userId))); - } catch (e) { + } catch { errors.push(`Failed to update statsConfig for host ${host.id}`); } } @@ -5346,7 +5335,7 @@ router.post( authenticateJWT, requireDataAccess, async (req: Request, res: Response) => { - const hostId = parseInt(req.params.id); + const hostId = Number.parseInt(String(req.params.id), 10); const userId = (req as AuthenticatedRequest).userId; try { diff --git a/src/backend/database/routes/users.ts b/src/backend/database/routes/users.ts index 7d81cb0b..5511e13c 100644 --- a/src/backend/database/routes/users.ts +++ b/src/backend/database/routes/users.ts @@ -30,6 +30,7 @@ import { networkTopology, dashboardPreferences, opksshTokens, + apiKeys, } from "../db/schema.js"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; @@ -1142,7 +1143,11 @@ router.get("/oidc/callback", async (req, res) => { } } - if (!isFirstUser) { + const oidcAllowRegistration = + (process.env.OIDC_ALLOW_REGISTRATION || "").trim().toLowerCase() === + "true"; + + if (!isFirstUser && !oidcAllowRegistration) { try { const regRow = db.$client .prepare( @@ -1321,10 +1326,6 @@ router.get("/oidc/callback", async (req, res) => { const redirectUrl = new URL(frontendOrigin); redirectUrl.searchParams.set("success", "true"); - if (deviceInfo.type === "desktop" || deviceInfo.type === "mobile") { - redirectUrl.searchParams.set("token", token); - } - const maxAge = deviceInfo.type === "desktop" || deviceInfo.type === "mobile" ? 30 * 24 * 60 * 60 * 1000 @@ -1576,14 +1577,6 @@ router.post("/login", async (req, res) => { username: userRecord.username, }; - const isElectron = - req.headers["x-electron-app"] === "true" || - req.headers["X-Electron-App"] === "true"; - - if (isElectron) { - response.token = token; - } - const timeoutRow = db.$client .prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'") .get() as { value: string } | undefined; @@ -1621,18 +1614,7 @@ router.post("/logout", authenticateJWT, async (req, res) => { const userId = authReq.userId; if (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 { - // expected - token verification may fail during logout - } - } + const sessionId = authReq.sessionId; await authManager.logoutUser(userId, sessionId); authLogger.info("User logged out", { @@ -1693,6 +1675,7 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => { is_oidc: !!user[0].isOidc, is_dual_auth: isDualAuth, totp_enabled: !!user[0].totpEnabled, + data_unlocked: authManager.isUserUnlocked(userId), }); } catch (err) { authLogger.error("Failed to get username", err); @@ -3396,10 +3379,6 @@ router.post("/totp/verify-login", async (req, res) => { deviceInfo: deviceInfo.deviceInfo, }); - const isElectron = - req.headers["x-electron-app"] === "true" || - req.headers["X-Electron-App"] === "true"; - authLogger.success("TOTP verification successful", { operation: "totp_verify_success", userId: userRecord.id, @@ -3416,10 +3395,6 @@ router.post("/totp/verify-login", async (req, res) => { totp_enabled: !!userRecord.totpEnabled, }; - if (isElectron) { - response.token = token; - } - const timeoutRow = db.$client .prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'") .get() as { value: string } | undefined; @@ -3560,9 +3535,14 @@ router.delete("/delete-user", authenticateJWT, async (req, res) => { * description: Failed to unlock data. */ router.post("/unlock-data", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; + const authReq = req as AuthenticatedRequest; + const userId = authReq.userId; const { password } = req.body; + if (!userId) { + return res.status(401).json({ error: "Authentication required" }); + } + if (!password) { return res.status(400).json({ error: "Password is required" }); } @@ -3570,6 +3550,19 @@ router.post("/unlock-data", authenticateJWT, async (req, res) => { try { const unlocked = await authManager.authenticateUser(userId, password); if (unlocked) { + const refreshedSession = + userId && authReq.sessionId + ? await authManager.refreshSessionToken(userId, authReq.sessionId) + : null; + + if (refreshedSession) { + res.cookie( + "jwt", + refreshedSession.token, + authManager.getSecureCookieOptions(req, refreshedSession.maxAge), + ); + } + res.json({ success: true, message: "Data unlocked successfully", @@ -3608,9 +3601,10 @@ router.get("/data-status", authenticateJWT, async (req, res) => { const userId = (req as AuthenticatedRequest).userId; try { + const unlocked = authManager.isUserUnlocked(userId); res.json({ - unlocked: true, - message: "Data is unlocked", + unlocked, + message: unlocked ? "Data is unlocked" : "Data is locked", }); } catch (err) { authLogger.error("Failed to check data status", err, { @@ -3638,7 +3632,9 @@ router.get("/data-status", authenticateJWT, async (req, res) => { * description: Failed to get sessions. */ router.get("/sessions", authenticateJWT, async (req, res) => { - const userId = (req as AuthenticatedRequest).userId; + const authReq = req as AuthenticatedRequest; + const userId = authReq.userId; + const currentSessionId = authReq.sessionId; try { const user = await db.select().from(users).where(eq(users.id, userId)); @@ -3661,8 +3657,16 @@ router.get("/sessions", authenticateJWT, async (req, res) => { .limit(1); return { - ...session, + id: session.id, + userId: session.userId, username: sessionUser[0]?.username || "Unknown", + deviceType: session.deviceType, + deviceInfo: session.deviceInfo, + createdAt: session.createdAt, + expiresAt: session.expiresAt, + lastActiveAt: session.lastActiveAt, + isRevoked: session.isRevoked, + isCurrentSession: session.id === currentSessionId, }; }), ); @@ -3670,7 +3674,19 @@ router.get("/sessions", authenticateJWT, async (req, res) => { return res.json({ sessions: enrichedSessions }); } else { sessionList = await authManager.getUserSessions(userId); - return res.json({ sessions: sessionList }); + return res.json({ + sessions: sessionList.map((session) => ({ + id: session.id, + userId: session.userId, + deviceType: session.deviceType, + deviceInfo: session.deviceInfo, + createdAt: session.createdAt, + expiresAt: session.expiresAt, + lastActiveAt: session.lastActiveAt, + isRevoked: session.isRevoked, + isCurrentSession: session.id === currentSessionId, + })), + }); } } catch (err) { authLogger.error("Failed to get sessions", err); @@ -3812,12 +3828,7 @@ router.post("/sessions/revoke-all", authenticateJWT, async (req, res) => { 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; - } + currentSessionId = (req as AuthenticatedRequest).sessionId; } const revokedCount = await authManager.revokeAllUserSessions( @@ -4205,7 +4216,7 @@ router.post("/unlink-oidc-from-password", authenticateJWT, async (req, res) => { * 500: * description: Failed to get guacamole settings. */ -router.get("/guacamole-settings", async (req, res) => { +router.get("/guacamole-settings", authenticateJWT, async (req, res) => { try { const enabledRow = db.$client .prepare("SELECT value FROM settings WHERE key = 'guac_enabled'") @@ -4305,7 +4316,7 @@ router.patch("/guacamole-settings", authenticateJWT, async (req, res) => { * 200: * description: Current log level. */ -router.get("/log-level", async (_req, res) => { +router.get("/log-level", authenticateJWT, async (_req, res) => { try { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'log_level'") @@ -4374,7 +4385,7 @@ router.patch("/log-level", authenticateJWT, async (req, res) => { * 200: * description: Current session timeout hours. */ -router.get("/session-timeout", async (_req, res) => { +router.get("/session-timeout", authenticateJWT, async (_req, res) => { try { const row = db.$client .prepare("SELECT value FROM settings WHERE key = 'session_timeout_hours'") @@ -4433,4 +4444,209 @@ router.patch("/session-timeout", authenticateJWT, async (req, res) => { } }); +/** + * @openapi + * /users/api-keys: + * post: + * summary: Create an API key (admin only) + * description: Creates a new API key scoped to a specific user. The full token is returned only once. + * tags: + * - API Keys + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - userId + * properties: + * name: + * type: string + * description: Human-readable name for the key. + * userId: + * type: string + * description: ID of the user this key is scoped to. + * expiresAt: + * type: string + * format: date-time + * description: Optional expiration date. Null means the key never expires. + * responses: + * 201: + * description: API key created. Contains the full token (shown only once). + * 400: + * description: Invalid input. + * 403: + * description: Admin access required. + * 404: + * description: Target user not found. + * 500: + * description: Failed to create API key. + */ +router.post("/api-keys", requireAdmin, async (req, res) => { + try { + const { name, userId: targetUserId, expiresAt } = req.body; + + if (typeof name !== "string" || !name.trim()) { + return res.status(400).json({ error: "name is required" }); + } + if (typeof targetUserId !== "string" || !targetUserId.trim()) { + return res.status(400).json({ error: "userId is required" }); + } + + const targetUser = await db + .select() + .from(users) + .where(eq(users.id, targetUserId)) + .limit(1); + if (targetUser.length === 0) { + return res.status(404).json({ error: "Target user not found" }); + } + + let expiresAtValue: string | null = null; + if (expiresAt) { + const parsed = new Date(expiresAt); + if (isNaN(parsed.getTime())) { + return res.status(400).json({ error: "Invalid expiresAt date" }); + } + if (parsed <= new Date()) { + return res + .status(400) + .json({ error: "expiresAt must be in the future" }); + } + expiresAtValue = parsed.toISOString(); + } + + const rawToken = "tmx_" + crypto.randomBytes(32).toString("hex"); + const tokenPrefix = rawToken.substring(0, 12); + const tokenHash = await bcrypt.hash(rawToken, 10); + const keyId = nanoid(); + const now = new Date().toISOString(); + + await db.insert(apiKeys).values({ + id: keyId, + userId: targetUserId, + name: name.trim(), + tokenHash, + tokenPrefix, + createdAt: now, + expiresAt: expiresAtValue, + lastUsedAt: null, + isActive: true, + }); + + const { saveMemoryDatabaseToFile } = await import("../db/index.js"); + await saveMemoryDatabaseToFile(); + + return res.status(201).json({ + id: keyId, + name: name.trim(), + userId: targetUserId, + username: targetUser[0].username, + tokenPrefix, + createdAt: now, + expiresAt: expiresAtValue, + token: rawToken, + }); + } catch (err) { + authLogger.error("Failed to create API key", err); + return res.status(500).json({ error: "Failed to create API key" }); + } +}); + +/** + * @openapi + * /users/api-keys: + * get: + * summary: List all API keys (admin only) + * description: Returns all API keys with associated usernames. Token hashes are never returned. + * tags: + * - API Keys + * responses: + * 200: + * description: List of API keys. + * 403: + * description: Admin access required. + * 500: + * description: Failed to fetch API keys. + */ +router.get("/api-keys", requireAdmin, async (_req, res) => { + try { + const keys = await db + .select({ + id: apiKeys.id, + name: apiKeys.name, + userId: apiKeys.userId, + username: users.username, + tokenPrefix: apiKeys.tokenPrefix, + createdAt: apiKeys.createdAt, + expiresAt: apiKeys.expiresAt, + lastUsedAt: apiKeys.lastUsedAt, + isActive: apiKeys.isActive, + }) + .from(apiKeys) + .leftJoin(users, eq(apiKeys.userId, users.id)) + .orderBy(apiKeys.createdAt); + + return res.json({ apiKeys: keys }); + } catch (err) { + authLogger.error("Failed to list API keys", err); + return res.status(500).json({ error: "Failed to fetch API keys" }); + } +}); + +/** + * @openapi + * /users/api-keys/{keyId}: + * delete: + * summary: Delete an API key (admin only) + * description: Permanently deletes an API key. It can no longer be used to authenticate. + * tags: + * - API Keys + * parameters: + * - in: path + * name: keyId + * required: true + * schema: + * type: string + * description: The ID of the API key to delete. + * responses: + * 200: + * description: API key deleted. + * 403: + * description: Admin access required. + * 404: + * description: API key not found. + * 500: + * description: Failed to delete API key. + */ +router.delete("/api-keys/:keyId", requireAdmin, async (req, res) => { + try { + const keyId = String(req.params.keyId); + + const existing = await db + .select() + .from(apiKeys) + .where(eq(apiKeys.id, keyId)) + .limit(1); + + if (existing.length === 0) { + return res.status(404).json({ error: "API key not found" }); + } + + await db.delete(apiKeys).where(eq(apiKeys.id, keyId)); + + const { saveMemoryDatabaseToFile } = await import("../db/index.js"); + await saveMemoryDatabaseToFile(); + + return res.json({ success: true }); + } catch (err) { + authLogger.error("Failed to delete API key", err, { + keyId: String(req.params.keyId), + }); + return res.status(500).json({ error: "Failed to delete API key" }); + } +}); + export default router; diff --git a/src/backend/guacamole/guacamole-server.ts b/src/backend/guacamole/guacamole-server.ts index 786aa377..a726765b 100644 --- a/src/backend/guacamole/guacamole-server.ts +++ b/src/backend/guacamole/guacamole-server.ts @@ -1,12 +1,8 @@ import GuacamoleLite from "guacamole-lite"; -import { parse as parseUrl } from "url"; import { guacLogger } from "../utils/logger.js"; -import { AuthManager } from "../utils/auth-manager.js"; import { GuacamoleTokenService } from "./token-service.js"; import { getDb } from "../database/db/index.js"; -import type { IncomingMessage } from "http"; -const authManager = AuthManager.getInstance(); const tokenService = GuacamoleTokenService.getInstance(); function parseGuacUrl(url: string): { host: string; port: number } { diff --git a/src/backend/guacamole/routes.ts b/src/backend/guacamole/routes.ts index ddbf4503..84f8c2a6 100644 --- a/src/backend/guacamole/routes.ts +++ b/src/backend/guacamole/routes.ts @@ -6,7 +6,7 @@ import { PermissionManager } from "../utils/permission-manager.js"; import { SimpleDBOps } from "../utils/simple-db-ops.js"; import { getDb } from "../database/db/index.js"; import { hosts } from "../database/db/schema.js"; -import { eq, and } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import type { AuthenticatedRequest } from "../../types/index.js"; const router = express.Router(); @@ -31,7 +31,6 @@ router.use(authManager.createAuthMiddleware()); */ router.post("/token", async (req, res) => { try { - const userId = (req as AuthenticatedRequest).userId; const { type, hostname, port, username, password, domain, ...options } = req.body; @@ -63,10 +62,15 @@ router.post("/token", async (req, res) => { ); break; case "vnc": - token = tokenService.createVncToken(hostname, password, { - port: port || 5900, - ...options, - }); + token = tokenService.createVncToken( + hostname, + username || undefined, + password, + { + port: port || 5900, + ...options, + }, + ); break; case "telnet": token = tokenService.createTelnetToken(hostname, username, password, { @@ -129,7 +133,7 @@ router.post( async (req: express.Request, res: express.Response) => { try { const userId = (req as AuthenticatedRequest).userId!; - const hostId = parseInt(req.params.hostId, 10); + const hostId = Number.parseInt(String(req.params.hostId), 10); if (!hostId || isNaN(hostId)) { return res.status(400).json({ error: "Invalid host ID" }); @@ -206,10 +210,15 @@ router.post( }); break; case "vnc": - token = tokenService.createVncToken(hostname, password, { - port: port || 5900, - ...guacConfig, - }); + token = tokenService.createVncToken( + hostname, + username || undefined, + password, + { + port: port || 5900, + ...guacConfig, + }, + ); break; case "telnet": token = tokenService.createTelnetToken(hostname, username, password, { diff --git a/src/backend/guacamole/token-service.ts b/src/backend/guacamole/token-service.ts index 647325c4..83660cb4 100644 --- a/src/backend/guacamole/token-service.ts +++ b/src/backend/guacamole/token-service.ts @@ -141,6 +141,7 @@ export class GuacamoleTokenService { createVncToken( hostname: string, + username?: string, password?: string, options: Partial = {}, ): string { @@ -149,6 +150,7 @@ export class GuacamoleTokenService { type: "vnc", settings: { hostname, + ...(username ? { username } : {}), password, port: 5900, ...options, diff --git a/src/backend/ssh/docker-console.ts b/src/backend/ssh/docker-console.ts index 1f8fb882..6c93afaf 100644 --- a/src/backend/ssh/docker-console.ts +++ b/src/backend/ssh/docker-console.ts @@ -1,6 +1,5 @@ import { Client as SSHClient } from "ssh2"; import { WebSocketServer, WebSocket } from "ws"; -import { parse as parseUrl } from "url"; import { AuthManager } from "../utils/auth-manager.js"; import { hosts, sshCredentials } from "../database/db/schema.js"; import { and, eq } from "drizzle-orm"; @@ -27,14 +26,18 @@ const wss = new WebSocketServer({ port: 30009, verifyClient: async (info) => { try { - const url = parseUrl(info.req.url || "", true); - let token = url.query.token as string; + let token: string | undefined; + + const cookieHeader = info.req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } if (!token) { - const cookieHeader = info.req.headers.cookie; - if (cookieHeader) { - const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); - if (match) token = decodeURIComponent(match[1]); + const authHeader = info.req.headers.authorization; + if (authHeader?.startsWith("Bearer ")) { + token = authHeader.slice("Bearer ".length); } } @@ -239,14 +242,18 @@ async function createJumpHostChain( } wss.on("connection", async (ws: WebSocket, req) => { - const url = parseUrl(req.url || "", true); - let token = url.query.token as string; + let token: string | undefined; + + const cookieHeader = req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } if (!token) { - const cookieHeader = req.headers.cookie; - if (cookieHeader) { - const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); - if (match) token = decodeURIComponent(match[1]); + const authHeader = req.headers.authorization; + if (authHeader?.startsWith("Bearer ")) { + token = authHeader.slice("Bearer ".length); } } diff --git a/src/backend/ssh/docker.ts b/src/backend/ssh/docker.ts index 79bbf0ba..7bc5d57e 100644 --- a/src/backend/ssh/docker.ts +++ b/src/backend/ssh/docker.ts @@ -971,7 +971,7 @@ app.post("/docker/ssh/connect", async (req, res) => { userId, }); - let errorStage: ConnectionStage = "error"; + let errorStage: ConnectionStage; if ( err.message.includes("ENOTFOUND") || err.message.includes("getaddrinfo") diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index a3f85323..ea4431c0 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -361,6 +361,31 @@ async function createJumpHostChain( } } +// Serializes SSH channel open requests so only one channel negotiation is +// in-flight at a time per session. Once the channel is established the slot +// is released immediately so the next open can proceed; the channels +// themselves remain open concurrently (one exec per command is short-lived, +// the SFTP channel is long-lived but only opened once). +class ChannelOpenSerializer { + private tail: Promise = Promise.resolve(); + + // Enqueue an action that opens a channel. The action runs after the previous + // one completes (success or failure). Returns a promise that resolves with + // the action's result. + run(action: () => Promise): Promise { + const next = this.tail.then( + () => action(), + () => action(), // run even if the previous open failed + ); + // Advance tail past this slot (swallow result so the chain keeps going) + this.tail = next.then( + () => {}, + () => {}, + ); + return next; + } +} + interface SSHSession { client: SSHClient; isConnected: boolean; @@ -369,6 +394,8 @@ interface SSHSession { activeOperations: number; sudoPassword?: string; sftp?: import("ssh2").SFTPWrapper; + sftpPending?: Promise; + channelOpener: ChannelOpenSerializer; poolKey?: string; userId?: string; } @@ -393,9 +420,11 @@ interface PendingTOTPSession { const sshSessions: Record = {}; const pendingTOTPSessions: Record = {}; +// Keyed by "sessionId:path" to prevent concurrent requests for the same path +const activeListRequests: Record = {}; function execWithSudo( - client: SSHClient, + session: SSHSession, command: string, sudoPassword: string, ): Promise<{ stdout: string; stderr: string; code: number }> { @@ -403,7 +432,7 @@ function execWithSudo( const escapedPassword = sudoPassword.replace(/'/g, "'\"'\"'"); const sudoCommand = `echo '${escapedPassword}' | sudo -S ${command} 2>&1`; - client.exec(sudoCommand, (err, stream) => { + execChannel(session, sudoCommand, (err, stream) => { if (err) { resolve({ stdout: "", stderr: err.message, code: 1 }); return; @@ -438,21 +467,76 @@ function getSessionSftp( if (session.sftp) { return Promise.resolve(session.sftp); } - return new Promise((resolve, reject) => { - session.client.sftp((err, sftp) => { - if (err) { - return reject(err); + + // Serialization: if a channel open is already in flight, join it + if (session.sftpPending) { + return session.sftpPending; + } + + const openOnce = (): Promise => + session.channelOpener.run( + () => + new Promise((resolve, reject) => { + session.client.sftp((err, sftp) => { + if (err) return reject(err); + session.sftp = sftp; + sftp.on("error", () => { + session.sftp = undefined; + }); + sftp.on("close", () => { + session.sftp = undefined; + }); + resolve(sftp); + }); + }), + ); + + session.sftpPending = openOnce() + .catch((err: Error) => { + const isChannelFailure = + err.message.toLowerCase().includes("channel open failure") || + err.message.toLowerCase().includes("open failed"); + if (isChannelFailure) { + // Single retry after 500ms for transient server-side rate limiting + return new Promise((resolve, reject) => + setTimeout(() => openOnce().then(resolve, reject), 500), + ); } - session.sftp = sftp; - sftp.on("error", () => { - session.sftp = undefined; - }); - sftp.on("close", () => { - session.sftp = undefined; - }); - resolve(sftp); + return Promise.reject(err); + }) + .finally(() => { + session.sftpPending = undefined; }); - }); + + return session.sftpPending; +} + +// Wraps client.exec through the channel serializer so only one SSH channel +// negotiation is in-flight at a time. The serializer slot is released as soon +// as the channel is established (not when it closes), so channels run +// concurrently once open — we only serialize the *open handshake*. +function execChannel( + session: SSHSession, + command: string, + callback: ( + err: Error | undefined, + stream: import("ssh2").ClientChannel, + ) => void, +): void { + session.channelOpener + .run( + () => + new Promise((resolve, reject) => { + session.client.exec(command, (err, stream) => { + if (err) return reject(err); + resolve(stream); + }); + }), + ) + .then( + (stream) => callback(undefined, stream), + (err: Error) => callback(err, undefined as never), + ); } function cleanupSession(sessionId: string) { @@ -840,11 +924,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { port, username, tryKeyboard: true, - keepaliveInterval: 30000, - keepaliveCountMax: 3, + keepaliveInterval: 10000, + keepaliveCountMax: 5, readyTimeout: 60000, tcpKeepAlive: true, - tcpKeepAliveInitialDelay: 30000, + tcpKeepAliveInitialDelay: 5000, hostVerifier: await SSHHostKeyVerifier.createHostVerifier( hostId, ip, @@ -1108,6 +1192,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { isConnected: true, lastActive: Date.now(), activeOperations: 0, + channelOpener: new ChannelOpenSerializer(), userId, }; scheduleSessionCleanup(sessionId); @@ -1173,7 +1258,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { error: err.message, }); - let errorStage: ConnectionStage = "error"; + let errorStage: ConnectionStage; if ( err.message.includes("ENOTFOUND") || err.message.includes("getaddrinfo") @@ -1752,6 +1837,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => { isConnected: true, lastActive: Date.now(), activeOperations: 0, + channelOpener: new ChannelOpenSerializer(), userId, }; scheduleSessionCleanup(sessionId); @@ -1954,6 +2040,7 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => { isConnected: true, lastActive: Date.now(), activeOperations: 0, + channelOpener: new ChannelOpenSerializer(), userId, }; scheduleSessionCleanup(sessionId); @@ -2048,6 +2135,10 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => { app.post("/ssh/file_manager/ssh/disconnect", (req, res) => { const { sessionId } = req.body; const userId = (req as AuthenticatedRequest).userId; + const session = sshSessions[sessionId]; + if (session && !verifySessionOwnership(session, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } fileLogger.info("File manager disconnection requested", { operation: "file_disconnect_request", sessionId, @@ -2149,7 +2240,7 @@ app.get("/ssh/file_manager/ssh/status", (req, res) => { * 400: * description: Session ID is required or session not found. */ -app.post("/ssh/file_manager/ssh/keepalive", (req, res) => { +app.post("/ssh/file_manager/ssh/keepalive", async (req, res) => { const { sessionId } = req.body; const userId = (req as AuthenticatedRequest).userId; @@ -2173,6 +2264,18 @@ app.post("/ssh/file_manager/ssh/keepalive", (req, res) => { session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); + // Probe the cached SFTP channel. If stale, clear it so the next operation + // opens a fresh one via the serialized getSessionSftp. + if (session.sftp && !session.sftpPending) { + try { + await new Promise((resolve, reject) => { + session.sftp!.stat("/", (err) => (err ? reject(err) : resolve())); + }); + } catch { + session.sftp = undefined; + } + } + res.json({ status: "success", connected: true, @@ -2226,6 +2329,17 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { return res.status(403).json({ error: "Session access denied" }); } + // Drop concurrent requests for the same session+path — each would open + // a new SSH channel and can exceed the server's per-connection channel limit. + const listKey = `${sessionId}:${sshPath}`; + if (activeListRequests[listKey]) { + return res.status(409).json({ error: "List request already in progress" }); + } + activeListRequests[listKey] = true; + res.on("finish", () => { + delete activeListRequests[listKey]; + }); + sshConn.lastActive = Date.now(); sshConn.activeOperations++; const trySFTP = () => { @@ -2327,6 +2441,13 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { fileLogger.warn( `SFTP failed for listFiles, trying fallback: ${err.message}`, ); + const isChannelFailure = + err.message.toLowerCase().includes("channel open failure") || + err.message.toLowerCase().includes("open failed"); + if (isChannelFailure) { + sshConn.isConnected = false; + sshConn.sftp = undefined; + } tryFallbackMethod(); }); } catch (sftpErr: unknown) { @@ -2340,11 +2461,14 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { const tryFallbackMethod = () => { if (!sshConn?.isConnected) { sshConn.activeOperations--; - return res.status(500).json({ error: "SSH session disconnected" }); + return res + .status(503) + .json({ error: "SSH session disconnected", disconnected: true }); } try { const escapedPath = sshPath.replace(/'/g, "'\"'\"'"); - sshConn.client.exec( + execChannel( + sshConn, `command ls -la --color=never '${escapedPath}'`, (err, stream) => { if (err) { @@ -2472,7 +2596,7 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { const escapedPassword = sshConn.sudoPassword!.replace(/'/g, "'\"'\"'"); const sudoCommand = `echo '${escapedPassword}' | sudo -S /bin/ls -la --color=never '${escapedPath}' 2>&1`; - sshConn.client.exec(sudoCommand, (err, stream) => { + execChannel(sshConn, sudoCommand, (err, stream) => { if (err) { sshConn.activeOperations--; fileLogger.error("SSH sudo listFiles error:", err); @@ -2628,6 +2752,7 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => { const sessionId = req.query.sessionId as string; const sshConn = sshSessions[sessionId]; const linkPath = decodeURIComponent(req.query.path as string); + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -2637,6 +2762,10 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!linkPath) { return res.status(400).json({ error: "Link path is required" }); } @@ -2646,7 +2775,7 @@ app.get("/ssh/file_manager/ssh/identifySymlink", (req, res) => { const escapedPath = linkPath.replace(/'/g, "'\"'\"'"); const command = `stat -L -c "%F" '${escapedPath}' && readlink -f '${escapedPath}'`; - sshConn.client.exec(command, (err, stream) => { + execChannel(sshConn, command, (err, stream) => { if (err) { fileLogger.error("SSH identifySymlink error:", err); return res.status(500).json({ error: err.message }); @@ -2722,6 +2851,7 @@ app.get("/ssh/file_manager/ssh/resolvePath", (req, res) => { const sessionId = req.query.sessionId as string; const sshConn = sshSessions[sessionId]; const rawPath = decodeURIComponent(req.query.path as string); + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -2731,21 +2861,26 @@ app.get("/ssh/file_manager/ssh/resolvePath", (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!rawPath) { return res.status(400).json({ error: "Path is required" }); } sshConn.lastActive = Date.now(); - let expandPath = rawPath; - if (expandPath.startsWith("~")) { - expandPath = "$HOME" + expandPath.substring(1); + let command: string; + if (rawPath.startsWith("~")) { + const rest = rawPath.substring(1).replace(/'/g, "'\"'\"'"); + command = `echo ~'${rest}'`; + } else { + const escapedPath = rawPath.replace(/'/g, "'\"'\"'"); + command = `echo '${escapedPath}'`; } - const escapedPath = expandPath.replace(/"/g, '\\"'); - const command = `echo "${escapedPath}"`; - - sshConn.client.exec(command, (err, stream) => { + execChannel(sshConn, command, (err, stream) => { if (err) { fileLogger.error("SSH resolvePath error:", err); return res.status(500).json({ error: err.message }); @@ -2826,6 +2961,10 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!filePath) { return res.status(400).json({ error: "File path is required" }); } @@ -2841,7 +2980,8 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { const MAX_READ_SIZE = 500 * 1024 * 1024; const escapedPath = filePath.replace(/'/g, "'\"'\"'"); - sshConn.client.exec( + execChannel( + sshConn, `stat -c%s '${escapedPath}' 2>/dev/null || wc -c < '${escapedPath}'`, (sizeErr, sizeStream) => { if (sizeErr) { @@ -2899,7 +3039,7 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => { }); } - sshConn.client.exec(`cat '${escapedPath}'`, (err, stream) => { + execChannel(sshConn, `cat '${escapedPath}'`, (err, stream) => { if (err) { fileLogger.error("SSH readFile error:", err); return res.status(500).json({ error: err.message }); @@ -3006,6 +3146,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!filePath) { return res.status(400).json({ error: "File path is required" }); } @@ -3067,7 +3211,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { const escapedPath = filePath.replace(/'/g, "'\"'\"'"); const chmodCommand = `chmod ${permissions} '${escapedPath}' && echo "SUCCESS"`; - sshConn.client.exec(chmodCommand, (err, stream) => { + execChannel(sshConn, chmodCommand, (err, stream) => { if (err) { fileLogger.warn("Failed to restore file permissions after save", { operation: "file_write_restore_permissions", @@ -3170,83 +3314,90 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { } sftp.stat(filePath, (statErr, stats) => { - if (statErr) { - fileLogger.warn( - "Failed to read existing file permissions before save", - { - operation: "file_write_stat", + try { + if (statErr) { + fileLogger.warn( + "Failed to read existing file permissions before save", + { + operation: "file_write_stat", + sessionId, + userId, + path: filePath, + error: statErr.message, + }, + ); + } else if (stats.isFile()) { + preservedMode = stats.mode & 0o7777; + } + + const writeStream = sftp.createWriteStream(filePath); + + let hasError = false; + let hasFinished = false; + let isFinalizing = false; + + const finalizeSuccess = () => { + if (hasError || hasFinished) return; + hasFinished = true; + isFinalizing = false; + fileLogger.success("File written successfully", { + operation: "file_write_success", sessionId, userId, path: filePath, - error: statErr.message, - }, - ); - } else if (stats.isFile()) { - preservedMode = stats.mode & 0o7777; - } - - const writeStream = sftp.createWriteStream(filePath); - - let hasError = false; - let hasFinished = false; - let isFinalizing = false; - - const finalizeSuccess = () => { - if (hasError || hasFinished) return; - hasFinished = true; - isFinalizing = false; - fileLogger.success("File written successfully", { - operation: "file_write_success", - sessionId, - userId, - path: filePath, - bytes: fileBuffer.length, - }); - if (!res.headersSent) { - res.json({ - message: "File written successfully", - path: filePath, - toast: { - type: "success", - message: `File written: ${filePath}`, - }, + bytes: fileBuffer.length, }); + if (!res.headersSent) { + res.json({ + message: "File written successfully", + path: filePath, + toast: { + type: "success", + message: `File written: ${filePath}`, + }, + }); + } + }; + + writeStream.on("error", (streamErr) => { + if (hasError || hasFinished || isFinalizing) return; + hasError = true; + isFinalizing = false; + fileLogger.warn( + `SFTP write failed, trying fallback method: ${streamErr.message}`, + ); + tryFallbackMethod(); + }); + + const finishWrite = () => { + if (hasError || hasFinished || isFinalizing) return; + isFinalizing = true; + restoreOriginalMode(sftp, finalizeSuccess); + }; + + writeStream.on("finish", () => { + finishWrite(); + }); + + writeStream.on("close", () => { + finishWrite(); + }); + + try { + writeStream.write(fileBuffer); + writeStream.end(); + } catch (writeErr) { + if (hasError || hasFinished) return; + hasError = true; + isFinalizing = false; + fileLogger.warn( + `SFTP write operation failed, trying fallback method: ${(writeErr as Error).message}`, + ); + tryFallbackMethod(); } - }; - - writeStream.on("error", (streamErr) => { - if (hasError || hasFinished || isFinalizing) return; - hasError = true; - isFinalizing = false; + } catch (callbackErr) { fileLogger.warn( - `SFTP write failed, trying fallback method: ${streamErr.message}`, - ); - tryFallbackMethod(); - }); - - const finishWrite = () => { - if (hasError || hasFinished || isFinalizing) return; - isFinalizing = true; - restoreOriginalMode(sftp, finalizeSuccess); - }; - - writeStream.on("finish", () => { - finishWrite(); - }); - - writeStream.on("close", () => { - finishWrite(); - }); - - try { - writeStream.write(fileBuffer); - writeStream.end(); - } catch (writeErr) { - if (hasError || hasFinished) return; - hasError = true; - isFinalizing = false; - fileLogger.warn( - `SFTP write operation failed, trying fallback method: ${writeErr.message}`, + `SFTP stat callback error, trying fallback method: ${(callbackErr as Error).message}`, ); tryFallbackMethod(); } @@ -3268,8 +3419,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { const tryFallbackMethod = () => { if (!sshConn?.isConnected) { - sshConn.activeOperations--; - return res.status(500).json({ error: "SSH session disconnected" }); + if (!res.headersSent) { + return res.status(500).json({ error: "SSH session disconnected" }); + } + return; } try { let contentBuffer: Buffer; @@ -3292,7 +3445,7 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { const writeCommand = `echo '${base64Content}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`; - sshConn.client.exec(writeCommand, (err, stream) => { + execChannel(sshConn, writeCommand, (err, stream) => { if (err) { fileLogger.error("Fallback write command failed:", err); if (!res.headersSent) { @@ -3318,6 +3471,10 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { errorData += chunk.toString(); }); + stream.stderr.on("error", (stderrErr) => { + fileLogger.error("Fallback write stderr error:", stderrErr); + }); + stream.on("close", (code) => { if (outputData.includes("SUCCESS")) { restoreOriginalMode(null, () => { @@ -3357,9 +3514,9 @@ app.post("/ssh/file_manager/ssh/writeFile", async (req, res) => { } catch (fallbackErr) { fileLogger.error("Fallback method failed:", fallbackErr); if (!res.headersSent) { - res - .status(500) - .json({ error: `All write methods failed: ${fallbackErr.message}` }); + res.status(500).json({ + error: `All write methods failed: ${(fallbackErr as Error).message}`, + }); } } }; @@ -3411,6 +3568,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!filePath || !fileName || content === undefined) { return res .status(400) @@ -3560,8 +3721,10 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { const tryFallbackMethod = () => { if (!sshConn?.isConnected) { - sshConn.activeOperations--; - return res.status(500).json({ error: "SSH session disconnected" }); + if (!res.headersSent) { + return res.status(500).json({ error: "SSH session disconnected" }); + } + return; } try { let contentBuffer: Buffer; @@ -3606,7 +3769,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { const writeCommand = `echo '${chunks[0]}' | base64 -d > '${escapedPath}' && echo "SUCCESS"`; - sshConn.client.exec(writeCommand, (err, stream) => { + execChannel(sshConn, writeCommand, (err, stream) => { if (err) { fileLogger.error("Fallback upload command failed:", err); if (!res.headersSent) { @@ -3680,7 +3843,7 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => { writeCommand += ` && echo "SUCCESS"`; - sshConn.client.exec(writeCommand, (err, stream) => { + execChannel(sshConn, writeCommand, (err, stream) => { if (err) { fileLogger.error("Chunked fallback upload failed:", err); if (!res.headersSent) { @@ -3797,6 +3960,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 } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -3806,6 +3970,10 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!filePath || !fileName) { return res.status(400).json({ error: "File path and name are required" }); } @@ -3819,7 +3987,7 @@ app.post("/ssh/file_manager/ssh/createFile", async (req, res) => { const createCommand = `touch '${escapedPath}' && echo "SUCCESS" && exit 0`; - sshConn.client.exec(createCommand, (err, stream) => { + execChannel(sshConn, createCommand, (err, stream) => { if (err) { fileLogger.error("SSH createFile error:", err); if (!res.headersSent) { @@ -3939,6 +4107,10 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!folderPath || !folderName) { return res.status(400).json({ error: "Folder path and name are required" }); } @@ -3958,7 +4130,7 @@ app.post("/ssh/file_manager/ssh/createFolder", async (req, res) => { const createCommand = `mkdir -p '${escapedPath}' && echo "SUCCESS" && exit 0`; - sshConn.client.exec(createCommand, (err, stream) => { + execChannel(sshConn, createCommand, (err, stream) => { if (err) { fileLogger.error("SSH createFolder error:", err); if (!res.headersSent) { @@ -4090,6 +4262,10 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!itemPath) { return res.status(400).json({ error: "Item path is required" }); } @@ -4111,7 +4287,7 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { const executeDelete = (useSudo: boolean): Promise => { return new Promise((resolve) => { if (useSudo && sshConn.sudoPassword) { - execWithSudo(sshConn.client, deleteCommand, sshConn.sudoPassword).then( + execWithSudo(sshConn, deleteCommand, sshConn.sudoPassword).then( (result) => { if ( result.code === 0 || @@ -4137,7 +4313,8 @@ app.delete("/ssh/file_manager/ssh/deleteItem", async (req, res) => { return; } - sshConn.client.exec( + execChannel( + sshConn, `${deleteCommand} && echo "SUCCESS"`, (err, stream) => { if (err) { @@ -4259,6 +4436,10 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!oldPath || !newName) { return res .status(400) @@ -4281,7 +4462,7 @@ app.put("/ssh/file_manager/ssh/renameItem", async (req, res) => { const renameCommand = `mv '${escapedOldPath}' '${escapedNewPath}' && echo "SUCCESS" && exit 0`; - sshConn.client.exec(renameCommand, (err, stream) => { + execChannel(sshConn, renameCommand, (err, stream) => { if (err) { fileLogger.error("SSH renameItem error:", err); if (!res.headersSent) { @@ -4412,6 +4593,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 } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId) { return res.status(400).json({ error: "Session ID is required" }); @@ -4421,6 +4603,10 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => { return res.status(400).json({ error: "SSH connection not established" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!oldPath || !newPath) { return res .status(400) @@ -4446,7 +4632,7 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => { } }, 60000); - sshConn.client.exec(moveCommand, (err, stream) => { + execChannel(sshConn, moveCommand, (err, stream) => { if (err) { clearTimeout(commandTimeout); fileLogger.error("SSH moveItem error:", err); @@ -4566,7 +4752,8 @@ app.put("/ssh/file_manager/ssh/moveItem", async (req, res) => { * description: Failed to download file. */ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { - const { sessionId, path: filePath, hostId, userId } = req.body; + const { sessionId, path: filePath, hostId } = req.body; + const userId = (req as AuthenticatedRequest).userId; const downloadStartTime = Date.now(); if (!sessionId || !filePath) { @@ -4597,6 +4784,10 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { .json({ error: "SSH session not found or not connected" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + sshConn.lastActive = Date.now(); scheduleSessionCleanup(sessionId); fileLogger.info("Opening SFTP channel", { @@ -4713,7 +4904,8 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { * description: Failed to copy item. */ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => { - const { sessionId, sourcePath, targetDir, hostId, userId } = req.body; + const { sessionId, sourcePath, targetDir, hostId } = req.body; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId || !sourcePath || !targetDir) { return res.status(400).json({ error: "Missing required parameters" }); @@ -4726,6 +4918,10 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => { .json({ error: "SSH session not found or not connected" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + sshConn.lastActive = Date.now(); scheduleSessionCleanup(sessionId); @@ -4757,7 +4953,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => { } }, 60000); - sshConn.client.exec(copyCommand, (err, stream) => { + execChannel(sshConn, copyCommand, (err, stream) => { if (err) { clearTimeout(commandTimeout); fileLogger.error("SSH copyItem error:", err); @@ -4905,6 +5101,7 @@ app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => { app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => { const { sessionId, filePath } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sshConn || !sshConn.isConnected) { fileLogger.error( @@ -4919,6 +5116,10 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => { return res.status(400).json({ error: "SSH connection not available" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!filePath) { return res.status(400).json({ error: "File path is required" }); } @@ -4927,7 +5128,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => { const checkCommand = `test -x '${escapedPath}' && echo "EXECUTABLE" || echo "NOT_EXECUTABLE"`; - sshConn.client.exec(checkCommand, (checkErr, checkStream) => { + execChannel(sshConn, checkCommand, (checkErr, checkStream) => { if (checkErr) { fileLogger.error("SSH executeFile check error:", checkErr); return res @@ -4947,7 +5148,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => { const executeCommand = `cd "$(dirname '${escapedPath}')" && '${escapedPath}' 2>&1; echo "EXIT_CODE:$?"`; - sshConn.client.exec(executeCommand, (err, stream) => { + execChannel(sshConn, executeCommand, (err, stream) => { if (err) { fileLogger.error("SSH executeFile error:", err); return res.status(500).json({ error: "Failed to execute file" }); @@ -5034,6 +5235,7 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => { app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => { const { sessionId, path, permissions } = req.body; const sshConn = sshSessions[sessionId]; + const userId = (req as AuthenticatedRequest).userId; if (!sshConn || !sshConn.isConnected) { fileLogger.error( @@ -5048,6 +5250,10 @@ app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => { return res.status(400).json({ error: "SSH connection not available" }); } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + if (!path) { return res.status(400).json({ error: "File path is required" }); } @@ -5086,7 +5292,7 @@ app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => { } }, 10000); - sshConn.client.exec(command, (err, stream) => { + execChannel(sshConn, command, (err, stream) => { if (err) { clearTimeout(commandTimeout); fileLogger.error("SSH changePermissions exec error:", err, { @@ -5232,6 +5438,7 @@ app.post("/ssh/file_manager/ssh/changePermissions", async (req, res) => { */ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { const { sessionId, archivePath, extractPath } = req.body; + const userId = (req as AuthenticatedRequest).userId; if (!sessionId || !archivePath) { return res.status(400).json({ error: "Missing required parameters" }); @@ -5242,13 +5449,17 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { return res.status(400).json({ error: "SSH session not connected" }); } + if (!verifySessionOwnership(session, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); const fileName = archivePath.split("/").pop() || ""; const fileExt = fileName.toLowerCase(); - let extractCommand = ""; + let extractCommand: string; const targetPath = extractPath || archivePath.substring(0, archivePath.lastIndexOf("/")); @@ -5290,7 +5501,7 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { command: extractCommand, }); - session.client.exec(extractCommand, (err, stream) => { + execChannel(session, extractCommand, (err, stream) => { if (err) { fileLogger.error("SSH exec error during extract:", err, { operation: "extract_archive", @@ -5438,6 +5649,7 @@ app.post("/ssh/file_manager/ssh/extractArchive", async (req, res) => { */ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { const { sessionId, paths, archiveName, format } = req.body; + const userId = (req as AuthenticatedRequest).userId; if ( !sessionId || @@ -5454,11 +5666,15 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { return res.status(400).json({ error: "SSH session not connected" }); } + if (!verifySessionOwnership(session, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + session.lastActive = Date.now(); scheduleSessionCleanup(sessionId); const compressionFormat = format || "zip"; - let compressCommand = ""; + let compressCommand: string; const firstPath = paths[0]; const workingDir = firstPath.substring(0, firstPath.lastIndexOf("/")) || "/"; @@ -5509,7 +5725,7 @@ app.post("/ssh/file_manager/ssh/compressFiles", async (req, res) => { command: compressCommand, }); - session.client.exec(compressCommand, (err, stream) => { + execChannel(session, compressCommand, (err, stream) => { if (err) { fileLogger.error("SSH exec error during compress:", err, { operation: "compress_files", diff --git a/src/backend/ssh/opkssh-auth.ts b/src/backend/ssh/opkssh-auth.ts index c4d62b41..5b00239a 100644 --- a/src/backend/ssh/opkssh-auth.ts +++ b/src/backend/ssh/opkssh-auth.ts @@ -1,7 +1,6 @@ import { spawn, ChildProcess } from "child_process"; import { randomUUID } from "crypto"; import { WebSocket } from "ws"; -import { IncomingMessage } from "http"; import { OPKSSHBinaryManager } from "../utils/opkssh-binary-manager.js"; import { sshLogger } from "../utils/logger.js"; import { getDb } from "../database/db/index.js"; @@ -13,7 +12,6 @@ import { promises as fs } from "fs"; import path from "path"; import axios from "axios"; import yaml from "js-yaml"; -import { getRequestOrigin } from "../utils/request-origin.js"; const AUTH_TIMEOUT = 60 * 1000; diff --git a/src/backend/ssh/opkssh-cert-auth.ts b/src/backend/ssh/opkssh-cert-auth.ts index bd8e4da8..33bdcb82 100644 --- a/src/backend/ssh/opkssh-cert-auth.ts +++ b/src/backend/ssh/opkssh-cert-auth.ts @@ -4,12 +4,64 @@ // DER → SSH wire format, and patches Protocol.authPK to use the base // algorithm in the signature wrapper (required by OpenSSH's sshkey_check_sigtype). -import type { Client, ConnectConfig } from "ssh2"; +import type { + AnyAuthMethod, + AuthHandlerMiddleware, + AuthenticationType, + Client, + ConnectConfig, + PublicKeyAuthMethod, +} from "ssh2"; interface OPKSSHToken { privateKey: string; sshCert: string; } + +type SignCallback = ( + data: Buffer, + callback: (signature: Buffer) => void, +) => void; + +interface ParsedPrivateKey { + type: string; + sign: (data: Buffer, algo?: string) => Buffer | Error; + getPublicSSH: () => Buffer; + [key: symbol]: unknown; +} + +interface OPKSSHProtocol { + authPK: ( + user: string, + pubKey: ParsedPrivateKey, + keyAlgo: string | undefined, + cbSign?: SignCallback, + ) => unknown; + _kex: { + sessionID: Buffer; + }; + _packetRW: { + write: { + alloc: (payloadLength: number) => Buffer; + allocStart: number; + finalize: (packet: Buffer) => Buffer; + }; + }; + _authsQueue: string[]; + _debug?: (message: string) => void; + _cipher: { + encrypt: (packet: Buffer) => void; + }; +} + +type OPKSSHClient = Client & { + _protocol?: OPKSSHProtocol; +}; + +type OPKSSHNextAuthHandler = ( + authInfo: AuthenticationType | AnyAuthMethod | false, +) => void; + export async function setupOPKSSHCertAuth( config: ConnectConfig, client: Client, @@ -26,7 +78,9 @@ export async function setupOPKSSHCertAuth( if (parsed instanceof Error || !parsed) { throw new Error("Failed to parse OPKSSH private key"); } - const privKey: any = Array.isArray(parsed) ? parsed[0] : parsed; + const privKey = ( + Array.isArray(parsed) ? parsed[0] : parsed + ) as ParsedPrivateKey; // Extract cert type and blob from the stored certificate const certParts = token.sshCert.trim().split(/\s+/); @@ -78,36 +132,43 @@ export async function setupOPKSSHCertAuth( // Set up authHandler to bypass ssh2's cert type rejection let certAuthAttempted = false; - config.authHandler = ( + const authHandler: AuthHandlerMiddleware = ( methodsLeft: string[], _partialSuccess: boolean, - callback: (authInfo: any) => void, + callback, ) => { + const next = callback as OPKSSHNextAuthHandler; if ( !certAuthAttempted && (!methodsLeft || methodsLeft.includes("publickey")) ) { certAuthAttempted = true; - callback({ type: "publickey", username, key: privKey }); + next({ + type: "publickey", + username, + key: privKey as unknown as PublicKeyAuthMethod["key"], + }); } else { - callback(false); + next(false); } }; + config.authHandler = authHandler; // Monkey-patch Protocol.authPK after connect() to fix the signature // wrapper algorithm for cert types. const baseAlgo = certType.replace(/-cert-v\d+@openssh\.com$/, ""); const origConnect = client.connect.bind(client); - (client as any).connect = (cfg: any) => { - origConnect(cfg); - const proto = (client as any)._protocol; - if (!proto) return; + const patchedClient = client as OPKSSHClient; + patchedClient.connect = (cfg: ConnectConfig) => { + const connectedClient = origConnect(cfg); + const proto = patchedClient._protocol; + if (!proto) return connectedClient; const origAuthPK = proto.authPK.bind(proto); proto.authPK = ( user: string, - pubKey: any, + pubKey: ParsedPrivateKey, keyAlgo: string | undefined, - cbSign?: Function, + cbSign?: SignCallback, ) => { const isCertAuth = !!cbSign && pubKey?.type?.includes("-cert-"); if (!isCertAuth) { @@ -232,5 +293,6 @@ export async function setupOPKSSHCertAuth( proto._cipher.encrypt(finalized); }); }; + return connectedClient; }; } diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 1876c795..c0bf3805 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -1088,8 +1088,6 @@ class PollingManager { for (const { host, viewerUserId } of hostsToRefresh) { await this.startPollingForHost(host, { statusOnly: true, viewerUserId }); } - - const skipped = this.pollingConfigs.size - hostsToRefresh.length; } registerViewer(hostId: number, sessionId: string, userId: string): void { @@ -1331,9 +1329,8 @@ async function resolveHostCredentials( const isSharedHost = userId !== ownerId; if (isSharedHost) { - const { SharedCredentialManager } = await import( - "../utils/shared-credential-manager.js" - ); + const { SharedCredentialManager } = + await import("../utils/shared-credential-manager.js"); const sharedCredManager = SharedCredentialManager.getInstance(); const sharedCred = await sharedCredManager.getSharedCredentialForUser( host.id as number, @@ -1552,7 +1549,9 @@ async function buildSshConfig( statsLogger.error( `SSH key format error for host ${host.ip}: ${keyError instanceof Error ? keyError.message : "Unknown error"}`, ); - throw new Error(`Invalid SSH key format for host ${host.ip}`); + throw new Error(`Invalid SSH key format for host ${host.ip}`, { + cause: keyError, + }); } } else if (host.authType === "none") { // no credentials needed @@ -1654,6 +1653,7 @@ function createSshFactory(host: SSHHostWithCredentials): () => Promise { (proxyError instanceof Error ? proxyError.message : "Unknown error"), + { cause: proxyError }, ); } } @@ -2581,7 +2581,7 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => { const errorMessage = error instanceof Error ? error.message : String(error); - let errorStage: ConnectionStage = "error"; + let errorStage: ConnectionStage; if ( errorMessage.includes("ENOTFOUND") || diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 982a54e2..fb775493 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -3,7 +3,6 @@ import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2"; import net from "net"; import dgram from "dgram"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; -import { parse as parseUrl } from "url"; import axios from "axios"; import { getDb } from "../database/db/index.js"; import { sshCredentials, hosts } from "../database/db/schema.js"; @@ -367,14 +366,18 @@ const wss = new WebSocketServer({ port: 30002, verifyClient: async (info) => { try { - const url = parseUrl(info.req.url!, true); - let token = url.query.token as string; + let token: string | undefined; + + const cookieHeader = info.req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } if (!token) { - const cookieHeader = info.req.headers.cookie; - if (cookieHeader) { - const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); - if (match) token = decodeURIComponent(match[1]); + const authHeader = info.req.headers.authorization; + if (authHeader?.startsWith("Bearer ")) { + token = authHeader.slice("Bearer ".length); } } @@ -414,14 +417,18 @@ wss.on("connection", async (ws: WebSocket, req) => { let sessionId: string | undefined; try { - const url = parseUrl(req.url!, true); - let token = url.query.token as string; + let token: string | undefined; + + const cookieHeader = req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) token = decodeURIComponent(match[1]); + } if (!token) { - const cookieHeader = req.headers.cookie; - if (cookieHeader) { - const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); - if (match) token = decodeURIComponent(match[1]); + const authHeader = req.headers.authorization; + if (authHeader?.startsWith("Bearer ")) { + token = authHeader.slice("Bearer ".length); } } @@ -487,6 +494,8 @@ wss.on("connection", async (ws: WebSocket, req) => { let isConnecting = false; let isConnected = false; let isCleaningUp = false; + let cwdPending = false; + let cwdBuffer = ""; let isShellInitializing = false; let warpgateAuthPromptSent = false; let warpgateAuthTimeout: NodeJS.Timeout | null = null; @@ -590,6 +599,22 @@ wss.on("connection", async (ws: WebSocket, req) => { connectData.hostConfig.userId = userId; } handleConnectToHost(connectData).catch((error) => { + const errMsg = + error instanceof Error ? error.message : "Unknown error"; + if ( + errMsg.includes("Cannot parse privateKey") && + errMsg.includes("no passphrase") + ) { + isAwaitingAuthCredentials = true; + ws.send( + JSON.stringify({ + type: "passphrase_required", + message: + "The SSH key is encrypted. Please enter the passphrase to unlock it.", + }), + ); + return; + } sshLogger.error("Failed to connect to host", error, { operation: "ssh_connect", userId, @@ -599,9 +624,7 @@ wss.on("connection", async (ws: WebSocket, req) => { ws.send( JSON.stringify({ type: "error", - message: - "Failed to connect to host: " + - (error instanceof Error ? error.message : "Unknown error"), + message: "Failed to connect to host: " + errMsg, }), ); }); @@ -729,6 +752,21 @@ wss.on("connection", async (ws: WebSocket, req) => { sshStream = null; break; + case "get_cwd": { + const activeStream = + sessionManager.getSession(currentSessionId)?.sshStream ?? sshStream; + if (!activeStream) { + ws.send(JSON.stringify({ type: "cwd", path: "/" })); + break; + } + cwdPending = true; + cwdBuffer = ""; + // Split the sentinel across shell variables so the echoed command + // itself never contains "TERMIX_CWD:" — only the output line does. + activeStream.write('a=TERMIX_CWD; echo "$a:$(pwd)"\r'); + break; + } + case "input": { const inputData = data as string; const inputStream = @@ -898,6 +936,8 @@ wss.on("connection", async (ws: WebSocket, req) => { credentialsData.hostConfig.key = credentialsData.sshKey; credentialsData.hostConfig.keyPassword = credentialsData.keyPassword; credentialsData.hostConfig.authType = "key"; + } else if (credentialsData.keyPassword) { + credentialsData.hostConfig.keyPassword = credentialsData.keyPassword; } isAwaitingAuthCredentials = false; @@ -916,6 +956,22 @@ wss.on("connection", async (ws: WebSocket, req) => { }; handleConnectToHost(reconnectData).catch((error) => { + const errMsg = + error instanceof Error ? error.message : "Unknown error"; + if ( + errMsg.includes("Cannot parse privateKey") && + errMsg.includes("no passphrase") + ) { + isAwaitingAuthCredentials = true; + ws.send( + JSON.stringify({ + type: "passphrase_required", + message: + "The SSH key is encrypted. Please enter the passphrase to unlock it.", + }), + ); + return; + } sshLogger.error("Failed to reconnect with credentials", error, { operation: "ssh_reconnect_with_credentials", userId, @@ -925,9 +981,7 @@ wss.on("connection", async (ws: WebSocket, req) => { ws.send( JSON.stringify({ type: "error", - message: - "Failed to connect with provided credentials: " + - (error instanceof Error ? error.message : "Unknown error"), + message: "Failed to connect with provided credentials: " + errMsg, }), ); }); @@ -1196,7 +1250,7 @@ wss.on("connection", async (ws: WebSocket, req) => { username: resolvedHost.username || username, password: resolvedHost.password, key: resolvedHost.key, - keyPassword: resolvedHost.keyPassword, + keyPassword: keyPassword || resolvedHost.keyPassword, keyType: resolvedHost.keyType, authType: resolvedHost.authType, }; @@ -1222,7 +1276,8 @@ wss.on("connection", async (ws: WebSocket, req) => { username: resolvedHost.username || username, password: resolvedHost.password, key: resolvedHost.key, - keyPassword: resolvedHost.keyPassword, + // Preserve user-supplied keyPassword (e.g. from passphrase dialog) over the empty DB value + keyPassword: keyPassword || resolvedHost.keyPassword, keyType: resolvedHost.keyType, authType: resolvedHost.authType, }; @@ -1439,9 +1494,47 @@ wss.on("connection", async (ws: WebSocket, req) => { const boundSessionId = currentSessionId; + const CWD_SENTINEL = "TERMIX_CWD:"; + stream.on("data", (data: Buffer) => { try { - const utf8String = data.toString("utf-8"); + let utf8String = data.toString("utf-8"); + + if (cwdPending) { + cwdBuffer += utf8String; + const sentinelIdx = cwdBuffer.indexOf(CWD_SENTINEL); + if (sentinelIdx !== -1) { + const afterSentinel = cwdBuffer.slice( + sentinelIdx + CWD_SENTINEL.length, + ); + const newlineIdx = afterSentinel.search(/[\r\n]/); + if (newlineIdx !== -1) { + const cwd = + afterSentinel.slice(0, newlineIdx).trim() || "/"; + cwdPending = false; + // Strip the sentinel line from output sent to terminal + const beforeSentinel = cwdBuffer.slice(0, sentinelIdx); + const afterNewline = afterSentinel.slice(newlineIdx); + utf8String = beforeSentinel + afterNewline; + cwdBuffer = ""; + const attachedWs = + sessionManager.getSession(boundSessionId)?.attachedWs ?? + ws; + if (attachedWs.readyState === WebSocket.OPEN) { + attachedWs.send( + JSON.stringify({ type: "cwd", path: cwd }), + ); + } + } else { + return; + } + } else { + return; + } + } + + if (!utf8String) return; + const session = sessionManager.getSession(boundSessionId); if (session) { sessionManager.bufferOutput(boundSessionId!, utf8String); @@ -1472,15 +1565,24 @@ wss.on("connection", async (ws: WebSocket, req) => { } }); - stream.on("close", () => { + stream.on("close", (code: number | null) => { const session = sessionManager.getSession(boundSessionId); if (session?.attachedWs?.readyState === WebSocket.OPEN) { - session.attachedWs.send( - JSON.stringify({ - type: "disconnected", - message: "Connection lost", - }), - ); + if (code != null) { + session.attachedWs.send( + JSON.stringify({ + type: "session_ended", + code, + }), + ); + } else { + session.attachedWs.send( + JSON.stringify({ + type: "disconnected", + message: "Connection lost", + }), + ); + } } if (boundSessionId) { sessionManager.destroySession(boundSessionId); @@ -1740,6 +1842,31 @@ wss.on("connection", async (ws: WebSocket, req) => { return; } + if ( + err.message.includes("Cannot parse privateKey") && + err.message.includes("no passphrase") + ) { + sendLog( + "auth", + "error", + "SSH key is encrypted but no passphrase was provided", + ); + isAwaitingAuthCredentials = true; + if (currentSessionId) { + sessionManager.destroySession(currentSessionId); + currentSessionId = null; + } + cleanupAuthState(connectionTimeout); + ws.send( + JSON.stringify({ + type: "passphrase_required", + message: + "The SSH key is encrypted. Please enter the passphrase to unlock it.", + }), + ); + return; + } + if ( authMethodNotAvailable && resolvedCredentials.authType === "none" && @@ -1913,7 +2040,7 @@ wss.on("connection", async (ws: WebSocket, req) => { }), ); } - } else if (!sshStream) { + } else { if (ws.readyState === WebSocket.OPEN) { ws.send( JSON.stringify({ @@ -2102,6 +2229,10 @@ wss.on("connection", async (ws: WebSocket, req) => { if (resolvedCredentials.keyPassword) { connectConfig.passphrase = resolvedCredentials.keyPassword; } + + if (resolvedCredentials.password) { + connectConfig.password = resolvedCredentials.password; + } } catch (keyError) { sshLogger.error("SSH key format error: " + keyError.message); ws.send( @@ -2191,7 +2322,7 @@ wss.on("connection", async (ws: WebSocket, req) => { { operation: "port_knock", hostId: hostConfig.id }, ); await performPortKnocking(hostConfig.ip, hostConfig.portKnockSequence); - } catch (err) { + } catch { sshLogger.warn("Port knocking failed, attempting connection anyway", { operation: "port_knock", hostId: hostConfig.id, diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 14fcddee..d9c1a736 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -1,9 +1,12 @@ import express, { type Response } from "express"; +import { createServer, type IncomingMessage } from "http"; import { createCorsMiddleware } from "../utils/cors-config.js"; import cookieParser from "cookie-parser"; -import { Client } from "ssh2"; +import { Client, type ClientChannel } from "ssh2"; +import { WebSocketServer, type WebSocket } from "ws"; import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js"; import { ChildProcess } from "child_process"; +import type { Duplex } from "stream"; import axios from "axios"; import { getDb } from "../database/db/index.js"; import { sshCredentials } from "../database/db/schema.js"; @@ -50,10 +53,85 @@ const countdownIntervals = new Map(); const retryExhaustedTunnels = new Set(); const cleanupInProgress = new Set(); const tunnelConnecting = new Set(); +const lastTunnelErrors = new Map(); +const lastTunnelErrorTypes = new Map(); const tunnelConfigs = new Map(); const activeTunnelProcesses = new Map(); const pendingTunnelOperations = new Map>(); +const tunnelStatusClients = new Set(); +let c2sRemoteStreamCounter = 0; +const C2S_WS_HIGH_WATERMARK = 1024 * 1024; +const C2S_WS_LOW_WATERMARK = 256 * 1024; +const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024; + +type ActiveTunnelRuntime = { + sourceClient: Client; + endpointClient?: Client; + bindClient?: Client; + bindHost?: string; + bindPort?: number; + close: () => void; +}; + +const activeTunnelRuntimes = new Map(); + +type C2SOpenMessage = { + type: "open" | "test"; + tunnelConfig?: Partial; + targetHost?: string; + targetPort?: number; +}; + +function extractRequestToken(req: IncomingMessage): string | undefined { + const cookieHeader = req.headers.cookie; + if (cookieHeader) { + const match = cookieHeader.match(/(?:^|;\s*)jwt=([^;]+)/); + if (match) return decodeURIComponent(match[1]); + } + + const authHeader = req.headers.authorization; + if (authHeader?.startsWith("Bearer ")) { + return authHeader.slice("Bearer ".length); + } + + return undefined; +} + +function sendC2SError(ws: WebSocket, message: string): void { + if (ws.readyState === 1) { + ws.send(JSON.stringify({ type: "error", error: message })); + } +} + +function describeC2SRelayError(error: unknown): string { + const message = error instanceof Error ? error.message : String(error); + const lowerMessage = message.toLowerCase(); + + if ( + lowerMessage.includes("administratively prohibited") || + lowerMessage.includes("forwarding disabled") || + lowerMessage.includes("open failed") + ) { + return `SSH forwarding was rejected by the endpoint server: ${message}`; + } + if ( + lowerMessage.includes("address already in use") || + lowerMessage.includes("unable to bind") || + lowerMessage.includes("bind") + ) { + return `Remote port is not available on the endpoint server: ${message}`; + } + if ( + lowerMessage.includes("name or service not known") || + lowerMessage.includes("enotfound") || + lowerMessage.includes("econnrefused") + ) { + return `Tunnel target is not reachable from the endpoint host: ${message}`; + } + + return message || "Failed to open relay"; +} function broadcastTunnelStatus(tunnelName: string, status: TunnelStatus): void { if ( @@ -63,14 +141,42 @@ function broadcastTunnelStatus(tunnelName: string, status: TunnelStatus): void { return; } + const nextStatus = { ...status }; + if ( retryExhaustedTunnels.has(tunnelName) && - status.status === CONNECTION_STATES.FAILED + nextStatus.status === CONNECTION_STATES.FAILED ) { - status.reason = "Max retries exhausted"; + const previousReason = lastTunnelErrors.get(tunnelName); + nextStatus.reason = previousReason + ? `Max retries exhausted: ${previousReason}` + : "Max retries exhausted"; } - connectionStatus.set(tunnelName, status); + if (nextStatus.status === CONNECTION_STATES.FAILED && nextStatus.reason) { + lastTunnelErrors.set(tunnelName, nextStatus.reason); + if (nextStatus.errorType) { + lastTunnelErrorTypes.set(tunnelName, nextStatus.errorType); + } + } else if ( + (nextStatus.status === CONNECTION_STATES.CONNECTING || + nextStatus.status === CONNECTION_STATES.RETRYING || + nextStatus.status === CONNECTION_STATES.WAITING) && + !nextStatus.reason + ) { + nextStatus.reason = lastTunnelErrors.get(tunnelName); + nextStatus.errorType = lastTunnelErrorTypes.get(tunnelName); + } else if ( + nextStatus.status === CONNECTION_STATES.CONNECTED || + (nextStatus.status === CONNECTION_STATES.DISCONNECTED && + nextStatus.manualDisconnect) + ) { + lastTunnelErrors.delete(tunnelName); + lastTunnelErrorTypes.delete(tunnelName); + } + + connectionStatus.set(tunnelName, nextStatus); + broadcastTunnelStatusSnapshot(); } function getAllTunnelStatus(): Record { @@ -81,6 +187,22 @@ function getAllTunnelStatus(): Record { return tunnelStatus; } +function sendTunnelStatusSnapshot(res: Response): void { + try { + res.write( + `event: statuses\ndata: ${JSON.stringify(getAllTunnelStatus())}\n\n`, + ); + } catch { + tunnelStatusClients.delete(res); + } +} + +function broadcastTunnelStatusSnapshot(): void { + for (const client of tunnelStatusClients) { + sendTunnelStatusSnapshot(client); + } +} + function classifyError(errorMessage: string): ErrorType { if (!errorMessage) return "UNKNOWN"; @@ -211,7 +333,19 @@ async function cleanupTunnelResources( cleanupInProgress.add(tunnelName); const tunnelConfig = tunnelConfigs.get(tunnelName); - if (tunnelConfig) { + const runtime = activeTunnelRuntimes.get(tunnelName); + if (runtime) { + try { + runtime.close(); + } catch (error) { + tunnelLogger.error("Error while closing managed tunnel runtime", error, { + operation: "managed_tunnel_cleanup", + tunnelName, + }); + } + activeTunnelRuntimes.delete(tunnelName); + cleanupInProgress.delete(tunnelName); + } else if (tunnelConfig) { await new Promise((resolve) => { killRemoteTunnelByMarker(tunnelConfig, tunnelName, (err) => { cleanupInProgress.delete(tunnelName); @@ -300,6 +434,8 @@ async function cleanupTunnelResources( function resetRetryState(tunnelName: string): void { retryCounters.delete(tunnelName); retryExhaustedTunnels.delete(tunnelName); + lastTunnelErrors.delete(tunnelName); + lastTunnelErrorTypes.delete(tunnelName); cleanupInProgress.delete(tunnelName); tunnelConnecting.delete(tunnelName); @@ -491,12 +627,894 @@ function setupPingInterval(tunnelName: string): void { verificationTimers.set(pingKey, pingInterval); } +function getTunnelMode( + tunnelConfig: TunnelConfig, +): "local" | "remote" | "dynamic" { + return tunnelConfig.mode || tunnelConfig.tunnelType || "remote"; +} + +function getTunnelScope(tunnelConfig: TunnelConfig): "s2s" | "c2s" { + return tunnelConfig.scope || "s2s"; +} + +function getTunnelBindHost(tunnelConfig: TunnelConfig): string { + return tunnelConfig.bindHost || "127.0.0.1"; +} + +function getManagedTunnelAlgorithms() { + return { + kex: [ + "curve25519-sha256", + "curve25519-sha256@libssh.org", + "ecdh-sha2-nistp521", + "ecdh-sha2-nistp384", + "ecdh-sha2-nistp256", + "diffie-hellman-group-exchange-sha256", + "diffie-hellman-group14-sha256", + "diffie-hellman-group14-sha1", + "diffie-hellman-group-exchange-sha1", + "diffie-hellman-group1-sha1", + ], + serverHostKey: [ + "ssh-ed25519", + "ecdsa-sha2-nistp521", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp256", + "rsa-sha2-512", + "rsa-sha2-256", + "ssh-rsa", + "ssh-dss", + ], + cipher: SSH_ALGORITHMS.cipher, + hmac: [ + "hmac-sha2-512-etm@openssh.com", + "hmac-sha2-256-etm@openssh.com", + "hmac-sha2-512", + "hmac-sha2-256", + "hmac-sha1", + "hmac-md5", + ], + compress: ["none", "zlib@openssh.com", "zlib"], + }; +} + +function applyAuthOptions( + connOptions: Record, + credentials: { + password?: string; + sshKey?: string; + keyPassword?: string; + keyType?: string; + authMethod?: string; + }, +): void { + if (credentials.authMethod === "key" && credentials.sshKey) { + const cleanKey = credentials.sshKey + .trim() + .replace(/\r\n/g, "\n") + .replace(/\r/g, "\n"); + connOptions.privateKey = Buffer.from(cleanKey, "utf8"); + if (credentials.keyPassword) { + connOptions.passphrase = credentials.keyPassword; + } + if (credentials.keyType && credentials.keyType !== "auto") { + connOptions.privateKeyType = credentials.keyType; + } + } else { + connOptions.password = credentials.password; + } +} + +function connectClient( + connOptions: Record, + tunnelName: string, + role: "source" | "endpoint", +): Promise { + return new Promise((resolve, reject) => { + const client = new Client(); + let settled = false; + client.once("ready", () => { + settled = true; + resolve(client); + }); + client.once("error", (error) => { + if (!settled) { + reject(error); + return; + } + tunnelLogger.error("Managed tunnel SSH client error", error, { + operation: "managed_tunnel_client_error", + tunnelName, + role, + }); + }); + client.connect(connOptions); + }); +} + +function forwardOut( + client: Client, + targetHost: string, + targetPort: number, + tunnelName?: string, +): Promise { + return new Promise((resolve, reject) => { + client.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream) => { + if (err) { + if (tunnelName) { + tunnelLogger.error("Managed tunnel forwardOut failed", err, { + operation: "managed_tunnel_forward_out_failed", + tunnelName, + targetHost, + targetPort, + }); + } + reject(err); + return; + } + resolve(stream); + }); + }); +} + +function bindForwardIn( + client: Client, + bindHost: string, + bindPort: number, +): Promise { + return new Promise((resolve, reject) => { + client.forwardIn(bindHost, bindPort, (err, actualPort) => { + if (err) { + reject(err); + return; + } + resolve(actualPort || bindPort); + }); + }); +} + +function unbindForwardIn( + client: Client, + bindHost: string, + bindPort: number, +): void { + try { + client.unforwardIn(bindHost, bindPort, (err) => { + if (err) { + tunnelLogger.warn("Failed to unbind managed tunnel listener", { + operation: "managed_tunnel_unforward_failed", + bindHost, + bindPort, + error: err.message, + }); + } + }); + } catch { + // The connection may already be gone. + } +} + +function pipeTunnelStreams( + inbound: Duplex, + outboundPromise: Promise, + tunnelName: string, +): void { + outboundPromise + .then((outbound) => { + inbound.pipe(outbound).pipe(inbound); + inbound.on("error", () => outbound.destroy()); + outbound.on("error", () => inbound.destroy()); + }) + .catch((error) => { + tunnelLogger.error( + "Failed to open managed tunnel outbound stream", + error, + { + operation: "managed_tunnel_outbound_failed", + tunnelName, + }, + ); + inbound.destroy(); + }); +} + +function parseSocksAddress(buffer: Buffer): { + address: string; + port: number; + bytesRead: number; +} | null { + if (buffer.length < 7 || buffer[0] !== 0x05 || buffer[1] !== 0x01) { + return null; + } + + const addressType = buffer[3]; + let offset = 4; + let address: string; + + if (addressType === 0x01) { + if (buffer.length < offset + 4 + 2) return null; + address = Array.from(buffer.subarray(offset, offset + 4)).join("."); + offset += 4; + } else if (addressType === 0x03) { + const len = buffer[offset]; + offset += 1; + if (buffer.length < offset + len + 2) return null; + address = buffer.subarray(offset, offset + len).toString("utf8"); + offset += len; + } else if (addressType === 0x04) { + if (buffer.length < offset + 16 + 2) return null; + const parts: string[] = []; + for (let i = 0; i < 16; i += 2) { + parts.push(buffer.readUInt16BE(offset + i).toString(16)); + } + address = parts.join(":"); + offset += 16; + } else { + return null; + } + + const port = buffer.readUInt16BE(offset); + return { address, port, bytesRead: offset + 2 }; +} + +function handleSocks5Connect( + inbound: Duplex, + openOutbound: (host: string, port: number) => Promise, + tunnelName: string, +): void { + let buffer = Buffer.alloc(0); + let stage: "greeting" | "connect" | "piping" = "greeting"; + + const fail = (code = 0x01) => { + if (!inbound.destroyed) { + inbound.write(Buffer.from([0x05, code, 0x00, 0x01, 0, 0, 0, 0, 0, 0])); + inbound.destroy(); + } + }; + + const onData = (chunk: Buffer) => { + buffer = Buffer.concat([buffer, chunk]); + + if (stage === "greeting") { + if (buffer.length < 2) return; + if (buffer[0] !== 0x05) { + fail(0x01); + return; + } + const methodsLength = buffer[1]; + if (buffer.length < 2 + methodsLength) return; + inbound.write(Buffer.from([0x05, 0x00])); + buffer = buffer.subarray(2 + methodsLength); + stage = "connect"; + } + + if (stage === "connect") { + const parsed = parseSocksAddress(buffer); + if (!parsed) return; + stage = "piping"; + inbound.off("data", onData); + const remainder = buffer.subarray(parsed.bytesRead); + openOutbound(parsed.address, parsed.port) + .then((outbound) => { + inbound.write( + Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]), + ); + if (remainder.length > 0) { + outbound.write(remainder); + } + inbound.pipe(outbound).pipe(inbound); + inbound.on("error", () => outbound.destroy()); + outbound.on("error", () => inbound.destroy()); + }) + .catch((error) => { + tunnelLogger.error("SOCKS5 tunnel connect failed", error, { + operation: "managed_tunnel_socks_connect_failed", + tunnelName, + targetHost: parsed.address, + targetPort: parsed.port, + }); + fail(0x05); + }); + } + }; + + inbound.on("data", onData); + inbound.on("error", () => inbound.destroy()); +} + +async function connectEndpointThroughSource( + sourceClient: Client, + tunnelConfig: TunnelConfig, + endpointCredentials: { + password?: string; + sshKey?: string; + keyPassword?: string; + keyType?: string; + authMethod?: string; + }, +): Promise { + const endpointSock = await forwardOut( + sourceClient, + tunnelConfig.endpointIP, + tunnelConfig.endpointSSHPort, + tunnelConfig.name, + ); + const endpointOptions: Record = { + sock: endpointSock, + username: tunnelConfig.endpointUsername, + tryKeyboard: true, + keepaliveInterval: 30000, + keepaliveCountMax: 3, + readyTimeout: 60000, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 30000, + algorithms: getManagedTunnelAlgorithms(), + }; + + applyAuthOptions(endpointOptions, endpointCredentials); + return connectClient(endpointOptions, tunnelConfig.name, "endpoint"); +} + +function resolveS2SLocalTargetHost(tunnelConfig: TunnelConfig): string { + const targetHost = tunnelConfig.targetHost?.trim(); + + if ( + !targetHost || + targetHost === tunnelConfig.endpointHost || + targetHost === tunnelConfig.hostName + ) { + return "127.0.0.1"; + } + + return targetHost; +} + +async function establishManagedS2STunnel( + sourceClient: Client, + tunnelConfig: TunnelConfig, + endpointCredentials: { + password?: string; + sshKey?: string; + keyPassword?: string; + keyType?: string; + authMethod?: string; + }, +): Promise { + const tunnelName = tunnelConfig.name; + const mode = getTunnelMode(tunnelConfig); + const bindHost = getTunnelBindHost(tunnelConfig); + const endpointClient = await connectEndpointThroughSource( + sourceClient, + tunnelConfig, + endpointCredentials, + ); + + const bindClient = mode === "remote" ? endpointClient : sourceClient; + const outboundClient = mode === "remote" ? sourceClient : endpointClient; + const bindPort = + mode === "remote" ? tunnelConfig.endpointPort : tunnelConfig.sourcePort; + const staticTargetHost = + mode === "remote" + ? tunnelConfig.targetHost || "127.0.0.1" + : resolveS2SLocalTargetHost(tunnelConfig); + const staticTargetPort = + mode === "remote" ? tunnelConfig.sourcePort : tunnelConfig.endpointPort; + + tunnelLogger.info("Managed S2S tunnel route resolved", { + operation: "managed_tunnel_route_resolved", + tunnelName, + mode, + bindHost, + bindPort, + targetHost: staticTargetHost, + targetPort: staticTargetPort, + endpointHost: tunnelConfig.endpointHost, + endpointIP: tunnelConfig.endpointIP, + }); + + const actualPort = await bindForwardIn(bindClient, bindHost, bindPort); + + const tcpHandler = ( + info: { + destIP: string; + destPort: number; + srcIP: string; + srcPort: number; + }, + accept: () => ClientChannel, + reject: () => void, + ) => { + if (info.destPort !== actualPort) { + reject(); + return; + } + + const inbound = accept(); + if (mode === "dynamic") { + handleSocks5Connect( + inbound, + (host, port) => forwardOut(outboundClient, host, port), + tunnelName, + ); + return; + } + + pipeTunnelStreams( + inbound, + forwardOut( + outboundClient, + staticTargetHost, + staticTargetPort, + tunnelName, + ), + tunnelName, + ); + }; + + bindClient.on("tcp connection", tcpHandler); + + const close = () => { + bindClient.off("tcp connection", tcpHandler); + unbindForwardIn(bindClient, bindHost, actualPort); + try { + endpointClient.end(); + } catch { + // expected during shutdown + } + try { + sourceClient.end(); + } catch { + // expected during shutdown + } + }; + + activeTunnelRuntimes.set(tunnelName, { + sourceClient, + endpointClient, + bindClient, + bindHost, + bindPort: actualPort, + close, + }); + + activeTunnels.set(tunnelName, sourceClient); +} + +async function resolveC2STunnelSource( + tunnelConfig: Partial, + userId: string, +): Promise { + if (!tunnelConfig.sourceHostId) { + throw new Error("Endpoint SSH host is required"); + } + + const accessInfo = await permissionManager.canAccessHost( + userId, + tunnelConfig.sourceHostId, + "read", + ); + if (!accessInfo.hasAccess) { + throw new Error("Access denied to this host"); + } + + const { resolveHostById } = await import("./host-resolver.js"); + const resolvedHost = await resolveHostById(tunnelConfig.sourceHostId, userId); + if (!resolvedHost) { + throw new Error("Endpoint SSH host not found"); + } + + return { + name: tunnelConfig.name || `c2s:${tunnelConfig.sourceHostId}`, + scope: "c2s", + mode: tunnelConfig.mode || "local", + tunnelType: + tunnelConfig.tunnelType || + (tunnelConfig.mode === "remote" ? "remote" : "local"), + bindHost: tunnelConfig.bindHost, + targetHost: tunnelConfig.targetHost || "127.0.0.1", + sourceHostId: resolvedHost.id || tunnelConfig.sourceHostId, + tunnelIndex: tunnelConfig.tunnelIndex || 0, + requestingUserId: userId, + hostName: + resolvedHost.name || `${resolvedHost.username}@${resolvedHost.ip}`, + sourceIP: resolvedHost.ip, + sourceSSHPort: resolvedHost.port, + sourceUsername: resolvedHost.username, + sourcePassword: resolvedHost.password, + sourceAuthMethod: resolvedHost.authType, + sourceSSHKey: resolvedHost.key, + sourceKeyPassword: resolvedHost.keyPassword, + sourceKeyType: resolvedHost.keyType, + sourceCredentialId: resolvedHost.credentialId, + sourceUserId: resolvedHost.userId, + endpointIP: tunnelConfig.endpointIP || resolvedHost.ip, + endpointSSHPort: tunnelConfig.endpointSSHPort || resolvedHost.port, + endpointUsername: resolvedHost.username, + endpointHost: + tunnelConfig.endpointHost || resolvedHost.name || resolvedHost.ip, + endpointAuthMethod: resolvedHost.authType, + endpointSSHKey: resolvedHost.key, + endpointKeyPassword: resolvedHost.keyPassword, + endpointKeyType: resolvedHost.keyType, + endpointCredentialId: resolvedHost.credentialId, + endpointUserId: resolvedHost.userId, + sourcePort: Number(tunnelConfig.sourcePort) || 0, + endpointPort: Number(tunnelConfig.endpointPort) || 0, + maxRetries: Number(tunnelConfig.maxRetries) || 0, + retryInterval: Number(tunnelConfig.retryInterval) || 0, + autoStart: Boolean(tunnelConfig.autoStart), + isPinned: Boolean(resolvedHost.pin), + useSocks5: Boolean(resolvedHost.useSocks5), + socks5Host: resolvedHost.socks5Host, + socks5Port: resolvedHost.socks5Port, + socks5Username: resolvedHost.socks5Username, + socks5Password: resolvedHost.socks5Password, + socks5ProxyChain: resolvedHost.socks5ProxyChain, + }; +} + +async function connectC2SSourceClient( + tunnelConfig: TunnelConfig, +): Promise { + const connOptions: Record = { + host: + tunnelConfig.sourceIP?.replace(/^\[|\]$/g, "") || tunnelConfig.sourceIP, + port: tunnelConfig.sourceSSHPort, + username: tunnelConfig.sourceUsername, + tryKeyboard: true, + keepaliveInterval: 30000, + keepaliveCountMax: 3, + readyTimeout: 60000, + tcpKeepAlive: true, + tcpKeepAliveInitialDelay: 30000, + algorithms: getManagedTunnelAlgorithms(), + }; + + applyAuthOptions(connOptions, { + password: tunnelConfig.sourcePassword, + sshKey: tunnelConfig.sourceSSHKey, + keyPassword: tunnelConfig.sourceKeyPassword, + keyType: tunnelConfig.sourceKeyType, + authMethod: tunnelConfig.sourceAuthMethod, + }); + + if ( + tunnelConfig.useSocks5 && + (tunnelConfig.socks5Host || + (tunnelConfig.socks5ProxyChain && + tunnelConfig.socks5ProxyChain.length > 0)) + ) { + const socks5Socket = await createSocks5Connection( + tunnelConfig.sourceIP, + tunnelConfig.sourceSSHPort, + { + useSocks5: tunnelConfig.useSocks5, + socks5Host: tunnelConfig.socks5Host, + socks5Port: tunnelConfig.socks5Port, + socks5Username: tunnelConfig.socks5Username, + socks5Password: tunnelConfig.socks5Password, + socks5ProxyChain: tunnelConfig.socks5ProxyChain, + }, + ); + if (socks5Socket) { + connOptions.sock = socks5Socket; + } + } + + return connectClient(connOptions, tunnelConfig.name, "source"); +} + +function pauseSourceForC2SWebSocket(ws: WebSocket, source?: Duplex): void { + if (!source) return; + if (ws.bufferedAmount <= C2S_WS_HIGH_WATERMARK) return; + + source.pause(); + const resumeTimer = setInterval(() => { + if ( + ws.readyState !== 1 || + source.destroyed || + ws.bufferedAmount <= C2S_WS_LOW_WATERMARK + ) { + clearInterval(resumeTimer); + if (ws.readyState === 1 && !source.destroyed) { + source.resume(); + } + } + }, 25); +} + +function sendC2SMessage( + ws: WebSocket, + message: Record, + source?: Duplex, +): void { + if (ws.readyState === 1) { + ws.send(JSON.stringify(message), (error) => { + if (error && source && !source.destroyed) { + source.destroy(error); + } + }); + pauseSourceForC2SWebSocket(ws, source); + } +} + +function writeC2SRemoteChunk( + target: ClientChannel, + chunk: Buffer, + ws: WebSocket, + closeTarget: () => void, +): void { + if (!target || target.destroyed) return; + + if (target.writableLength > C2S_STREAM_WRITE_LIMIT) { + closeTarget(); + return; + } + + const canContinue = target.write(chunk); + if (!canContinue) { + ws.pause(); + target.once("drain", () => { + if (ws.readyState === 1) { + ws.resume(); + } + }); + } +} + +async function handleC2SRemoteRelayOpen( + ws: WebSocket, + tunnelConfig: TunnelConfig, +): Promise { + const tunnelName = tunnelConfig.name; + const sourceClient = await connectC2SSourceClient(tunnelConfig); + const bindHost = tunnelConfig.targetHost || "127.0.0.1"; + const bindPort = Number(tunnelConfig.sourcePort); + let closed = false; + + if (!Number.isInteger(bindPort) || bindPort < 1 || bindPort > 65535) { + throw new Error("Invalid remote port"); + } + + const actualPort = await bindForwardIn(sourceClient, bindHost, bindPort); + const streams = new Map(); + + const closeStream = (streamId: string): void => { + const stream = streams.get(streamId); + if (!stream) return; + streams.delete(streamId); + try { + stream.destroy(); + } catch { + // expected during shutdown + } + }; + + const close = (): void => { + if (closed) return; + closed = true; + for (const streamId of streams.keys()) { + closeStream(streamId); + } + unbindForwardIn(sourceClient, bindHost, actualPort); + try { + sourceClient.end(); + } catch { + // expected during shutdown + } + }; + + sourceClient.on("tcp connection", (info, accept, reject) => { + if (info.destPort !== actualPort) { + reject(); + return; + } + + const inbound = accept(); + const streamId = `${Date.now()}-${++c2sRemoteStreamCounter}`; + streams.set(streamId, inbound); + + sendC2SMessage(ws, { type: "connection", streamId }); + + inbound.on("data", (chunk) => { + sendC2SMessage( + ws, + { + type: "data", + streamId, + data: chunk.toString("base64"), + }, + inbound, + ); + }); + inbound.on("close", () => { + streams.delete(streamId); + sendC2SMessage(ws, { type: "close", streamId }); + }); + inbound.on("error", (error) => { + streams.delete(streamId); + sendC2SMessage(ws, { + type: "close", + streamId, + error: error instanceof Error ? error.message : String(error), + }); + }); + }); + + ws.on("message", (data, isBinary) => { + if (isBinary) return; + + try { + const message = JSON.parse(data.toString()) as { + type?: string; + streamId?: string; + data?: string; + }; + if (!message.streamId) return; + + if (message.type === "data" && message.data) { + const stream = streams.get(message.streamId); + if (stream) { + writeC2SRemoteChunk( + stream, + Buffer.from(message.data, "base64"), + ws, + () => closeStream(message.streamId as string), + ); + } + } else if (message.type === "close") { + closeStream(message.streamId); + } + } catch (error) { + tunnelLogger.warn("Invalid C2S remote relay message", { + operation: "c2s_remote_relay_invalid_message", + tunnelName, + error: error instanceof Error ? error.message : String(error), + }); + } + }); + + ws.on("close", close); + ws.on("error", close); + sourceClient.on("close", () => { + if (ws.readyState === 1) ws.close(); + }); + sourceClient.on("error", (error) => { + sendC2SMessage(ws, { + type: "error", + error: error instanceof Error ? error.message : String(error), + }); + if (ws.readyState === 1) ws.close(); + }); + + tunnelLogger.info("C2S remote tunnel ready", { + operation: "c2s_remote_tunnel_ready", + tunnelName, + bindHost, + bindPort: actualPort, + endpointHost: tunnelConfig.endpointHost, + }); + sendC2SMessage(ws, { type: "ready", bindHost, bindPort: actualPort }); +} + +async function handleC2SRelayOpen( + ws: WebSocket, + message: C2SOpenMessage, + userId: string, +): Promise { + const tunnelConfig = await resolveC2STunnelSource( + message.tunnelConfig || {}, + userId, + ); + const mode = getTunnelMode(tunnelConfig); + if (mode === "remote") { + await handleC2SRemoteRelayOpen(ws, tunnelConfig); + return; + } + + const targetHost = + mode === "dynamic" + ? message.targetHost + : tunnelConfig.targetHost || "127.0.0.1"; + const targetPort = + mode === "dynamic" + ? Number(message.targetPort) + : Number(tunnelConfig.endpointPort); + + if (!targetHost || !Number.isInteger(targetPort) || targetPort < 1) { + throw new Error("Invalid client tunnel target"); + } + + const sourceClient = await connectC2SSourceClient(tunnelConfig); + const outbound = await forwardOut(sourceClient, targetHost, targetPort); + + const close = () => { + try { + outbound.destroy(); + } catch { + // expected during shutdown + } + try { + sourceClient.end(); + } catch { + // expected during shutdown + } + }; + + outbound.on("data", (chunk) => { + if (ws.readyState === 1) { + ws.send(chunk); + } + }); + outbound.on("close", () => { + if (ws.readyState === 1) ws.close(); + }); + outbound.on("error", () => { + if (ws.readyState === 1) ws.close(); + }); + ws.on("close", close); + ws.on("error", close); + ws.on("message", (data, isBinary) => { + if (!isBinary) return; + const chunk = Buffer.isBuffer(data) + ? data + : Array.isArray(data) + ? Buffer.concat(data) + : Buffer.from(data); + outbound.write(chunk); + }); + + ws.send(JSON.stringify({ type: "ready" })); +} + +async function handleC2SRelayTest( + ws: WebSocket, + message: C2SOpenMessage, + userId: string, +): Promise { + const tunnelConfig = await resolveC2STunnelSource( + message.tunnelConfig || {}, + userId, + ); + const mode = getTunnelMode(tunnelConfig); + const sourceClient = await connectC2SSourceClient(tunnelConfig); + + try { + if (mode === "remote") { + const bindHost = tunnelConfig.targetHost || "127.0.0.1"; + const bindPort = Number(tunnelConfig.sourcePort); + if (!Number.isInteger(bindPort) || bindPort < 1 || bindPort > 65535) { + throw new Error("Invalid remote port"); + } + + const actualPort = await bindForwardIn(sourceClient, bindHost, bindPort); + unbindForwardIn(sourceClient, bindHost, actualPort); + } else if (mode === "local") { + const targetHost = tunnelConfig.targetHost || "127.0.0.1"; + const targetPort = Number(tunnelConfig.endpointPort); + if (!Number.isInteger(targetPort) || targetPort < 1) { + throw new Error("Invalid remote target port"); + } + + const outbound = await forwardOut(sourceClient, targetHost, targetPort); + outbound.destroy(); + } + + sendC2SMessage(ws, { type: "ready" }); + } finally { + try { + sourceClient.end(); + } catch { + // expected during shutdown + } + } +} + async function connectSSHTunnel( tunnelConfig: TunnelConfig, retryAttempt = 0, ): Promise { const tunnelName = tunnelConfig.name; - const tunnelMarker = getTunnelMarker(tunnelName); tunnelLogger.info("Tunnel creation request received", { operation: "tunnel_create_request", userId: tunnelConfig.sourceUserId, @@ -637,46 +1655,6 @@ async function connectSSHTunnel( authMethod: tunnelConfig.endpointAuthMethod, }; - if ( - resolvedEndpointCredentials.authMethod === "password" && - !resolvedEndpointCredentials.password - ) { - const errorMessage = `Cannot connect tunnel '${tunnelName}': endpoint host requires password authentication but no plaintext password available. Enable autostart for endpoint host or configure credentials in tunnel connection.`; - tunnelLogger.error(errorMessage, undefined, { - operation: "tunnel_endpoint_password_unavailable", - tunnelName, - endpointHost: `${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}:${tunnelConfig.endpointPort}`, - endpointAuthMethod: resolvedEndpointCredentials.authMethod, - }); - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: errorMessage, - }); - tunnelConnecting.delete(tunnelName); - return; - } - - if ( - resolvedEndpointCredentials.authMethod === "key" && - !resolvedEndpointCredentials.sshKey - ) { - const errorMessage = `Cannot connect tunnel '${tunnelName}': endpoint host requires key authentication but no plaintext key available. Enable autostart for endpoint host or configure credentials in tunnel connection.`; - tunnelLogger.error(errorMessage, undefined, { - operation: "tunnel_endpoint_key_unavailable", - tunnelName, - endpointHost: `${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}:${tunnelConfig.endpointPort}`, - endpointAuthMethod: resolvedEndpointCredentials.authMethod, - }); - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: errorMessage, - }); - tunnelConnecting.delete(tunnelName); - return; - } - if (tunnelConfig.endpointCredentialId && tunnelConfig.endpointUserId) { try { const userDataKey = DataCrypto.getUserDataKey( @@ -723,6 +1701,46 @@ async function connectSSHTunnel( }); } + if ( + resolvedEndpointCredentials.authMethod === "password" && + !resolvedEndpointCredentials.password + ) { + const errorMessage = `Cannot connect tunnel '${tunnelName}': endpoint host requires password authentication but no plaintext password available. Enable autostart for endpoint host or configure credentials in tunnel connection.`; + tunnelLogger.error(errorMessage, undefined, { + operation: "tunnel_endpoint_password_unavailable", + tunnelName, + endpointHost: `${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}:${tunnelConfig.endpointPort}`, + endpointAuthMethod: resolvedEndpointCredentials.authMethod, + }); + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: errorMessage, + }); + tunnelConnecting.delete(tunnelName); + return; + } + + if ( + resolvedEndpointCredentials.authMethod === "key" && + !resolvedEndpointCredentials.sshKey + ) { + const errorMessage = `Cannot connect tunnel '${tunnelName}': endpoint host requires key authentication but no plaintext key available. Enable autostart for endpoint host or configure credentials in tunnel connection.`; + tunnelLogger.error(errorMessage, undefined, { + operation: "tunnel_endpoint_key_unavailable", + tunnelName, + endpointHost: `${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}:${tunnelConfig.endpointPort}`, + endpointAuthMethod: resolvedEndpointCredentials.authMethod, + }); + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + reason: errorMessage, + }); + tunnelConnecting.delete(tunnelName); + return; + } + const conn = new Client(); const connectionTimeout = setTimeout(() => { @@ -834,13 +1852,15 @@ async function connectSSHTunnel( } }); - conn.on("ready", () => { + conn.on("ready", async () => { clearTimeout(connectionTimeout); - tunnelLogger.info("Creating new SSH connection for tunnel", { - operation: "tunnel_connection_create", + tunnelLogger.info("Creating managed SSH tunnel", { + operation: "managed_tunnel_connection_create", userId: tunnelConfig.sourceUserId, hostId: tunnelConfig.sourceHostId, tunnelName, + scope: getTunnelScope(tunnelConfig), + mode: getTunnelMode(tunnelConfig), }); const isAlreadyVerifying = tunnelVerifications.has(tunnelName); @@ -848,213 +1868,64 @@ async function connectSSHTunnel( return; } - const tunnelType = tunnelConfig.tunnelType || "remote"; - const tunnelFlag = tunnelType === "local" ? "-L" : "-R"; - const portMapping = - tunnelType === "local" - ? `${tunnelConfig.sourcePort}:${tunnelConfig.endpointIP}:${tunnelConfig.endpointPort}` - : `${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort}`; - - let tunnelCmd: string; - if ( - resolvedEndpointCredentials.authMethod === "key" && - resolvedEndpointCredentials.sshKey - ) { - const keyFilePath = `/tmp/tunnel_key_${tunnelName.replace(/[^a-zA-Z0-9]/g, "_")}`; - tunnelCmd = `echo '${resolvedEndpointCredentials.sshKey}' > ${keyFilePath} && chmod 600 ${keyFilePath} && exec -a "${tunnelMarker}" ssh -i ${keyFilePath} -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o GatewayPorts=yes ${tunnelFlag} ${portMapping} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP} && rm -f ${keyFilePath}`; - } else { - tunnelCmd = `exec -a "${tunnelMarker}" sshpass -p '${resolvedEndpointCredentials.password || ""}' ssh -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o GatewayPorts=yes ${tunnelFlag} ${portMapping} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}`; - } - - conn.exec(tunnelCmd, (err, stream) => { - if (err) { - const errorType = classifyError(err.message); - - tunnelLogger.error( - `Failed to execute tunnel command for '${tunnelName}'`, - err, - { - operation: "tunnel_exec_error", - tunnelName, - errorType, - errorMessage: err.message, - sourceHost: `${tunnelConfig.sourceUsername}@${tunnelConfig.sourceIP}:${tunnelConfig.sourceSSHPort}`, - endpointHost: `${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}:${tunnelConfig.endpointPort}`, - tunnelType: tunnelConfig.tunnelType || "remote", - sourcePort: tunnelConfig.sourcePort, - endpointPort: tunnelConfig.endpointPort, - retryAttempt, - }, + try { + if (getTunnelScope(tunnelConfig) !== "s2s") { + throw new Error( + "C2S tunnels must be started from the desktop client local configuration", ); - - conn.end(); - - activeTunnels.delete(tunnelName); - - const shouldNotRetry = - errorType === "AUTHENTICATION_FAILED" || - errorType === "CONNECTION_FAILED"; - - handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry); - return; } - activeTunnels.set(tunnelName, conn); - tunnelLogger.success("Tunnel port binding successful", { - operation: "tunnel_port_bound", + await establishManagedS2STunnel( + conn, + tunnelConfig, + resolvedEndpointCredentials, + ); + + tunnelConnecting.delete(tunnelName); + tunnelLogger.success("Managed tunnel creation complete", { + operation: "managed_tunnel_create_complete", userId: tunnelConfig.sourceUserId, hostId: tunnelConfig.sourceHostId, tunnelName, + mode: getTunnelMode(tunnelConfig), sourcePort: tunnelConfig.sourcePort, endpointPort: tunnelConfig.endpointPort, }); - setTimeout(() => { - if ( - !manualDisconnects.has(tunnelName) && - activeTunnels.has(tunnelName) - ) { - tunnelConnecting.delete(tunnelName); - tunnelLogger.success("Tunnel creation complete", { - operation: "tunnel_create_complete", - userId: tunnelConfig.sourceUserId, - hostId: tunnelConfig.sourceHostId, - tunnelName, - }); - - broadcastTunnelStatus(tunnelName, { - connected: true, - status: CONNECTION_STATES.CONNECTED, - }); - setupPingInterval(tunnelName); - } - }, 2000); - - stream.on("close", (code: number) => { - if (activeRetryTimers.has(tunnelName)) { - return; - } - - activeTunnels.delete(tunnelName); - - if (tunnelVerifications.has(tunnelName)) { - try { - const verification = tunnelVerifications.get(tunnelName); - if (verification?.timeout) clearTimeout(verification.timeout); - verification?.conn.end(); - } catch { - // expected - } - tunnelVerifications.delete(tunnelName); - } - - const isLikelyRemoteClosure = code === 255; - - if (isLikelyRemoteClosure && retryExhaustedTunnels.has(tunnelName)) { - retryExhaustedTunnels.delete(tunnelName); - } - - if ( - !manualDisconnects.has(tunnelName) && - code !== 0 && - code !== undefined - ) { - if (retryExhaustedTunnels.has(tunnelName)) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: "Max retries exhausted", - }); - } else { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: isLikelyRemoteClosure - ? "Connection closed by remote host" - : "Connection closed unexpectedly", - }); - } - } - - if ( - !activeRetryTimers.has(tunnelName) && - !retryExhaustedTunnels.has(tunnelName) - ) { - handleDisconnect( - tunnelName, - tunnelConfig, - !manualDisconnects.has(tunnelName), - ); - } else if ( - retryExhaustedTunnels.has(tunnelName) && - isLikelyRemoteClosure - ) { - retryExhaustedTunnels.delete(tunnelName); - retryCounters.delete(tunnelName); - handleDisconnect(tunnelName, tunnelConfig, true); - } + broadcastTunnelStatus(tunnelName, { + connected: true, + status: CONNECTION_STATES.CONNECTED, }); - - stream.stdout?.on("data", () => {}); - - stream.on("error", () => {}); - - stream.stderr.on("data", (data) => { - const errorMsg = data.toString().trim(); - if (errorMsg) { - const isDebugMessage = - errorMsg.startsWith("debug1:") || - errorMsg.startsWith("debug2:") || - errorMsg.startsWith("debug3:") || - errorMsg.includes("Reading configuration data") || - errorMsg.includes("include /etc/ssh/ssh_config.d") || - errorMsg.includes("matched no files") || - errorMsg.includes("Applying options for"); - - if (!isDebugMessage) { - tunnelLogger.error(`SSH stderr for '${tunnelName}': ${errorMsg}`); - } - - if ( - errorMsg.includes("sshpass: command not found") || - errorMsg.includes("sshpass not found") - ) { - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: - "sshpass tool not found on source host. Please install sshpass or use SSH key authentication.", - }); - } - - if ( - errorMsg.includes("remote port forwarding failed") || - errorMsg.includes("Error: remote port forwarding failed") - ) { - const portMatch = errorMsg.match(/listen port (\d+)/); - const port = portMatch ? portMatch[1] : tunnelConfig.endpointPort; - - tunnelLogger.error( - `Port forwarding failed for tunnel '${tunnelName}' on port ${port}. This prevents tunnel establishment.`, - ); - - if (activeTunnels.has(tunnelName)) { - const conn = activeTunnels.get(tunnelName); - if (conn) { - conn.end(); - } - activeTunnels.delete(tunnelName); - } - - broadcastTunnelStatus(tunnelName, { - connected: false, - status: CONNECTION_STATES.FAILED, - reason: `Remote port forwarding failed for port ${port}. Port may be in use, requires root privileges, or SSH server doesn't allow port forwarding. Try a different port.`, - }); - } - } + setupPingInterval(tunnelName); + } catch (error) { + const message = + error instanceof Error ? error.message : "Failed to create tunnel"; + const errorType = classifyError(message); + tunnelLogger.error("Failed to create managed tunnel", error, { + operation: "managed_tunnel_create_failed", + tunnelName, + errorType, + retryAttempt, }); - }); + tunnelConnecting.delete(tunnelName); + activeTunnels.delete(tunnelName); + activeTunnelRuntimes.delete(tunnelName); + try { + conn.end(); + } catch { + // expected + } + broadcastTunnelStatus(tunnelName, { + connected: false, + status: CONNECTION_STATES.FAILED, + errorType, + reason: message, + }); + const shouldNotRetry = + errorType === "AUTHENTICATION_FAILED" || + errorType === "CONNECTION_FAILED"; + handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry); + } }); const connOptions: Record = { @@ -1421,6 +2292,7 @@ async function killRemoteTunnelByMarker( (socks5Error instanceof Error ? socks5Error.message : "Unknown error"), + { cause: socks5Error }, ); } } @@ -1534,9 +2406,53 @@ async function killRemoteTunnelByMarker( * 200: * description: A list of all tunnel statuses. */ -app.get("/ssh/tunnel/status", (req, res) => { - res.json(getAllTunnelStatus()); -}); +app.get( + "/ssh/tunnel/status", + authenticateJWT, + (req: AuthenticatedRequest, res: Response) => { + if (!req.userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + res.json(getAllTunnelStatus()); + }, +); + +app.get( + "/ssh/tunnel/status/stream", + authenticateJWT, + (req: AuthenticatedRequest, res: Response) => { + if (!req.userId) { + return res.status(401).json({ error: "Authentication required" }); + } + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-store, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }); + res.flushHeaders?.(); + + tunnelStatusClients.add(res); + sendTunnelStatusSnapshot(res); + + const heartbeat = setInterval(() => { + try { + res.write(": keepalive\n\n"); + } catch { + closeStream(); + } + }, 30000); + + const closeStream = () => { + clearInterval(heartbeat); + tunnelStatusClients.delete(res); + }; + + req.on("close", closeStream); + }, +); /** * @openapi @@ -1558,16 +2474,27 @@ app.get("/ssh/tunnel/status", (req, res) => { * 404: * description: Tunnel not found. */ -app.get("/ssh/tunnel/status/:tunnelName", (req, res) => { - const { tunnelName } = req.params; - const status = connectionStatus.get(tunnelName); +app.get( + "/ssh/tunnel/status/:tunnelName", + authenticateJWT, + (req: AuthenticatedRequest, res: Response) => { + if (!req.userId) { + return res.status(401).json({ error: "Authentication required" }); + } - if (!status) { - return res.status(404).json({ error: "Tunnel not found" }); - } + const tunnelNameParam = req.params.tunnelName; + const tunnelName = Array.isArray(tunnelNameParam) + ? tunnelNameParam[0] + : tunnelNameParam; + const status = connectionStatus.get(tunnelName); - res.json({ name: tunnelName, status }); -}); + if (!status) { + return res.status(404).json({ error: "Tunnel not found" }); + } + + res.json({ name: tunnelName, status }); + }, +); /** * @openapi @@ -1755,6 +2682,7 @@ app.post( ); throw new Error( `Failed to resolve endpoint host: ${resolveError instanceof Error ? resolveError.message : "Unknown error"}`, + { cause: resolveError }, ); } } @@ -2037,6 +2965,13 @@ async function initializeAutoStartTunnels(): Promise { tunnelConnection.endpointHost, tunnelConnection.endpointPort, ), + scope: tunnelConnection.scope || "s2s", + mode: + tunnelConnection.mode || + tunnelConnection.tunnelType || + "remote", + bindHost: tunnelConnection.bindHost, + targetHost: tunnelConnection.targetHost, tunnelType: tunnelConnection.tunnelType || "remote", sourceHostId: host.id, tunnelIndex: tunnelIndex, @@ -2102,7 +3037,56 @@ async function initializeAutoStartTunnels(): Promise { } const PORT = 30003; -app.listen(PORT, () => { +const server = createServer(app); +const c2sRelayWss = new WebSocketServer({ + server, + path: "/ssh/tunnel/c2s/stream", +}); + +c2sRelayWss.on("connection", (ws, req) => { + let opened = false; + + ws.once("message", async (raw) => { + try { + const token = extractRequestToken(req); + const payload = token ? await authManager.verifyJWTToken(token) : null; + if (!payload?.userId || payload.pendingTOTP) { + sendC2SError(ws, "Authentication required"); + ws.close(); + return; + } + + const message = JSON.parse(raw.toString()) as C2SOpenMessage; + if (message.type !== "open" && message.type !== "test") { + throw new Error("Invalid client tunnel relay request"); + } + + opened = true; + if (message.type === "test") { + await handleC2SRelayTest(ws, message, payload.userId); + } else { + await handleC2SRelayOpen(ws, message, payload.userId); + } + } catch (error) { + const message = describeC2SRelayError(error); + tunnelLogger.error("Failed to open C2S relay", error, { + operation: "c2s_relay_open_failed", + }); + sendC2SError(ws, message); + ws.close(); + } + }); + + ws.on("close", () => { + if (!opened) { + tunnelLogger.info("C2S relay closed before opening", { + operation: "c2s_relay_closed_before_open", + }); + } + }); +}); + +server.listen(PORT, () => { setTimeout(() => { initializeAutoStartTunnels(); }, 2000); diff --git a/src/backend/ssh/widgets/cpu-collector.ts b/src/backend/ssh/widgets/cpu-collector.ts index 2328f0b3..d1f59144 100644 --- a/src/backend/ssh/widgets/cpu-collector.ts +++ b/src/backend/ssh/widgets/cpu-collector.ts @@ -79,7 +79,6 @@ export async function collectCpuMetrics(client: Client): Promise<{ cores = Number.isFinite(coresNum) && coresNum > 0 ? coresNum : null; } catch { cpuPercent = null; - cores = null; loadTriplet = null; } diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 3a82dda7..e6464b6e 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -98,6 +98,7 @@ import { const systemCrypto = SystemCrypto.getInstance(); await systemCrypto.initializeJWTSecret(); await systemCrypto.initializeDatabaseKey(); + await systemCrypto.initializeEncryptionKey(); await systemCrypto.initializeInternalAuthToken(); await AutoSSLSetup.initialize(); diff --git a/src/backend/swagger.ts b/src/backend/swagger.ts index c631c1b4..8ed87a03 100644 --- a/src/backend/swagger.ts +++ b/src/backend/swagger.ts @@ -1,15 +1,20 @@ -import swaggerJSDoc from "swagger-jsdoc"; +import swaggerJSDoc from "@deadendjs/swagger-jsdoc"; import path from "path"; import { fileURLToPath } from "url"; import { promises as fs } from "fs"; import { systemLogger } from "./utils/logger.js"; +interface SwaggerOptions { + definition: object; + apis: string[]; +} + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const projectRoot = path.join(__dirname, "..", "..", ".."); -const swaggerOptions: swaggerJSDoc.Options = { +const swaggerOptions: SwaggerOptions = { definition: { openapi: "3.0.3", info: { @@ -130,7 +135,7 @@ async function generateOpenAPISpec() { operation: "openapi_generate_start", }); - const swaggerSpec = swaggerJSDoc(swaggerOptions); + const swaggerSpec = await swaggerJSDoc(swaggerOptions); const outputPath = path.join(projectRoot, "openapi.json"); diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 87d4669c..c3d5760b 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -1,15 +1,13 @@ import jwt from "jsonwebtoken"; +import crypto from "crypto"; import { UserCrypto } from "./user-crypto.js"; import { SystemCrypto } from "./system-crypto.js"; import { DataCrypto } from "./data-crypto.js"; import { databaseLogger, authLogger } from "./logger.js"; import type { Request, Response, NextFunction } from "express"; -import { - db, - getSqlite, - saveMemoryDatabaseToFile, -} from "../database/db/index.js"; -import { sessions, trustedDevices } from "../database/db/schema.js"; +import bcrypt from "bcryptjs"; +import { db } from "../database/db/index.js"; +import { sessions, trustedDevices, apiKeys } from "../database/db/schema.js"; import { eq, and, sql } from "drizzle-orm"; import { nanoid } from "nanoid"; import type { DeviceType } from "./user-agent-parser.js"; @@ -29,12 +27,21 @@ interface JWTPayload { userId: string; sessionId?: string; pendingTOTP?: boolean; + dataKeyWrap?: WrappedDataKey; iat?: number; exp?: number; } +interface WrappedDataKey { + version: "v1"; + iv: string; + tag: string; + data: string; +} + interface AuthenticatedRequest extends Request { userId?: string; + sessionId?: string; pendingTOTP?: boolean; dataKey?: Buffer; } @@ -198,6 +205,113 @@ class AuthManager { } } + private getDataKeyAAD(userId: string, sessionId?: string): Buffer { + return Buffer.from(`${userId}:${sessionId || ""}`, "utf8"); + } + + private async wrapUserDataKey( + userId: string, + sessionId: string | undefined, + dataKey: Buffer, + ): Promise { + const encryptionKey = await this.systemCrypto.getEncryptionKey(); + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv("aes-256-gcm", encryptionKey, iv); + cipher.setAAD(this.getDataKeyAAD(userId, sessionId)); + + const encrypted = Buffer.concat([cipher.update(dataKey), cipher.final()]); + const tag = cipher.getAuthTag(); + + return { + version: "v1", + iv: iv.toString("base64url"), + tag: tag.toString("base64url"), + data: encrypted.toString("base64url"), + }; + } + + private async unwrapUserDataKey( + userId: string, + sessionId: string | undefined, + wrapped: WrappedDataKey, + ): Promise { + if (wrapped.version !== "v1") { + throw new Error( + `Unsupported wrapped data key version: ${wrapped.version}`, + ); + } + + const encryptionKey = await this.systemCrypto.getEncryptionKey(); + const decipher = crypto.createDecipheriv( + "aes-256-gcm", + encryptionKey, + Buffer.from(wrapped.iv, "base64url"), + ); + decipher.setAAD(this.getDataKeyAAD(userId, sessionId)); + decipher.setAuthTag(Buffer.from(wrapped.tag, "base64url")); + + return Buffer.concat([ + decipher.update(Buffer.from(wrapped.data, "base64url")), + decipher.final(), + ]); + } + + private async addWrappedDataKey(payload: JWTPayload): Promise { + if (payload.pendingTOTP) { + return; + } + + const dataKey = this.userCrypto.getUserDataKey(payload.userId); + if (!dataKey) { + return; + } + + payload.dataKeyWrap = await this.wrapUserDataKey( + payload.userId, + payload.sessionId, + dataKey, + ); + } + + private async restoreDataKeyFromPayload( + payload: JWTPayload, + sessionExpiresAt?: string, + ): Promise { + if ( + !payload.dataKeyWrap || + this.userCrypto.getUserDataKey(payload.userId) + ) { + return; + } + + const expiresAt = sessionExpiresAt + ? new Date(sessionExpiresAt).getTime() + : payload.exp + ? payload.exp * 1000 + : Date.now(); + + if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) { + return; + } + + try { + const dataKey = await this.unwrapUserDataKey( + payload.userId, + payload.sessionId, + payload.dataKeyWrap, + ); + this.userCrypto.restoreUserDataKey(payload.userId, dataKey, expiresAt); + dataKey.fill(0); + } catch (error) { + databaseLogger.warn("Failed to restore data key from session token", { + operation: "session_data_key_restore_failed", + userId: payload.userId, + sessionId: payload.sessionId, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + async generateJWTToken( userId: string, options: { @@ -234,6 +348,7 @@ class AuthManager { if (!options.pendingTOTP && options.deviceType && options.deviceInfo) { const sessionId = nanoid(); payload.sessionId = sessionId; + await this.addWrappedDataKey(payload); const token = jwt.sign(payload, jwtSecret, { expiresIn, @@ -281,6 +396,7 @@ class AuthManager { return token; } + await this.addWrappedDataKey(payload); return jwt.sign(payload, jwtSecret, { expiresIn } as jwt.SignOptions); } @@ -327,6 +443,11 @@ class AuthManager { }); return null; } + + await this.restoreDataKeyFromPayload( + payload, + sessionRecords[0].expiresAt, + ); } catch (dbError) { databaseLogger.error( "Failed to check session in database during JWT verification", @@ -338,6 +459,8 @@ class AuthManager { ); return null; } + } else { + await this.restoreDataKeyFromPayload(payload); } return payload; } catch (error) { @@ -350,12 +473,63 @@ class AuthManager { } } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + async refreshSessionToken( + userId: string, + sessionId: string, + ): Promise<{ token: string; maxAge: number } | null> { + const sessionRecords = await db + .select() + .from(sessions) + .where(eq(sessions.id, sessionId)) + .limit(1); + + if (sessionRecords.length === 0 || sessionRecords[0].userId !== userId) { + return null; + } + + const expiresAt = new Date(sessionRecords[0].expiresAt).getTime(); + const maxAge = expiresAt - Date.now(); + if (!Number.isFinite(maxAge) || maxAge <= 0) { + return null; + } + + const payload: JWTPayload = { userId, sessionId }; + await this.addWrappedDataKey(payload); + + const token = jwt.sign(payload, await this.systemCrypto.getJWTSecret(), { + expiresIn: Math.ceil(maxAge / 1000), + } as jwt.SignOptions); + + await db + .update(sessions) + .set({ + jwtToken: token, + lastActiveAt: new Date().toISOString(), + }) + .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 token refresh", + saveError, + { + operation: "session_token_refresh_db_save_failed", + sessionId, + }, + ); + } + + return { token, maxAge }; + } + invalidateJWTToken(_token: string): void { // expected - no-op, JWT tokens are stateless } - // eslint-disable-next-line @typescript-eslint/no-unused-vars invalidateUserTokens(_userId: string): void { // expected - no-op, handled by session management } @@ -537,9 +711,9 @@ class AuthManager { maxAge: number = 24 * 60 * 60 * 1000, ) { return { - httpOnly: false, + httpOnly: true, secure: req.secure || req.headers["x-forwarded-proto"] === "https", - sameSite: "strict" as const, + sameSite: "lax" as const, maxAge: maxAge, path: "/", }; @@ -547,13 +721,88 @@ class AuthManager { getClearCookieOptions(req: RequestWithHeaders) { return { - httpOnly: false, + httpOnly: true, secure: req.secure || req.headers["x-forwarded-proto"] === "https", - sameSite: "strict" as const, + sameSite: "lax" as const, path: "/", }; } + private async handleApiKeyAuth( + req: AuthenticatedRequest, + res: Response, + next: NextFunction, + token: string, + requireAdmin = false, + ): Promise { + try { + const tokenPrefix = token.substring(0, 12); + + const candidates = await db + .select() + .from(apiKeys) + .where( + and(eq(apiKeys.tokenPrefix, tokenPrefix), eq(apiKeys.isActive, true)), + ); + + if (candidates.length === 0) { + res.status(401).json({ error: "Invalid API key" }); + return; + } + + let matchedKey: (typeof candidates)[0] | null = null; + for (const candidate of candidates) { + if (await bcrypt.compare(token, candidate.tokenHash)) { + matchedKey = candidate; + break; + } + } + + if (!matchedKey) { + res.status(401).json({ error: "Invalid API key" }); + return; + } + + if (matchedKey.expiresAt && new Date(matchedKey.expiresAt) < new Date()) { + res.status(401).json({ error: "API key has expired" }); + return; + } + + if (requireAdmin) { + const { users } = await import("../database/db/schema.js"); + const userRows = await db + .select() + .from(users) + .where(eq(users.id, matchedKey.userId)) + .limit(1); + if (!userRows[0]?.isAdmin) { + res.status(403).json({ error: "Admin access required" }); + return; + } + } + + db.update(apiKeys) + .set({ lastUsedAt: new Date().toISOString() }) + .where(eq(apiKeys.id, matchedKey.id)) + .then(() => {}) + .catch((err) => { + databaseLogger.warn("Failed to update API key lastUsedAt", { + operation: "api_key_update_last_used", + keyId: matchedKey!.id, + error: err instanceof Error ? err.message : "Unknown", + }); + }); + + req.userId = matchedKey.userId; + next(); + } catch (error) { + databaseLogger.error("API key authentication failed", error, { + operation: "api_key_auth_failed", + }); + res.status(500).json({ error: "API key authentication failed" }); + } + } + createAuthMiddleware() { return async (req: Request, res: Response, next: NextFunction) => { const authReq = req as AuthenticatedRequest; @@ -570,10 +819,17 @@ class AuthManager { return res.status(401).json({ error: "Missing authentication token" }); } + if (token.startsWith("tmx_")) { + return this.handleApiKeyAuth(authReq, res, next, token); + } + const payload = await this.verifyJWTToken(token); if (!payload) { - return res.status(401).json({ error: "Invalid token" }); + return res + .clearCookie("jwt", this.getClearCookieOptions(req)) + .status(401) + .json({ error: "Invalid token" }); } if (payload.pendingTOTP) { @@ -597,10 +853,13 @@ class AuthManager { sessionId: payload.sessionId, userId: payload.userId, }); - return res.status(401).json({ - error: "Session not found", - code: "SESSION_NOT_FOUND", - }); + return res + .clearCookie("jwt", this.getClearCookieOptions(req)) + .status(401) + .json({ + error: "Session not found", + code: "SESSION_NOT_FOUND", + }); } const session = sessionRecords[0]; @@ -657,10 +916,13 @@ class AuthManager { ); }); - return res.status(401).json({ - error: "Session has expired", - code: "SESSION_EXPIRED", - }); + return res + .clearCookie("jwt", this.getClearCookieOptions(req)) + .status(401) + .json({ + error: "Session has expired", + code: "SESSION_EXPIRED", + }); } db.update(sessions) @@ -684,6 +946,7 @@ class AuthManager { } authReq.userId = payload.userId; + authReq.sessionId = payload.sessionId; authReq.pendingTOTP = payload.pendingTOTP; next(); }; @@ -718,10 +981,23 @@ class AuthManager { return res.status(401).json({ error: "Missing authentication token" }); } + if (token.startsWith("tmx_")) { + return this.handleApiKeyAuth( + req as AuthenticatedRequest, + res, + next, + token, + true, + ); + } + const payload = await this.verifyJWTToken(token); if (!payload) { - return res.status(401).json({ error: "Invalid token" }); + return res + .clearCookie("jwt", this.getClearCookieOptions(req)) + .status(401) + .json({ error: "Invalid token" }); } if (payload.pendingTOTP) { @@ -755,6 +1031,7 @@ class AuthManager { const authReq = req as AuthenticatedRequest; authReq.userId = payload.userId; + authReq.sessionId = payload.sessionId; authReq.pendingTOTP = payload.pendingTOTP; next(); } catch (error) { diff --git a/src/backend/utils/auto-ssl-setup.ts b/src/backend/utils/auto-ssl-setup.ts index e6a2a86c..7550e8eb 100644 --- a/src/backend/utils/auto-ssl-setup.ts +++ b/src/backend/utils/auto-ssl-setup.ts @@ -171,6 +171,7 @@ IP.3 = 0.0.0.0 } catch (error) { throw new Error( `SSL certificate generation failed: ${error instanceof Error ? error.message : "Unknown error"}`, + { cause: error }, ); } } diff --git a/src/backend/utils/cors-config.ts b/src/backend/utils/cors-config.ts index 07416a6c..7a96d889 100644 --- a/src/backend/utils/cors-config.ts +++ b/src/backend/utils/cors-config.ts @@ -53,7 +53,6 @@ export function createCorsMiddleware( return callback(null, true); const configured = getAllowedOrigins(); - if (configured.length === 0) return callback(null, true); if (configured.includes("*") || configured.includes(origin)) return callback(null, true); diff --git a/src/backend/utils/database-file-encryption.ts b/src/backend/utils/database-file-encryption.ts index 4e31a2dd..a7c0e512 100644 --- a/src/backend/utils/database-file-encryption.ts +++ b/src/backend/utils/database-file-encryption.ts @@ -102,6 +102,7 @@ class DatabaseFileEncryption { }); throw new Error( `Database buffer encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`, + { cause: error }, ); } } @@ -197,6 +198,7 @@ class DatabaseFileEncryption { }); throw new Error( `Database file encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`, + { cause: error }, ); } } @@ -237,6 +239,7 @@ class DatabaseFileEncryption { if (!fs.existsSync(metadataPath)) { throw new Error( `Could not read database: Not a valid single-file format and metadata file is missing: ${metadataPath}. Error: ${singleFileError.message}`, + { cause: singleFileError }, ); } @@ -247,6 +250,7 @@ class DatabaseFileEncryption { } catch (twoFileError) { throw new Error( `Failed to read database using both single-file and two-file formats. Error: ${twoFileError.message}`, + { cause: twoFileError }, ); } } @@ -358,6 +362,7 @@ class DatabaseFileEncryption { `- .env file readable: ${envFileReadable}\n` + `- DATABASE_KEY in environment: ${!!process.env.DATABASE_KEY}\n` + `Original error: ${errorMessage}`, + { cause: error }, ); } @@ -366,7 +371,9 @@ class DatabaseFileEncryption { encryptedPath, errorMessage, }); - throw new Error(`Database buffer decryption failed: ${errorMessage}`); + throw new Error(`Database buffer decryption failed: ${errorMessage}`, { + cause: error, + }); } } @@ -398,6 +405,7 @@ class DatabaseFileEncryption { }); throw new Error( `Database file decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`, + { cause: error }, ); } } diff --git a/src/backend/utils/database-migration.ts b/src/backend/utils/database-migration.ts index 7172fc40..0d35a694 100644 --- a/src/backend/utils/database-migration.ts +++ b/src/backend/utils/database-migration.ts @@ -50,8 +50,8 @@ export class DatabaseMigration { } } - let needsMigration = false; - let reason = ""; + let needsMigration: boolean; + let reason: string; if (hasEncryptedDb && hasUnencryptedDb) { const unencryptedSize = fs.statSync(this.unencryptedDbPath).size; @@ -119,6 +119,7 @@ export class DatabaseMigration { }); throw new Error( `Backup creation failed: ${error instanceof Error ? error.message : "Unknown error"}`, + { cause: error }, ); } } diff --git a/src/backend/utils/logger.ts b/src/backend/utils/logger.ts index c287502b..19731b88 100644 --- a/src/backend/utils/logger.ts +++ b/src/backend/utils/logger.ts @@ -1,4 +1,5 @@ import chalk from "chalk"; +import type { ChalkInstance } from "chalk"; export type LogLevel = "debug" | "info" | "warn" | "error" | "success"; @@ -78,7 +79,17 @@ export class Logger { } private getTimeStamp(): string { - return chalk.gray(`[${new Date().toLocaleTimeString()}]`); + const now = new Date(); + const format = process.env.LOG_TIMESTAMP_FORMAT?.toLowerCase(); + let time: string; + if (format === "iso") { + time = now.toISOString(); + } else if (format === "24h") { + time = now.toLocaleTimeString("en-GB", { hour12: false }); + } else { + time = now.toLocaleTimeString(); + } + return chalk.gray(`[${time}]`); } private sanitizeContext(context: LogContext): LogContext { @@ -149,7 +160,7 @@ export class Logger { return `${timestamp} ${levelTag} ${serviceTag} ${message}${contextStr}`; } - private getLevelColor(level: LogLevel): chalk.Chalk { + private getLevelColor(level: LogLevel): ChalkInstance { switch (level) { case "debug": return chalk.magenta; diff --git a/src/backend/utils/proxy-agent.ts b/src/backend/utils/proxy-agent.ts index 6e73e275..420c9758 100644 --- a/src/backend/utils/proxy-agent.ts +++ b/src/backend/utils/proxy-agent.ts @@ -1,7 +1,7 @@ -import { HttpsProxyAgent } from "https-proxy-agent"; -import type { Agent } from "http"; +import { ProxyAgent } from "undici"; +import type { Dispatcher } from "undici-types"; -export function getProxyAgent(targetUrl?: string): Agent | undefined { +export function getProxyAgent(targetUrl?: string): Dispatcher | undefined { const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || @@ -26,5 +26,5 @@ export function getProxyAgent(targetUrl?: string): Agent | undefined { } } - return new HttpsProxyAgent(proxyUrl); + return new ProxyAgent(proxyUrl) as unknown as Dispatcher; } diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts index 1d43238e..915b8248 100644 --- a/src/backend/utils/system-crypto.ts +++ b/src/backend/utils/system-crypto.ts @@ -7,6 +7,7 @@ class SystemCrypto { private static instance: SystemCrypto; private jwtSecret: string | null = null; private databaseKey: Buffer | null = null; + private encryptionKey: Buffer | null = null; private internalAuthToken: string | null = null; private credentialSharingKey: Buffer | null = null; @@ -61,7 +62,7 @@ class SystemCrypto { databaseLogger.error("Failed to initialize JWT secret", error, { operation: "jwt_init_failed", }); - throw new Error("JWT secret initialization failed"); + throw new Error("JWT secret initialization failed", { cause: error }); } } @@ -103,7 +104,7 @@ class SystemCrypto { operation: "db_key_init_failed", dataDir: process.env.DATA_DIR || "./db/data", }); - throw new Error("Database key initialization failed"); + throw new Error("Database key initialization failed", { cause: error }); } } @@ -114,6 +115,46 @@ class SystemCrypto { return this.databaseKey!; } + async initializeEncryptionKey(): Promise { + try { + const dataDir = process.env.DATA_DIR || "./db/data"; + const envPath = path.join(dataDir, ".env"); + + const envKey = process.env.ENCRYPTION_KEY; + if (envKey && envKey.length >= 64) { + this.encryptionKey = Buffer.from(envKey, "hex"); + return; + } + + try { + const envContent = await fs.readFile(envPath, "utf8"); + const keyMatch = envContent.match(/^ENCRYPTION_KEY=(.+)$/m); + if (keyMatch && keyMatch[1] && keyMatch[1].length >= 64) { + this.encryptionKey = Buffer.from(keyMatch[1], "hex"); + process.env.ENCRYPTION_KEY = keyMatch[1]; + return; + } + } catch { + // expected - env file may not exist + } + + await this.generateAndGuideEncryptionKey(); + } catch (error) { + databaseLogger.error("Failed to initialize encryption key", error, { + operation: "encryption_key_init_failed", + dataDir: process.env.DATA_DIR || "./db/data", + }); + throw new Error("Encryption key initialization failed", { cause: error }); + } + } + + async getEncryptionKey(): Promise { + if (!this.encryptionKey) { + await this.initializeEncryptionKey(); + } + return this.encryptionKey!; + } + async initializeInternalAuthToken(): Promise { try { const envToken = process.env.INTERNAL_AUTH_TOKEN; @@ -142,7 +183,9 @@ class SystemCrypto { databaseLogger.error("Failed to initialize internal auth token", error, { operation: "internal_auth_init_failed", }); - throw new Error("Internal auth token initialization failed"); + throw new Error("Internal auth token initialization failed", { + cause: error, + }); } } @@ -186,7 +229,9 @@ class SystemCrypto { dataDir: process.env.DATA_DIR || "./db/data", }, ); - throw new Error("Credential sharing key initialization failed"); + throw new Error("Credential sharing key initialization failed", { + cause: error, + }); } } @@ -230,6 +275,23 @@ class SystemCrypto { }); } + private async generateAndGuideEncryptionKey(): Promise { + const newKey = crypto.randomBytes(32); + const newKeyHex = newKey.toString("hex"); + const instanceId = crypto.randomBytes(8).toString("hex"); + + this.encryptionKey = newKey; + + await this.updateEnvFile("ENCRYPTION_KEY", newKeyHex); + + databaseLogger.success("Encryption key auto-generated and saved to .env", { + operation: "encryption_key_auto_generated", + instanceId, + envVarName: "ENCRYPTION_KEY", + note: "Used to wrap session data keys - no restart required", + }); + } + private async generateAndGuideInternalAuthToken(): Promise { const newToken = crypto.randomBytes(32).toString("hex"); const instanceId = crypto.randomBytes(8).toString("hex"); diff --git a/src/backend/utils/user-agent-parser.ts b/src/backend/utils/user-agent-parser.ts index efd0b21e..b0644eca 100644 --- a/src/backend/utils/user-agent-parser.ts +++ b/src/backend/utils/user-agent-parser.ts @@ -255,16 +255,12 @@ function parseMacVersion(userAgent: string): string { * Ignores minor version numbers to handle browser auto-updates. */ export function generateDeviceFingerprint(deviceInfo: DeviceInfo): string { - let fingerprintString = ""; - - if (deviceInfo.type === "desktop") { - fingerprintString = `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}`; - } else if (deviceInfo.type === "mobile") { - fingerprintString = `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}`; - } else { - const browserMajor = deviceInfo.version.split(".")[0]; - fingerprintString = `${deviceInfo.type}|${deviceInfo.browser} ${browserMajor}|${deviceInfo.os}`; - } + const fingerprintString = + deviceInfo.type === "desktop" || deviceInfo.type === "mobile" + ? `${deviceInfo.type}|${deviceInfo.browser}|${deviceInfo.os}` + : `${deviceInfo.type}|${deviceInfo.browser} ${ + deviceInfo.version.split(".")[0] + }|${deviceInfo.os}`; return crypto.createHash("sha256").update(fingerprintString).digest("hex"); } diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index b536bcf7..f8fb1b32 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -265,6 +265,19 @@ class UserCrypto { return session.dataKey; } + restoreUserDataKey(userId: string, dataKey: Buffer, expiresAt: number): void { + const oldSession = this.userSessions.get(userId); + if (oldSession) { + oldSession.dataKey.fill(0); + } + + this.userSessions.set(userId, { + dataKey: Buffer.from(dataKey), + expiresAt, + lastActivity: Date.now(), + }); + } + logoutUser(userId: string): void { const session = this.userSessions.get(userId); if (session) { diff --git a/src/backend/utils/user-data-import.ts b/src/backend/utils/user-data-import.ts index bb1e14b9..7ee07c49 100644 --- a/src/backend/utils/user-data-import.ts +++ b/src/backend/utils/user-data-import.ts @@ -482,7 +482,7 @@ class UserDataImport { return await this.importUserData(targetUserId, exportData, options); } catch (error) { if (error instanceof SyntaxError) { - throw new Error("Invalid JSON format in import data"); + throw new Error("Invalid JSON format in import data", { cause: error }); } throw error; } diff --git a/src/components/ui/folder.tsx b/src/components/ui/folder.tsx new file mode 100644 index 00000000..fd254302 --- /dev/null +++ b/src/components/ui/folder.tsx @@ -0,0 +1,677 @@ +"use client"; +import React, { + useState, + useCallback, + createContext, + useContext, + useRef, + useEffect, +} from "react"; +import { + motion, + AnimatePresence, + easeInOut, + type Variants, +} from "motion/react"; +import { + ChevronRight, + Folder, + FolderOpen, + File, + type LucideIcon, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +const animationVariants: Variants = { + rootInitial: { opacity: 0, y: 20 }, + rootAnimate: { opacity: 1, y: 0 }, + itemInitial: { opacity: 0, x: -10 }, + itemAnimate: { opacity: 1, x: 0 }, + contentHidden: { opacity: 0, height: 0 }, + contentVisible: { opacity: 1, height: "auto" }, + chevronClosed: { rotate: 0 }, + chevronOpen: { rotate: 90 }, +}; + +const transitions = { + root: { duration: 0.4 }, + item: { duration: 0.2 }, + content: { duration: 0.3, ease: easeInOut }, + chevron: { duration: 0.2 }, +}; + +interface ExpansionContextType { + expandedIds: Set; + toggleExpanded: (id: string) => void; +} + +interface SelectionContextType { + selectedId: string | null; + setSelected: (id: string) => void; + onSelect?: (id: string, label: string) => void; +} + +interface TreeContextType { + focusedId: string | null; + setFocusedId: (id: string | null) => void; + treeId: string; + setKeyboardMode: (mode: boolean) => void; + keyboardMode: boolean; +} + +interface LevelContextType { + level: number; +} + +const ExpansionContext = createContext(null); +const SelectionContext = createContext(null); +const TreeContext = createContext(null); +const LevelContext = createContext({ level: 0 }); + +const useExpansion = () => { + const context = useContext(ExpansionContext); + if (!context) { + throw new Error( + "FolderTree components must be used within FolderTree.Root", + ); + } + return context; +}; + +const useSelection = () => { + const context = useContext(SelectionContext); + if (!context) { + throw new Error( + "FolderTree components must be used within FolderTree.Root", + ); + } + return context; +}; + +const useTree = () => { + const context = useContext(TreeContext); + if (!context) { + throw new Error( + "FolderTree components must be used within FolderTree.Root", + ); + } + return context; +}; + +const useLevel = () => { + return useContext(LevelContext); +}; + +const getPaddingClass = (level: number): string => { + const paddingMap: Record = { + 0: "pl-3", + 1: "pl-8", + 2: "pl-12", + 3: "pl-16", + 4: "pl-20", + 5: "pl-24", + 6: "pl-28", + 7: "pl-32", + }; + return paddingMap[level] || `pl-[${Math.min(level * 4 + 12, 48)}px]`; +}; + +interface CustomBadge { + content: React.ReactNode; + className?: string; + ariaLabel?: string; +} + +interface RootProps { + defaultExpanded?: string[]; + defaultSelected?: string; + selectedId?: string | null; + expandedIds?: Set; + onSelect?: (id: string, label: string) => void; + className?: string; + children: React.ReactNode; + id?: string; +} + +interface ItemProps { + id: string; + label: string; + icon?: LucideIcon; + badge?: string | number; + modified?: boolean | CustomBadge; + untracked?: boolean | CustomBadge; + className?: string; + children?: React.ReactNode; +} + +interface TriggerProps { + className?: string; +} + +interface ContentProps { + children: React.ReactNode; + className?: string; +} + +const Root: React.FC = ({ + defaultExpanded = [], + defaultSelected, + selectedId: controlledSelectedId, + expandedIds: additionalExpandedIds, + onSelect, + className = "", + children, + id = "folder-tree", +}) => { + const [expandedIds, setExpandedIds] = useState>( + new Set(defaultExpanded), + ); + const [internalSelectedId, setInternalSelectedId] = useState( + defaultSelected || null, + ); + const selectedId = + controlledSelectedId !== undefined + ? controlledSelectedId + : internalSelectedId; + const [focusedId, setFocusedId] = useState(null); + const [keyboardMode, setKeyboardMode] = useState(false); + const treeRef = useRef(null); + + useEffect(() => { + if (!additionalExpandedIds || additionalExpandedIds.size === 0) return; + setExpandedIds((prev) => { + const merged = new Set(prev); + let changed = false; + for (const id of additionalExpandedIds) { + if (!merged.has(id)) { + merged.add(id); + changed = true; + } + } + return changed ? merged : prev; + }); + }, [additionalExpandedIds]); + + const toggleExpanded = useCallback((id: string) => { + setExpandedIds((prev) => { + const newSet = new Set(prev); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + return newSet; + }); + }, []); + + const setSelected = useCallback((id: string) => { + setInternalSelectedId(id); + }, []); + + const getVisibleItemIds = useCallback(() => { + const items = Array.from( + treeRef.current?.querySelectorAll('[role="treeitem"]') || [], + ); + return items + .filter((item) => { + const element = item as HTMLElement; + return element.offsetHeight > 0 && element.offsetWidth > 0; + }) + .map((item) => item.getAttribute("data-id")) + .filter(Boolean) as string[]; + }, []); + + const getAllItemIds = useCallback(() => { + const items = Array.from( + treeRef.current?.querySelectorAll('[role="treeitem"]') || [], + ); + return items + .map((item) => item.getAttribute("data-id")) + .filter(Boolean) as string[]; + }, []); + + const [treeHasFocus, setTreeHasFocus] = useState(false); + + const handleTreeFocus = useCallback(() => { + if (!treeHasFocus) { + setTreeHasFocus(true); + setKeyboardMode(true); + } + }, [treeHasFocus]); + + const handleTreeBlur = useCallback((e: React.FocusEvent) => { + if (!treeRef.current?.contains(e.relatedTarget as Node)) { + setTreeHasFocus(false); + setFocusedId(null); + setKeyboardMode(false); + } + }, []); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + const getVisibleItems = () => { + return Array.from( + treeRef.current?.querySelectorAll('[role="treeitem"]') || [], + ).filter((item) => { + const element = item as HTMLElement; + return element.offsetHeight > 0 && element.offsetWidth > 0; + }); + }; + + if (e.key === "Tab") { + if (treeHasFocus && !focusedId) { + const visibleItemIds = getVisibleItemIds(); + if (visibleItemIds.length > 0) { + setFocusedId(visibleItemIds[0]); + e.preventDefault(); + return; + } + } + + if (focusedId) { + const visibleItems = getVisibleItems(); + const currentIndex = visibleItems.findIndex( + (item) => item.getAttribute("data-id") === focusedId, + ); + + if (e.shiftKey) { + if (currentIndex === 0) { + setFocusedId(null); + setTreeHasFocus(false); + setKeyboardMode(false); + return; + } + const nextIndex = Math.max(0, currentIndex - 1); + const nextItem = visibleItems[nextIndex] as HTMLElement; + const nextId = nextItem?.getAttribute("data-id"); + if (nextId) { + setFocusedId(nextId); + e.preventDefault(); + } + } else { + if (currentIndex === visibleItems.length - 1) { + setFocusedId(null); + setTreeHasFocus(false); + setKeyboardMode(false); + return; + } + const nextIndex = Math.min( + visibleItems.length - 1, + currentIndex + 1, + ); + const nextItem = visibleItems[nextIndex] as HTMLElement; + const nextId = nextItem?.getAttribute("data-id"); + if (nextId) { + setFocusedId(nextId); + e.preventDefault(); + } + } + } + return; + } + + if (!keyboardMode || !focusedId) return; + + const visibleItems = getVisibleItems(); + const currentIndex = visibleItems.findIndex( + (item) => item.getAttribute("data-id") === focusedId, + ); + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + if (currentIndex < visibleItems.length - 1) { + const nextItem = visibleItems[currentIndex + 1] as HTMLElement; + const nextId = nextItem.getAttribute("data-id"); + if (nextId) setFocusedId(nextId); + } + break; + case "ArrowUp": + e.preventDefault(); + if (currentIndex > 0) { + const prevItem = visibleItems[currentIndex - 1] as HTMLElement; + const prevId = prevItem.getAttribute("data-id"); + if (prevId) setFocusedId(prevId); + } + break; + case "ArrowRight": + e.preventDefault(); + if (!expandedIds.has(focusedId)) { + toggleExpanded(focusedId); + } + break; + case "ArrowLeft": + e.preventDefault(); + if (expandedIds.has(focusedId)) { + toggleExpanded(focusedId); + } + break; + case "Enter": + case " ": + e.preventDefault(); + setSelected(focusedId); + if (onSelect) { + const currentItem = visibleItems[currentIndex] as HTMLElement; + const label = + currentItem.querySelector("span:nth-of-type(2)")?.textContent || + ""; + onSelect(focusedId, label); + } + break; + } + }, + [ + focusedId, + keyboardMode, + expandedIds, + toggleExpanded, + setSelected, + onSelect, + getVisibleItemIds, + treeHasFocus, + ], + ); + + useEffect(() => { + const handleMouseDown = () => setKeyboardMode(false); + document.addEventListener("mousedown", handleMouseDown); + return () => { + document.removeEventListener("mousedown", handleMouseDown); + }; + }, []); + + const expansionValue: ExpansionContextType = { + expandedIds, + toggleExpanded, + }; + + const selectionValue: SelectionContextType = { + selectedId, + setSelected, + onSelect, + }; + + const treeValue: TreeContextType = { + focusedId, + setFocusedId, + treeId: id, + setKeyboardMode, + keyboardMode, + }; + + return ( + + + + + +
+ {children} +
+
+
+
+
+
+ ); +}; + +const ItemContext = createContext<{ + itemId: string; + hasChildren: boolean; + isExpanded: boolean; + toggleExpanded: () => void; +} | null>(null); + +const Item: React.FC = ({ + id, + label, + icon, + badge, + modified, + untracked, + className = "", + children, +}) => { + const expansionContext = useExpansion(); + const selectionContext = useSelection(); + const treeContext = useTree(); + const { level } = useLevel(); + const itemRef = useRef(null); + const keyboardMode = treeContext.keyboardMode; + + const hasChildren = React.Children.count(children) > 0; + const isExpanded = expansionContext.expandedIds.has(id); + const isSelected = selectionContext.selectedId === id; + const isFocused = treeContext.focusedId === id; + + const handleItemClick = useCallback(() => { + treeContext.setKeyboardMode(false); + selectionContext.setSelected(id); + treeContext.setFocusedId(id); + if (selectionContext.onSelect) { + selectionContext.onSelect(id, label); + } + }, [id, label, selectionContext, treeContext]); + + const toggleExpanded = useCallback(() => { + if (hasChildren) { + expansionContext.toggleExpanded(id); + } + }, [id, hasChildren, expansionContext]); + + const handleFocus = useCallback(() => { + treeContext.setFocusedId(id); + }, [id, treeContext]); + + useEffect(() => { + if (isFocused && itemRef.current) { + itemRef.current.focus(); + } + }, [isFocused]); + + const IconComponent = + icon || (hasChildren ? (isExpanded ? FolderOpen : Folder) : File); + + const itemContextValue = { + itemId: id, + hasChildren, + isExpanded, + toggleExpanded, + }; + + const renderBadge = ( + badgeData: boolean | CustomBadge | undefined, + defaultContent: string, + defaultClassName: string, + ) => { + if (!badgeData) return null; + + if (typeof badgeData === "boolean") { + return ( + + {defaultContent} + + ); + } + + return ( + + {badgeData.content} + + ); + }; + + return ( + + +
+ { + handleItemClick(); + e.stopPropagation(); + toggleExpanded(); + }} + onFocus={handleFocus} + role="treeitem" + tabIndex={isFocused ? 0 : -1} + aria-expanded={hasChildren ? isExpanded : undefined} + aria-selected={isSelected} + aria-label={`${hasChildren ? "Folder" : "File"}: ${label}`} + aria-level={level + 1} + > + {hasChildren && ( + + )} + {!hasChildren && + {children} +
+
+
+ ); +}; + +const Trigger: React.FC = ({ className = "" }) => { + const itemContext = useContext(ItemContext); + if (!itemContext || !itemContext.hasChildren) { + return null; + } + + return ( + { + e.stopPropagation(); + itemContext.toggleExpanded(); + }} + role="button" + aria-label={itemContext.isExpanded ? "Collapse" : "Expand"} + tabIndex={-1} + > + + + ); +}; + +const Content: React.FC = ({ children, className = "" }) => { + const itemContext = useContext(ItemContext); + if (!itemContext) { + return <>{children}; + } + + const hasContent = React.Children.count(children) > 0; + + return ( + + {hasContent && itemContext.isExpanded && ( + + {children} + + )} + + ); +}; + +const FolderTree = { + Root, + Item, + Trigger, + Content, +}; + +export default FolderTree; diff --git a/src/components/ui/kbd.tsx b/src/components/ui/kbd.tsx index 44fefd02..ab78efd3 100644 --- a/src/components/ui/kbd.tsx +++ b/src/components/ui/kbd.tsx @@ -1,28 +1,59 @@ +import { type ComponentProps, type ReactNode } from "react"; import { cn } from "@/lib/utils"; -function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { - return ( - - ); -} +export type KbdProps = ComponentProps<"span"> & { + children: ReactNode; +}; -function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { - return ( - - ); -} +export const Kbd = ({ className, children, ...props }: KbdProps) => ( + + {children} + +); -export { Kbd, KbdGroup }; +export type KbdKeyProps = ComponentProps<"span"> & { + "aria-label"?: string; + className?: string; +}; + +export const KbdKey = ({ className, children, ...props }: KbdKeyProps) => ( + + {children} + +); + +export type KbdSeparatorProps = ComponentProps<"span"> & { + children?: ReactNode; + className?: string; +}; + +export const KbdSeparator = ({ + className, + children = "+", + ...props +}: KbdSeparatorProps) => ( + + {children} + +); diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx index 2835db13..6287ded1 100644 --- a/src/components/ui/resizable.tsx +++ b/src/components/ui/resizable.tsx @@ -1,15 +1,19 @@ import * as React from "react"; import { GripVerticalIcon } from "lucide-react"; -import * as ResizablePrimitive from "react-resizable-panels"; +import { + Group as ResizableGroup, + Panel as ResizablePrimitivePanel, + Separator as ResizableSeparator, +} from "react-resizable-panels"; import { cn } from "@/lib/utils"; function ResizablePanelGroup({ className, ...props -}: React.ComponentProps) { +}: React.ComponentProps) { return ( - ) { - return ; +}: React.ComponentProps) { + return ; } function ResizableHandle({ withHandle, className, ...props -}: React.ComponentProps & { +}: React.ComponentProps & { withHandle?: boolean; }) { return ( - div]:rotate-90 bg-edge-hover hover:bg-interact active:bg-pressed transition-colors duration-150", @@ -47,7 +51,7 @@ function ResizableHandle({ )} - + ); } diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index a1d07285..8413b712 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -598,9 +598,9 @@ function SidebarMenuSkeleton({ showIcon?: boolean; }) { // Random width between 50 to 90%. - const width = React.useMemo(() => { + const [width] = React.useState(() => { return `${Math.floor(Math.random() * 40) + 50}%`; - }, []); + }); return (
+ + {t("versionCheck.betaVersion")} + + {t("versionCheck.betaVersionDesc", { + current: updateInfo.localVersion, + latest: updateInfo.remoteVersion, + })} + + + ); + } + if (updateInfo.status === "requires_update") { return ( diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts index 41d1424f..16013f7f 100644 --- a/src/hooks/use-confirmation.ts +++ b/src/hooks/use-confirmation.ts @@ -144,7 +144,7 @@ export function useConfirmation() { setPendingConfirmCallback(null); setPendingResolve(null); }, - } as any); + } as NonNullable[1]>); if (confirmOnEnter) { setActiveToastId(toastId); diff --git a/src/hooks/use-mobile.ts b/src/hooks/use-mobile.ts index a93d5839..ae226bd0 100644 --- a/src/hooks/use-mobile.ts +++ b/src/hooks/use-mobile.ts @@ -3,8 +3,9 @@ import * as React from "react"; const MOBILE_BREAKPOINT = 768; export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState( - undefined, + const [isMobile, setIsMobile] = React.useState( + () => + typeof window !== "undefined" && window.innerWidth < MOBILE_BREAKPOINT, ); React.useEffect(() => { @@ -13,9 +14,8 @@ export function useIsMobile() { setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); }; mql.addEventListener("change", onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); return () => mql.removeEventListener("change", onChange); }, []); - return !!isMobile; + return isMobile; } diff --git a/src/hooks/use-service-worker.ts b/src/hooks/use-service-worker.ts index f7d12781..6f71700b 100644 --- a/src/hooks/use-service-worker.ts +++ b/src/hooks/use-service-worker.ts @@ -1,5 +1,5 @@ import { useEffect, useState, useCallback } from "react"; -import { isElectron } from "@/ui/main-axios"; +import { isElectron } from "@/lib/electron"; import { getBasePath } from "@/lib/base-path"; interface ServiceWorkerState { @@ -40,27 +40,54 @@ export function useServiceWorker(): ServiceWorkerState { if (!isSupported) return; + const shouldReloadOnControllerChange = Boolean( + navigator.serviceWorker.controller, + ); + let hasReloadedForUpdate = false; + const handleControllerChange = () => { + if (!shouldReloadOnControllerChange || hasReloadedForUpdate) { + return; + } + + hasReloadedForUpdate = true; + window.location.reload(); + }; + const registerSW = async () => { try { const registration = await navigator.serviceWorker.register( `${getBasePath()}/sw.js`, + { updateViaCache: "none" }, ); setState((prev) => ({ ...prev, isRegistered: true })); registration.addEventListener("updatefound", () => handleUpdateFound(registration), ); + await registration.update(); } catch (error) { console.error("[SW] Registration failed:", error); } }; + navigator.serviceWorker.addEventListener( + "controllerchange", + handleControllerChange, + ); + if (document.readyState === "complete") { registerSW(); } else { window.addEventListener("load", registerSW); - return () => window.removeEventListener("load", registerSW); } + + return () => { + window.removeEventListener("load", registerSW); + navigator.serviceWorker.removeEventListener( + "controllerchange", + handleControllerChange, + ); + }; }, [handleUpdateFound]); return state; diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index c15e938e..392d1fe0 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -1,84 +1,82 @@ -import i18n from "i18next"; +import i18n, { type BackendModule, type ResourceKey } from "i18next"; import { initReactI18next } from "react-i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import enTranslation from "../locales/en.json"; -import afTranslation from "../locales/translated/af_ZA.json"; -import arTranslation from "../locales/translated/ar_SA.json"; -import bnTranslation from "../locales/translated/bn_BD.json"; -import bgTranslation from "../locales/translated/bg_BG.json"; -import caTranslation from "../locales/translated/ca_ES.json"; -import csTranslation from "../locales/translated/cs_CZ.json"; -import daTranslation from "../locales/translated/da_DK.json"; -import deTranslation from "../locales/translated/de_DE.json"; -import elTranslation from "../locales/translated/el_GR.json"; -import esESTranslation from "../locales/translated/es_ES.json"; -import fiTranslation from "../locales/translated/fi_FI.json"; -import frTranslation from "../locales/translated/fr_FR.json"; -import heTranslation from "../locales/translated/he_IL.json"; -import hiTranslation from "../locales/translated/hi_IN.json"; -import huTranslation from "../locales/translated/hu_HU.json"; -import idTranslation from "../locales/translated/id_ID.json"; -import itTranslation from "../locales/translated/it_IT.json"; -import jaTranslation from "../locales/translated/ja_JP.json"; -import koTranslation from "../locales/translated/ko_KR.json"; -import nlTranslation from "../locales/translated/nl_NL.json"; -import noTranslation from "../locales/translated/no_NO.json"; -import plTranslation from "../locales/translated/pl_PL.json"; -import ptPTTranslation from "../locales/translated/pt_PT.json"; -import ptBRTranslation from "../locales/translated/pt_BR.json"; -import roTranslation from "../locales/translated/ro_RO.json"; -import ruTranslation from "../locales/translated/ru_RU.json"; -import srTranslation from "../locales/translated/sr_SP.json"; -import svSETranslation from "../locales/translated/sv_SE.json"; -import thTranslation from "../locales/translated/th_TH.json"; -import trTranslation from "../locales/translated/tr_TR.json"; -import ukTranslation from "../locales/translated/uk_UA.json"; -import viTranslation from "../locales/translated/vi_VN.json"; -import zhCNTranslation from "../locales/translated/zh_CN.json"; -import zhTWTranslation from "../locales/translated/zh_TW.json"; + +type LocaleModule = { default: ResourceKey }; + +const localeLoaders = { + af: () => import("../locales/translated/af_ZA.json"), + ar: () => import("../locales/translated/ar_SA.json"), + bn: () => import("../locales/translated/bn_BD.json"), + bg: () => import("../locales/translated/bg_BG.json"), + ca: () => import("../locales/translated/ca_ES.json"), + cs: () => import("../locales/translated/cs_CZ.json"), + da: () => import("../locales/translated/da_DK.json"), + de: () => import("../locales/translated/de_DE.json"), + el: () => import("../locales/translated/el_GR.json"), + "es-ES": () => import("../locales/translated/es_ES.json"), + fi: () => import("../locales/translated/fi_FI.json"), + fr: () => import("../locales/translated/fr_FR.json"), + he: () => import("../locales/translated/he_IL.json"), + hi: () => import("../locales/translated/hi_IN.json"), + hu: () => import("../locales/translated/hu_HU.json"), + id: () => import("../locales/translated/id_ID.json"), + it: () => import("../locales/translated/it_IT.json"), + ja: () => import("../locales/translated/ja_JP.json"), + ko: () => import("../locales/translated/ko_KR.json"), + nl: () => import("../locales/translated/nl_NL.json"), + no: () => import("../locales/translated/no_NO.json"), + pl: () => import("../locales/translated/pl_PL.json"), + "pt-PT": () => import("../locales/translated/pt_PT.json"), + "pt-BR": () => import("../locales/translated/pt_BR.json"), + ro: () => import("../locales/translated/ro_RO.json"), + ru: () => import("../locales/translated/ru_RU.json"), + sr: () => import("../locales/translated/sr_SP.json"), + "sv-SE": () => import("../locales/translated/sv_SE.json"), + th: () => import("../locales/translated/th_TH.json"), + tr: () => import("../locales/translated/tr_TR.json"), + uk: () => import("../locales/translated/uk_UA.json"), + vi: () => import("../locales/translated/vi_VN.json"), + "zh-CN": () => import("../locales/translated/zh_CN.json"), + "zh-TW": () => import("../locales/translated/zh_TW.json"), +} satisfies Record Promise>; + +const supportedLngs = ["en", ...Object.keys(localeLoaders)]; + +const localeBackend: BackendModule = { + type: "backend", + init: () => {}, + read: (language, _namespace, callback) => { + if (language === "en") { + callback(null, enTranslation); + return; + } + + const loadLocale = localeLoaders[language]; + if (!loadLocale) { + callback(new Error(`Unsupported language: ${language}`), false); + return; + } + + loadLocale() + .then((module) => callback(null, module.default)) + .catch((error: unknown) => { + callback( + error instanceof Error ? error : new Error(String(error)), + false, + ); + }); + }, +}; i18n + .use(localeBackend) .use(LanguageDetector) .use(initReactI18next) .init({ - supportedLngs: [ - "en", - "af", - "ar", - "bn", - "bg", - "ca", - "cs", - "da", - "de", - "el", - "es-ES", - "fi", - "fr", - "he", - "hi", - "hu", - "id", - "it", - "ja", - "ko", - "nl", - "no", - "pl", - "pt-PT", - "pt-BR", - "ro", - "ru", - "sr", - "sv-SE", - "th", - "tr", - "uk", - "vi", - "zh-CN", - "zh-TW", - ], + supportedLngs, fallbackLng: "en", debug: false, @@ -94,109 +92,8 @@ i18n en: { translation: enTranslation, }, - af: { - translation: afTranslation, - }, - ar: { - translation: arTranslation, - }, - bn: { - translation: bnTranslation, - }, - bg: { - translation: bgTranslation, - }, - ca: { - translation: caTranslation, - }, - cs: { - translation: csTranslation, - }, - da: { - translation: daTranslation, - }, - de: { - translation: deTranslation, - }, - el: { - translation: elTranslation, - }, - "es-ES": { - translation: esESTranslation, - }, - fi: { - translation: fiTranslation, - }, - fr: { - translation: frTranslation, - }, - he: { - translation: heTranslation, - }, - hi: { - translation: hiTranslation, - }, - hu: { - translation: huTranslation, - }, - id: { - translation: idTranslation, - }, - it: { - translation: itTranslation, - }, - ja: { - translation: jaTranslation, - }, - ko: { - translation: koTranslation, - }, - nl: { - translation: nlTranslation, - }, - no: { - translation: noTranslation, - }, - pl: { - translation: plTranslation, - }, - "pt-PT": { - translation: ptPTTranslation, - }, - "pt-BR": { - translation: ptBRTranslation, - }, - ro: { - translation: roTranslation, - }, - ru: { - translation: ruTranslation, - }, - sr: { - translation: srTranslation, - }, - "sv-SE": { - translation: svSETranslation, - }, - th: { - translation: thTranslation, - }, - tr: { - translation: trTranslation, - }, - uk: { - translation: ukTranslation, - }, - vi: { - translation: viTranslation, - }, - "zh-CN": { - translation: zhCNTranslation, - }, - "zh-TW": { - translation: zhTWTranslation, - }, }, + partialBundledLanguages: true, interpolation: { escapeValue: false, diff --git a/src/index.css b/src/index.css index 04a5e82b..2e5cd2d2 100644 --- a/src/index.css +++ b/src/index.css @@ -553,3 +553,7 @@ .skinny-scrollbar::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); } + +.h-fade { + mask-image: linear-gradient(transparent, #000 6%, #000 94%, transparent); +} diff --git a/src/lib/client-cache-version.ts b/src/lib/client-cache-version.ts new file mode 100644 index 00000000..866df712 --- /dev/null +++ b/src/lib/client-cache-version.ts @@ -0,0 +1,45 @@ +const CLIENT_CACHE_VERSION_KEY = "termix_client_cache_version"; +const CURRENT_CLIENT_VERSION = import.meta.env.VITE_APP_VERSION || "0.0.0"; + +async function clearCacheStorage(): Promise { + if (!("caches" in window)) return; + + const cacheNames = await caches.keys(); + await Promise.all(cacheNames.map((name) => caches.delete(name))); +} + +async function clearServiceWorkers(): Promise { + if (!("serviceWorker" in navigator)) return; + + const registrations = await navigator.serviceWorker.getRegistrations(); + await Promise.all( + registrations.map((registration) => registration.unregister()), + ); +} + +function storeCurrentVersion(): void { + try { + localStorage.setItem(CLIENT_CACHE_VERSION_KEY, CURRENT_CLIENT_VERSION); + } catch { + // expected - storage can be unavailable in restricted contexts + } +} + +export async function prepareClientCacheVersion(): Promise { + if (typeof window === "undefined") return; + + let storedVersion: string | null = null; + try { + storedVersion = localStorage.getItem(CLIENT_CACHE_VERSION_KEY); + } catch { + storedVersion = null; + } + + if (storedVersion === CURRENT_CLIENT_VERSION) { + return; + } + + await Promise.allSettled([clearCacheStorage(), clearServiceWorkers()]); + + storeCurrentVersion(); +} diff --git a/src/lib/clipboard-provider.ts b/src/lib/clipboard-provider.ts index 9254fbe2..2266f401 100644 --- a/src/lib/clipboard-provider.ts +++ b/src/lib/clipboard-provider.ts @@ -12,6 +12,12 @@ export class RobustClipboardProvider implements IClipboardProvider { if (this.pendingWrite !== null) { const text = this.pendingWrite; this.pendingWrite = null; + if (window.electronClipboard) { + window.electronClipboard.writeText(text).catch(() => { + this.pendingWrite = text; + }); + return; + } navigator.clipboard.writeText(text).catch(() => { this.pendingWrite = text; }); @@ -25,18 +31,20 @@ export class RobustClipboardProvider implements IClipboardProvider { this.pendingWrite = null; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - readText(selection: ClipboardSelectionType): string { - return ""; + readText(_selection: ClipboardSelectionType): string | Promise { + if (window.electronClipboard) { + return window.electronClipboard.readText(); + } + return navigator.clipboard?.readText?.() ?? ""; } async writeText( - selection: ClipboardSelectionType, + _selection: ClipboardSelectionType, text: string, ): Promise { try { if (window.electronClipboard) { - window.electronClipboard.writeText(text); + await window.electronClipboard.writeText(text); return; } await navigator.clipboard.writeText(text); diff --git a/src/lib/db-health-monitor.ts b/src/lib/db-health-monitor.ts index 8f6599c2..1c9a9664 100644 --- a/src/lib/db-health-monitor.ts +++ b/src/lib/db-health-monitor.ts @@ -14,7 +14,18 @@ * to reflect the current UX contract: users can keep working regardless * of backend hiccups and are simply informed via a toast. */ -type EventListener = (...args: any[]) => void; +type EventListener = (...args: unknown[]) => void; + +interface HttpLikeError { + message?: string; + code?: string; + response?: { + data?: { + error?: string; + code?: string; + }; + }; +} class DatabaseHealthMonitor { private static instance: DatabaseHealthMonitor; @@ -47,7 +58,7 @@ class DatabaseHealthMonitor { } } - private emit(event: string, ...args: any[]): void { + private emit(event: string, ...args: unknown[]): void { const eventListeners = this.listeners.get(event); if (eventListeners) { eventListeners.forEach((listener) => listener(...args)); @@ -58,9 +69,11 @@ class DatabaseHealthMonitor { this.emit("session-expired", { timestamp: Date.now() }); } - reportDatabaseError(error: any, _wasAuthenticated: boolean = false) { - const errorMessage = error?.response?.data?.error || error?.message || ""; - const errorCode = error?.response?.data?.code || error?.code; + reportDatabaseError(error: unknown) { + const errorLike = error as HttpLikeError; + const errorMessage = + errorLike.response?.data?.error || errorLike.message || ""; + const errorCode = errorLike.response?.data?.code || errorLike.code; const lowerMessage = errorMessage.toLowerCase(); const isDatabaseError = @@ -78,7 +91,7 @@ class DatabaseHealthMonitor { errorCode === "ETIMEDOUT" || errorCode === "ERR_CANCELED" || (lowerMessage.includes("network error") && - error?.response === undefined) || + errorLike.response === undefined) || lowerMessage.includes("request aborted") || lowerMessage.includes("timeout"); diff --git a/src/lib/electron.ts b/src/lib/electron.ts new file mode 100644 index 00000000..87ede70d --- /dev/null +++ b/src/lib/electron.ts @@ -0,0 +1,18 @@ +type ElectronWindow = Window & + typeof globalThis & { + IS_ELECTRON?: boolean; + electronAPI?: { + isElectron?: boolean; + }; + }; + +export function isElectron(): boolean { + if (typeof window === "undefined") return false; + + const win = window as ElectronWindow; + const hasISElectron = win.IS_ELECTRON === true; + const hasElectronAPI = !!win.electronAPI; + const isElectronProp = win.electronAPI?.isElectron === true; + + return hasISElectron || hasElectronAPI || isElectronProp; +} diff --git a/src/locales/en.json b/src/locales/en.json index 94f9414a..35579388 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -402,6 +402,8 @@ "currentVersion": "You are running version {{version}}", "updateAvailable": "Update Available", "newVersionAvailable": "A new version is available! You are running {{current}}, but {{latest}} is available.", + "betaVersion": "Beta Version", + "betaVersionDesc": "You are running {{current}}, which is newer than the latest stable release {{latest}}.", "releasedOn": "Released on {{date}}", "downloadUpdate": "Download Update", "dismiss": "Dismiss", @@ -426,6 +428,8 @@ "warning": "Warning", "info": "Info", "success": "Success", + "unsavedChanges": "Unsaved changes", + "dismiss": "Dismiss", "loading": "Loading...", "required": "Required", "optional": "Optional", @@ -471,6 +475,7 @@ "chinese": "Chinese", "german": "German", "cancel": "Cancel", + "done": "Done", "username": "Username", "name": "Name", "login": "Login", @@ -486,6 +491,7 @@ "save": "Save", "saving": "Saving...", "delete": "Delete", + "rename": "Rename", "edit": "Edit", "add": "Add", "search": "Search", @@ -559,7 +565,8 @@ "passwordCopied": "Password copied to clipboard", "sudoPasswordCopied": "Sudo password copied to clipboard", "noPasswordAvailable": "No password available", - "failedToCopyPassword": "Failed to copy password" + "failedToCopyPassword": "Failed to copy password", + "openFileManager": "Open File Manager" }, "admin": { "title": "Admin Settings", @@ -876,7 +883,42 @@ "passwordMinLength": "Password must be at least 6 characters", "currentRoles": "Current Roles", "noRolesAssigned": "No roles assigned", - "assignNewRole": "Assign New Role" + "assignNewRole": "Assign New Role", + "apiKeys": { + "tabLabel": "API Keys", + "title": "API Keys", + "createApiKey": "Create API Key", + "createApiKeyDescription": "Create a new API key scoped to a specific user. The token is shown only once.", + "keyCreated": "API Key Created", + "keyCreatedDescription": "Copy this key now — it will not be shown again.", + "keyName": "Key Name", + "keyNamePlaceholder": "e.g. CI/CD Pipeline", + "scopedUser": "Scoped User", + "selectUser": "Select a user...", + "searchUsers": "Search users...", + "noUsersFound": "No users found.", + "expiresAt": "Expires At", + "optional": "optional", + "expiresAtHelp": "Leave empty for a key that never expires.", + "copyWarningTitle": "Save your API key", + "copyWarningDescription": "This key will only be shown once. Store it in a safe place.", + "apiKey": "API Key", + "tokenCopied": "Token copied to clipboard", + "creating": "Creating...", + "nameRequired": "Key name is required", + "userRequired": "Please select a user", + "failedToCreate": "Failed to create API key", + "noKeys": "No API keys found.", + "name": "Name", + "prefix": "Prefix", + "lastUsed": "Last Used", + "never": "Never", + "revokeKey": "Revoke key", + "confirmRevoke": "Are you sure you want to revoke the API key \"{{name}}\"? This cannot be undone.", + "revokedSuccessfully": "API key revoked successfully", + "failedToRevoke": "Failed to revoke API key", + "failedToFetch": "Failed to fetch API keys" + } }, "hosts": { "title": "Host Manager", @@ -970,28 +1012,8 @@ "enableDocker": "Enable Docker", "defaultPath": "Default Path", "defaultPathDesc": "Default directory when opening file manager for this host", - "tunnelConnections": "Tunnel Connections", "connection": "Connection", "remove": "Remove", - "sourcePort": "Source Port", - "sourcePortDesc": " (Source refers to the Current Connection Details in the General tab)", - "endpointPort": "Endpoint Port", - "endpointSshConfig": "Endpoint SSH Configuration", - "tunnelForwardDescription": "This tunnel will forward traffic from port {{sourcePort}} on the source machine (current connection details in general tab) to port {{endpointPort}} on the endpoint machine.", - "maxRetries": "Max Retries", - "maxRetriesDescription": "Maximum number of retry attempts for tunnel connection.", - "retryInterval": "Retry Interval (seconds)", - "retryIntervalDescription": "Time to wait between retry attempts.", - "autoStartContainer": "Auto Start on Container Launch", - "autoStartDesc": "Automatically start this tunnel when the container launches", - "addConnection": "Add Tunnel Connection", - "tunnelType": "Tunnel Type", - "tunnelTypeLocal": "Local (-L)", - "tunnelTypeRemote": "Remote (-R)", - "tunnelTypeLocalDesc": "Forward local port to remote endpoint", - "tunnelTypeRemoteDesc": "Forward remote port to local machine", - "tunnelForwardDescriptionLocal": "This tunnel will forward traffic from local port {{sourcePort}} to port {{endpointPort}} on the endpoint machine.", - "tunnelForwardDescriptionRemote": "This tunnel will forward traffic from port {{sourcePort}} on the source machine (current connection details in general tab) to port {{endpointPort}} on the endpoint machine.", "sshpassRequired": "Sshpass Required For Password Authentication", "sshpassRequiredDesc": "For password authentication in tunnels, sshpass must be installed on the system.", "otherInstallMethods": "Other installation methods:", @@ -1656,6 +1678,7 @@ "automaticFallback": "Automatically trying {{method}} authentication...", "totpTimeout": "TOTP verification timeout. Please reconnect.", "passwordTimeout": "Password verification timeout. Please reconnect.", + "sessionEnded": "Session ended.", "connectionRejected": "Connection rejected by server. Please check your authentication and network configuration.", "hostKeyRejected": "SSH host key verification rejected. Connection cancelled.", "sessionTakenOver": "Session was opened in another tab. Reconnecting...", @@ -1993,12 +2016,10 @@ "ascending": "Ascending", "descending": "Descending" }, - "tunnel": { - "noTunnelsConfigured": "No Tunnels Configured", - "configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started" - }, "tunnels": { "title": "SSH Tunnels", + "noTunnelsConfigured": "No Tunnels Configured", + "configureTunnelsInHostSettings": "Configure tunnel connections in the Host Manager to get started", "noSshTunnels": "No SSH Tunnels", "createFirstTunnelMessage": "You haven't created any SSH tunnels yet. Configure tunnel connections in the Host Manager to get started.", "connected": "Connected", @@ -2019,27 +2040,100 @@ "disconnect": "Disconnect", "cancel": "Cancel", "port": "Port", + "localPort": "Local Port", + "remotePort": "Remote Port", + "currentHostPort": "Current Host Port", + "endpointPort": "Endpoint Port", + "bindIp": "Local IP", + "currentHostIp": "Current Host IP", + "endpointSshConfig": "Endpoint SSH Configuration", + "endpointSshConfigRequired": "Endpoint SSH configuration is required", + "endpointSshHost": "Endpoint SSH Host", + "endpointSshHostPlaceholder": "Select a configured host", + "endpointSshHostRequired": "Select an endpoint SSH host for each client tunnel.", "attempt": "Attempt {{current}} of {{max}}", "nextRetryIn": "Next retry in {{seconds}} seconds", "checkDockerLogs": "Check your Docker logs for the error reason, join the", "orCreate": "or create a ", "noTunnelConnections": "No tunnel connections configured", "tunnelConnections": "Tunnel Connections", + "serverTunnels": "Server Tunnels", + "serverTunnelsDesc": "Backend-managed tunnels stored with this host.", + "clientTunnels": "Client Tunnels", + "clientTunnelsUnavailable": "Client tunnels require a desktop client.", + "serverTunnel": "Server Tunnel", + "clientTunnel": "Client Tunnel", + "addServerTunnel": "Add Server Tunnel", + "addClientTunnel": "Add Client Tunnel", + "noServerTunnels": "No server tunnels configured.", + "noClientTunnels": "No client tunnels configured on this desktop.", + "manageClientTunnels": "Manage Client Tunnels", "addTunnel": "Add Tunnel", "editTunnel": "Edit Tunnel", "deleteTunnel": "Delete Tunnel", "tunnelName": "Tunnel Name", - "localPort": "Local Port", "remoteHost": "Remote Host", - "remotePort": "Remote Port", "autoStart": "Auto Start", + "autoStartContainer": "Auto Start on Launch", + "autoStartContainerDesc": "Automatically start this tunnel when your Termix server launches.", + "autoStartEnableFailed": "Host saved, but failed to start auto-start tunnels for {{name}}.", + "clientAutoStartDesc": "Starts when this desktop client opens and stays connected.", + "clientManualStartDesc": "Use Start and Stop from this row. Termix will not open it automatically.", + "clientRemoteServerNote": "Remote forwarding may require AllowTcpForwarding and GatewayPorts on the endpoint SSH server. The remote port closes when this desktop disconnects.", + "clientTunnelStarted": "Client tunnel started", + "clientTunnelStopped": "Client tunnel stopped", + "tunnelTestSucceeded": "Tunnel test succeeded", + "tunnelTestFailed": "Tunnel test failed", + "localSaved": "Client tunnels saved", + "localSaveError": "Failed to save local client tunnels", + "invalidBindIp": "Local IP must be a valid IPv4 address.", + "invalidLocalTargetIp": "Local target IP must be a valid IPv4 address.", + "invalidCurrentHostIp": "Current Host IP must be a valid IPv4 address.", + "invalidLocalPort": "Local port must be between 1 and 65535.", + "invalidRemotePort": "Remote port must be between 1 and 65535.", + "invalidLocalTargetPort": "Local target port must be between 1 and 65535.", + "invalidEndpointPort": "Endpoint port must be between 1 and 65535.", + "duplicateAutoStartBind": "Only one auto-start client tunnel can use {{bind}}.", + "manualControlError": "Failed to update tunnel state.", + "saveHostBeforeManualControl": "Save this host before starting or stopping its server tunnels.", "status": "Status", "active": "Active", "inactive": "Inactive", "start": "Start", "stop": "Stop", + "test": "Test", "restart": "Restart", "connectionType": "Connection Type", + "type": "Tunnel Type", + "typeLocal": "Local (-L)", + "typeRemote": "Remote (-R)", + "typeDynamic": "Dynamic (-D)", + "typeServerLocalDesc": "Current host to endpoint.", + "typeServerRemoteDesc": "Endpoint back to current host.", + "typeClientLocalDesc": "Local computer to endpoint.", + "typeClientRemoteDesc": "Endpoint back to local computer.", + "typeClientDynamicDesc": "SOCKS on local computer.", + "typeDynamicDesc": "Forward SOCKS5 CONNECT traffic through SSH", + "forwardDescriptionServerLocal": "Current host {{sourcePort}} → endpoint {{endpointPort}}.", + "forwardDescriptionServerRemote": "Endpoint {{endpointPort}} → current host {{sourcePort}}.", + "forwardDescriptionServerDynamic": "SOCKS on current host {{sourcePort}}.", + "forwardDescriptionClientLocal": "Local {{sourcePort}} → remote {{endpointPort}}.", + "forwardDescriptionClientRemote": "Remote {{sourcePort}} → local {{endpointPort}}.", + "forwardDescriptionClientDynamic": "SOCKS on local port {{sourcePort}}.", + "summaryClientLocal": "{{localHost}}:{{localPort}} → {{endpoint}}:{{remotePort}}", + "summaryClientRemote": "{{endpoint}}:{{remotePort}} → {{localHost}}:{{localPort}}", + "summaryClientDynamic": "{{localHost}}:{{localPort}} → SOCKS via {{endpoint}}", + "autoNameClientLocal": "Local {{localPort}} → {{endpoint}} {{remotePort}}", + "autoNameClientRemote": "{{endpoint}} {{remotePort}} → local {{localPort}}", + "autoNameClientDynamic": "SOCKS {{localPort}} via {{endpoint}}", + "route": "Route:", + "lastStarted": "Last started", + "lastTested": "Last tested", + "lastError": "Last error", + "maxRetries": "Max Retries", + "maxRetriesDescription": "Maximum amount of retry attempts.", + "retryInterval": "Retry Interval (seconds)", + "retryIntervalDescription": "Time to wait between retry attempts.", "local": "Local", "remote": "Remote", "dynamic": "Dynamic", @@ -2223,6 +2317,8 @@ "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.", + "passphraseRequired": "Passphrase Required", + "passphraseRequiredDescription": "The SSH key is encrypted. Please enter the passphrase to unlock it.", "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", @@ -2297,7 +2393,7 @@ "failedCompleteReset": "Failed to complete password reset", "invalidTotpCode": "Invalid TOTP code", "failedOidcLogin": "Failed to start OIDC login", - "failedUserInfo": "Failed to get user info after OIDC login", + "failedUserInfo": "Failed to get user info after login", "oidcAuthFailed": "OIDC authentication failed", "noTokenReceived": "No token received from login", "invalidAuthUrl": "Invalid authorization URL received from backend", @@ -2373,6 +2469,23 @@ "showHostTagsDesc": "Display tags under each host in the sidebar. Disable to hide all tags.", "account": "Account", "appearance": "Appearance", + "c2sTunnelConfigDesc": "Local desktop tunnels targeting configured SSH hosts.", + "c2sTunnelPresets": "Client Tunnel Presets", + "c2sTunnelPresetsDesc": "Save this desktop client's local tunnel list as a named server preset, or load a preset back into this client.", + "c2sTunnelPresetsUnavailable": "Client tunnel presets are only available in the desktop client.", + "c2sPresetName": "Preset Name", + "c2sPresetNamePlaceholder": "Client preset name", + "c2sPresetToLoad": "Preset To Load", + "c2sNoPresetSelected": "No preset selected", + "c2sNoPresets": "No presets saved", + "c2sLoadPreset": "Load", + "c2sCurrentLocalConfig": "{{count}} local client tunnel(s) configured on this desktop.", + "c2sPresetSyncNote": "Presets are explicit snapshots; loading one replaces this desktop client's local client tunnel list.", + "c2sPresetSaved": "Client tunnel preset saved", + "c2sPresetLoaded": "Client tunnel preset loaded locally", + "c2sPresetRenamed": "Client tunnel preset renamed", + "c2sPresetDeleted": "Client tunnel preset deleted", + "c2sPresetLoadError": "Failed to load client tunnel presets", "languageLocalization": "Language & Localization", "fileManagerSettings": "File Manager", "terminalSettings": "Terminal", @@ -2421,6 +2534,10 @@ "description": "SSH credential description", "searchCredentials": "Search credentials by name, username, or tags...", "sshConfig": "endpoint ssh configuration", + "bindLocalhost": "127.0.0.1 (bind to localhost)", + "localListenerHost": "127.0.0.1 (listen locally)", + "localTargetHost": "127.0.0.1 (target on this computer)", + "socksListenerHost": "127.0.0.1 (SOCKS listener)", "homePath": "/home", "clientId": "your-client-id", "clientSecret": "your-client-secret", @@ -2600,6 +2717,7 @@ "version": "Version", "upToDate": "Up to Date", "updateAvailable": "Update Available", + "beta": "Beta", "uptime": "Uptime", "database": "Database", "healthy": "Healthy", diff --git a/src/locales/translated/fr_FR.json b/src/locales/translated/fr_FR.json index 1ca15c8e..02578ac4 100644 --- a/src/locales/translated/fr_FR.json +++ b/src/locales/translated/fr_FR.json @@ -2246,7 +2246,7 @@ "failedCompleteReset": "Échec de la réinitialisation du mot de passe", "invalidTotpCode": "Invalid TOTP code", "failedOidcLogin": "Impossible de démarrer la connexion OIDC", - "failedUserInfo": "Impossible d'obtenir les informations de l'utilisateur après la connexion OIDC", + "failedUserInfo": "Impossible d'obtenir les informations de l'utilisateur après la connexion", "oidcAuthFailed": "Échec de l'authentification OIDC", "noTokenReceived": "Aucun jeton reçu de la connexion", "invalidAuthUrl": "URL d'autorisation invalide reçue du backend", diff --git a/src/main.tsx b/src/main.tsx index 740bf373..4a30cbcd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,20 +1,44 @@ /* eslint-disable react-refresh/only-export-components */ -import { StrictMode, useEffect, useState, useRef } from "react"; +import { prepareClientCacheVersion } from "@/lib/client-cache-version"; +import { StrictMode, Suspense, lazy, useEffect, useState, useRef } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; -import DesktopApp from "@/ui/desktop/DesktopApp.tsx"; -import { MobileApp } from "@/ui/mobile/MobileApp.tsx"; import { ThemeProvider } from "@/components/theme-provider"; -import { ElectronVersionCheck } from "@/ui/desktop/user/ElectronVersionCheck.tsx"; import "./i18n/i18n"; -import { isElectron } from "./ui/main-axios.ts"; -import HostManagerApp from "./ui/desktop/apps/host-manager/HostManagerApp.tsx"; -import TerminalApp from "./ui/desktop/apps/features/terminal/TerminalApp.tsx"; -import FileManagerApp from "./ui/desktop/apps/features/file-manager/FileManagerApp.tsx"; -import TunnelApp from "./ui/desktop/apps/features/tunnel/TunnelApp.tsx"; -import ServerStatsApp from "./ui/desktop/apps/features/server-stats/ServerStatsApp.tsx"; -import DockerApp from "./ui/desktop/apps/features/docker/DockerApp.tsx"; -import GuacamoleApp from "@/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx"; +import { isElectron } from "@/lib/electron"; + +const DesktopApp = lazy(() => import("@/ui/desktop/DesktopApp.tsx")); +const MobileApp = lazy(() => + import("@/ui/mobile/MobileApp.tsx").then((module) => ({ + default: module.MobileApp, + })), +); +const HostManagerApp = lazy( + () => import("./ui/desktop/apps/host-manager/HostManagerApp.tsx"), +); +const TerminalApp = lazy( + () => import("./ui/desktop/apps/features/terminal/TerminalApp.tsx"), +); +const FileManagerApp = lazy( + () => import("./ui/desktop/apps/features/file-manager/FileManagerApp.tsx"), +); +const TunnelApp = lazy( + () => import("./ui/desktop/apps/features/tunnel/TunnelApp.tsx"), +); +const ServerStatsApp = lazy( + () => import("./ui/desktop/apps/features/server-stats/ServerStatsApp.tsx"), +); +const DockerApp = lazy( + () => import("./ui/desktop/apps/features/docker/DockerApp.tsx"), +); +const GuacamoleApp = lazy( + () => import("@/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx"), +); +const ElectronVersionCheck = lazy(() => + import("@/ui/desktop/user/ElectronVersionCheck.tsx").then((module) => ({ + default: module.ElectronVersionCheck, + })), +); const FullscreenApp: React.FC = () => { const searchParams = new URLSearchParams(window.location.search); @@ -96,7 +120,10 @@ function RootApp() { useServiceWorker(); const userAgent = - navigator.userAgent || navigator.vendor || (window as any).opera || ""; + navigator.userAgent || + navigator.vendor || + (window as Window & { opera?: string }).opera || + ""; const isTermixMobile = /Termix-Mobile/.test(userAgent); const searchParams = new URLSearchParams(window.location.search); @@ -141,22 +168,26 @@ function RootApp() { )}
{isElectron() && showVersionCheck && !isFullscreen ? ( - setShowVersionCheck(false)} - isAuthenticated={false} - /> + + setShowVersionCheck(false)} + isAuthenticated={false} + /> + ) : ( - renderApp() + {renderApp()} )}
); } -createRoot(document.getElementById("root")!).render( - - - - - , -); +prepareClientCacheVersion().finally(() => { + createRoot(document.getElementById("root")!).render( + + + + + , + ); +}); diff --git a/src/types/connection-log.ts b/src/types/connection-log.ts index 02e7f967..62a9f04a 100644 --- a/src/types/connection-log.ts +++ b/src/types/connection-log.ts @@ -32,7 +32,7 @@ export type LogEntry = { type: "info" | "success" | "warning" | "error"; stage: ConnectionStage; message: string; - details?: Record; + details?: Record; }; export interface ConnectionLogResponse { diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 6ded0abc..54a1d6bd 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -32,6 +32,46 @@ export interface ElectronAPI { getServerConfig: () => Promise; saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>; testServerConnection: (serverUrl: string) => Promise; + getC2STunnelConfig: () => Promise; + saveC2STunnelConfig: ( + config: unknown[], + ) => Promise<{ success: boolean; error?: string }>; + checkLocalPortAvailable: ( + host: string, + port: number, + ) => Promise<{ available: boolean; error?: string }>; + getC2STunnelPresetDefaultName: () => Promise; + startC2STunnel: ( + tunnel: unknown, + index: number, + ) => Promise<{ success: boolean; tunnelName?: string; error?: string }>; + testC2STunnel: ( + tunnel: unknown, + index: number, + ) => Promise<{ success: boolean; message?: string; error?: string }>; + stopC2STunnel: ( + tunnelName: string, + ) => Promise<{ success: boolean; error?: string }>; + getC2STunnelStatuses: () => Promise>; + onC2STunnelStatuses?: ( + callback: (statuses: Record) => void, + ) => () => void; + startC2SAutoStartTunnels: () => Promise<{ + success: boolean; + started: number; + errors: string[]; + }>; + clearSessionCookies: () => Promise; + getSessionCookie: ( + name: string, + targetUrl?: string, + ) => Promise; + waitForSessionCookie: ( + name: string, + targetUrl?: string, + previousValue?: string | null, + timeoutMs?: number, + ) => Promise<{ success: boolean; value?: string; error?: string }>; showSaveDialog: (options: DialogOptions) => Promise; showOpenDialog: (options: DialogOptions) => Promise; @@ -89,8 +129,8 @@ declare global { electronAPI: ElectronAPI; IS_ELECTRON: boolean; electronClipboard?: { - writeText(text: string): void; - readText(): string; + writeText(text: string): Promise; + readText(): Promise; }; } } diff --git a/src/types/index.ts b/src/types/index.ts index 2a59696e..f39913e5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ import type { Client } from "ssh2"; import type { Request } from "express"; +import type { RefObject } from "react"; // ============================================================================ // HOST TYPES (SSH, RDP, VNC, Telnet) @@ -248,11 +249,20 @@ export interface CredentialData { // TUNNEL TYPES // ============================================================================ +export type TunnelScope = "s2s" | "c2s"; +export type TunnelMode = "local" | "remote" | "dynamic"; + export interface TunnelConnection { + scope?: TunnelScope; + mode?: TunnelMode; tunnelType?: "local" | "remote"; + bindHost?: string; + sourceHostId?: number; + sourceHostName?: string; sourcePort: number; endpointPort: number; - endpointHost: string; + endpointHost?: string; + targetHost?: string; endpointPassword?: string; endpointKey?: string; @@ -267,7 +277,11 @@ export interface TunnelConnection { export interface TunnelConfig { name: string; + scope?: TunnelScope; + mode?: TunnelMode; tunnelType?: "local" | "remote"; + bindHost?: string; + targetHost?: string; sourceHostId: number; tunnelIndex: number; @@ -311,6 +325,17 @@ export interface TunnelConfig { socks5ProxyChain?: ProxyNode[]; } +export interface C2STunnelPreset { + id: number; + userId: string; + name: string; + config: TunnelConnection[]; + platform?: string | null; + computerName?: string | null; + createdAt: string; + updatedAt: string; +} + export interface TunnelStatus { connected: boolean; status: ConnectionState; @@ -325,7 +350,7 @@ export interface TunnelStatus { type: "info" | "success" | "warning" | "error"; stage: string; message: string; - details?: Record; + details?: Record; }>; } @@ -468,12 +493,22 @@ export interface TabContextTab { | "telnet"; title: string; hostConfig?: SSHHost; - terminalRef?: any; + terminalRef?: RefObject; initialTab?: string; _updateTimestamp?: number; connectionConfig?: Record; } +export interface TerminalRefHandle { + disconnect?: () => void; + reconnect?: () => void; + fit?: () => void; + sendInput?: (data: string) => void; + notifyResize?: () => void; + refresh?: () => void; + openFileManager?: () => void; +} + export type SplitLayout = "2h" | "2v" | "3l" | "3r" | "3t" | "4grid"; export interface SplitConfiguration { @@ -715,6 +750,7 @@ export type PartialExcept = Partial & Pick; export interface AuthenticatedRequest extends Request { userId: string; + sessionId?: string; user?: { id: string; username: string; diff --git a/src/ui/contexts/ServerStatusContext.tsx b/src/ui/contexts/ServerStatusContext.tsx index 1ff6b995..3535f0db 100644 --- a/src/ui/contexts/ServerStatusContext.tsx +++ b/src/ui/contexts/ServerStatusContext.tsx @@ -80,7 +80,7 @@ export function ServerStatusProvider({ return prev; }); return enabled; - } catch (error) { + } catch { return new Set(); } }, [isAuthenticated]); @@ -111,7 +111,7 @@ export function ServerStatusProvider({ } setStatuses(newStatuses); - } catch (error) { + } catch { if (mountedRef.current) { setStatuses((prev) => { const updated = new Map(prev); diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index afc9fa25..9ab6bca6 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -1,16 +1,15 @@ import React, { - useState, - useEffect, useCallback, - useRef, Component, - type ErrorInfo, + Suspense, + lazy, type ReactNode, + useEffect, + useRef, + useState, } from "react"; import { LeftSidebar } from "@/ui/desktop/navigation/LeftSidebar.tsx"; -import { Dashboard } from "@/ui/desktop/apps/dashboard/Dashboard.tsx"; import { AppView } from "@/ui/desktop/navigation/AppView.tsx"; -import { HostManager } from "@/ui/desktop/apps/host-manager/hosts/HostManager.tsx"; import { TabProvider, useTabs, @@ -18,16 +17,47 @@ import { import { TopNavbar } from "@/ui/desktop/navigation/TopNavbar.tsx"; import { CommandHistoryProvider } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx"; import { ServerStatusProvider } from "@/ui/contexts/ServerStatusContext"; -import { AdminSettings } from "@/ui/desktop/apps/admin/AdminSettings.tsx"; -import { UserProfile } from "@/ui/desktop/user/UserProfile.tsx"; -import { NetworkGraphCard } from "@/ui/desktop/apps/dashboard/cards/NetworkGraphCard"; import { Toaster } from "@/components/ui/sonner.tsx"; import { toast } from "sonner"; -import { CommandPalette } from "@/ui/desktop/apps/command-palette/CommandPalette.tsx"; -import { getUserInfo, logoutUser, isElectron } from "@/ui/main-axios.ts"; +import { + getUserInfo, + logoutUser, + isCurrentAuthInvalidationError, +} from "@/ui/main-axios.ts"; import { useTheme } from "@/components/theme-provider"; import { dbHealthMonitor } from "@/lib/db-health-monitor.ts"; import { useTranslation } from "react-i18next"; +import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; + +const Dashboard = lazy(() => + import("@/ui/desktop/apps/dashboard/Dashboard.tsx").then((module) => ({ + default: module.Dashboard, + })), +); +const HostManager = lazy(() => + import("@/ui/desktop/apps/host-manager/hosts/HostManager.tsx").then( + (module) => ({ + default: module.HostManager, + }), + ), +); +const AdminSettings = lazy(() => + import("@/ui/desktop/apps/admin/AdminSettings.tsx").then((module) => ({ + default: module.AdminSettings, + })), +); +const UserProfile = lazy(() => + import("@/ui/desktop/user/UserProfile.tsx").then((module) => ({ + default: module.UserProfile, + })), +); +const CommandPalette = lazy(() => + import("@/ui/desktop/apps/command-palette/CommandPalette.tsx").then( + (module) => ({ + default: module.CommandPalette, + }), + ), +); function AppContent({ onAuthStateChange, @@ -52,6 +82,7 @@ function AppContent({ const { theme, setTheme } = useTheme(); const [rightSidebarOpen, setRightSidebarOpen] = useState(false); const [rightSidebarWidth, setRightSidebarWidth] = useState(400); + const isAuthenticatedRef = useRef(false); const isDarkMode = theme === "dark" || @@ -96,6 +127,8 @@ function AppContent({ const handleSessionExpired = () => { setIsAuthenticated(false); + setIsAdmin(false); + setUsername(null); }; dbHealthMonitor.on( @@ -177,9 +210,8 @@ function AppContent({ if (hostIdentifier) { const openTerminal = async () => { try { - const { getSSHHostById, getSSHHosts } = await import( - "@/ui/main-axios.ts" - ); + const { getSSHHostById, getSSHHosts } = + await import("@/ui/main-axios.ts"); let host = null; if (/^\d+$/.test(hostIdentifier)) { @@ -211,6 +243,22 @@ function AppContent({ }, [addTab]); const isCheckingAuth = useRef(false); + const clientTunnelAutoStartStarted = useRef(false); + + const startClientTunnelAutoStart = useCallback(() => { + if ( + clientTunnelAutoStartStarted.current || + !window.electronAPI?.isElectron + ) { + return; + } + + clientTunnelAutoStartStarted.current = true; + window.electronAPI.startC2SAutoStartTunnels?.().catch((error) => { + clientTunnelAutoStartStarted.current = false; + console.error("Failed to start client tunnel auto-start entries:", error); + }); + }, []); useEffect(() => { const checkAuth = () => { @@ -227,16 +275,22 @@ function AppContent({ setIsAuthenticated(true); setIsAdmin(!!meRes.is_admin); setUsername(meRes.username || null); + startClientTunnelAutoStart(); } }) .catch((err) => { - setIsAuthenticated(false); - setIsAdmin(false); - setUsername(null); - - const errorCode = err?.response?.data?.code; - if (errorCode === "SESSION_EXPIRED") { + if (isCurrentAuthInvalidationError(err)) { + setIsAuthenticated(false); + setIsAdmin(false); + setUsername(null); console.warn("Session expired - please log in again"); + return; + } + + if (!isAuthenticatedRef.current) { + setIsAuthenticated(false); + setIsAdmin(false); + setUsername(null); } }) .finally(() => { @@ -251,7 +305,7 @@ function AppContent({ window.addEventListener("storage", handleStorageChange); return () => window.removeEventListener("storage", handleStorageChange); - }, []); + }, [startClientTunnelAutoStart]); useEffect(() => { localStorage.setItem("topNavbarOpen", JSON.stringify(isTopbarOpen)); @@ -259,6 +313,7 @@ function AppContent({ useEffect(() => { onAuthStateChange?.(isAuthenticated); + isAuthenticatedRef.current = isAuthenticated; }, [isAuthenticated, onAuthStateChange]); const handleAuthSuccess = useCallback( @@ -274,6 +329,7 @@ function AppContent({ setIsAuthenticated(true); setIsAdmin(authData.isAdmin); setUsername(authData.username); + startClientTunnelAutoStart(); setTransitionPhase("fadeIn"); setTimeout(() => { @@ -282,7 +338,7 @@ function AppContent({ }, 800); }, 1200); }, - [], + [startClientTunnelAutoStart], ); const handleLogout = useCallback(async () => { @@ -347,18 +403,22 @@ function AppContent({ return (
- + + + {!isAuthenticated && (
- + + +
)} @@ -382,56 +442,124 @@ function AppContent({ {showHome && (
- + + +
+ } + > + +
)} {showSshManager && (
- + + +
+ } + > + +
)} {showAdmin && (
- + + +
+ } + > + + )} {showProfile && (
- + + +
+ } + > + + )} setIsCommandPaletteOpen(true)} onRightSidebarStateChange={(isOpen, width) => { setRightSidebarOpen(isOpen); setRightSidebarWidth(width); @@ -643,7 +771,7 @@ class TabErrorBoundary extends Component< throw error; } - componentDidCatch(error: Error, errorInfo: ErrorInfo) { + componentDidCatch(error: Error, _errorInfo: ErrorInfo) { if (error.message?.includes("useTabs must be used within a TabProvider")) { console.warn( "TabProvider mounting race condition detected, recovering...", diff --git a/src/ui/desktop/apps/FullScreenAppWrapper.tsx b/src/ui/desktop/apps/FullScreenAppWrapper.tsx index 999c6bb4..3834349b 100644 --- a/src/ui/desktop/apps/FullScreenAppWrapper.tsx +++ b/src/ui/desktop/apps/FullScreenAppWrapper.tsx @@ -6,6 +6,7 @@ import { getSSHHosts, getUserInfo } from "@/ui/main-axios.ts"; import type { SSHHost } from "@/types"; import { Dashboard } from "@/ui/desktop/apps/dashboard/Dashboard.tsx"; import { Toaster } from "@/components/ui/sonner.tsx"; +import { dbHealthMonitor } from "@/lib/db-health-monitor.ts"; interface FullScreenAppWrapperProps { hostId?: string; @@ -20,7 +21,18 @@ export const FullScreenAppWrapper: React.FC = ({ const [loading, setLoading] = useState(true); const [isAuthenticated, setIsAuthenticated] = useState(false); const [authLoading, setAuthLoading] = useState(true); - const [isAdmin, setIsAdmin] = useState(false); + const [, setIsAdmin] = useState(false); + + useEffect(() => { + const handleSessionExpired = () => { + setIsAuthenticated(false); + setIsAdmin(false); + setHostConfig(null); + }; + + dbHealthMonitor.on("session-expired", handleSessionExpired); + return () => dbHealthMonitor.off("session-expired", handleSessionExpired); + }, []); useEffect(() => { const checkAuth = async () => { @@ -28,9 +40,8 @@ export const FullScreenAppWrapper: React.FC = ({ const userInfo = await getUserInfo(); if (userInfo) { setIsAuthenticated(true); - setIsAdmin(userInfo.isAdmin || false); } - } catch (error) { + } catch { setIsAuthenticated(false); } finally { setAuthLoading(false); @@ -65,13 +76,8 @@ export const FullScreenAppWrapper: React.FC = ({ } }, [hostId, isAuthenticated, authLoading]); - const handleAuthSuccess = (authData: { - isAdmin: boolean; - username: string | null; - userId: string | null; - }) => { + const handleAuthSuccess = () => { setIsAuthenticated(true); - setIsAdmin(authData.isAdmin); window.location.reload(); }; diff --git a/src/ui/desktop/apps/admin/AdminSettings.tsx b/src/ui/desktop/apps/admin/AdminSettings.tsx index f7ef1859..6ac257e7 100644 --- a/src/ui/desktop/apps/admin/AdminSettings.tsx +++ b/src/ui/desktop/apps/admin/AdminSettings.tsx @@ -7,7 +7,7 @@ import { TabsList, TabsTrigger, } from "@/components/ui/tabs.tsx"; -import { Shield, Users, Database, Clock } from "lucide-react"; +import { Shield, Users, Database, Clock, Key } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; @@ -22,12 +22,14 @@ import { getSessions, unlinkOIDCFromPasswordAccount, } from "@/ui/main-axios.ts"; +import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; import { RolesTab } from "@/ui/desktop/apps/admin/tabs/RolesTab.tsx"; import { GeneralSettingsTab } from "@/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx"; import { OIDCSettingsTab } from "@/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx"; import { UserManagementTab } from "@/ui/desktop/apps/admin/tabs/UserManagementTab.tsx"; import { SessionManagementTab } from "@/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx"; import { DatabaseSecurityTab } from "@/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx"; +import { ApiKeysTab } from "@/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx"; import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx"; import { UserEditDialog } from "./dialogs/UserEditDialog.tsx"; import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx"; @@ -47,6 +49,7 @@ export function AdminSettings({ const { confirmWithToast } = useConfirmation(); const { state: sidebarState } = useSidebar(); + const [loading, setLoading] = React.useState(true); const [allowRegistration, setAllowRegistration] = React.useState(true); const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true); const [allowPasswordReset, setAllowPasswordReset] = React.useState(true); @@ -102,8 +105,8 @@ export function AdminSettings({ createdAt: string; expiresAt: string; lastActiveAt: string; - jwtToken: string; isRevoked?: boolean; + isCurrentSession?: boolean; }> >([]); const [sessionsLoading, setSessionsLoading] = React.useState(false); @@ -119,36 +122,45 @@ export function AdminSettings({ const serverUrl = (window as { configuredServerUrl?: string }) .configuredServerUrl; if (!serverUrl) { + setLoading(false); return; } } - getAdminOIDCConfig() - .then((res) => { - if (res) setOidcConfig(res); - }) - .catch((err) => { - if (!err.message?.includes("No server configured")) { - toast.error(t("admin.failedToFetchOidcConfig")); - } - }); - getUserInfo() - .then((info) => { - if (info) { - setCurrentUser({ - id: info.userId, - username: info.username, - is_admin: info.is_admin, - is_oidc: info.is_oidc, - }); - } - }) - .catch((err) => { - if (!err?.message?.includes("No server configured")) { - console.warn("Failed to fetch current user info", err); - } - }); - fetchSessions(); + Promise.allSettled([ + getAdminOIDCConfig() + .then((res) => { + if (res) setOidcConfig(res); + }) + .catch((err) => { + if (!err.message?.includes("No server configured")) { + toast.error(t("admin.failedToFetchOidcConfig")); + } + }), + getUserInfo() + .then((info) => { + if (info) { + setCurrentUser({ + id: info.userId, + username: info.username, + is_admin: info.is_admin, + is_oidc: info.is_oidc, + }); + } + }) + .catch((err) => { + if (!err?.message?.includes("No server configured")) { + console.warn("Failed to fetch current user info", err); + } + }), + getSessions() + .then((data) => setSessions(data.sessions || [])) + .catch((err) => { + if (!err?.message?.includes("No server configured")) { + toast.error(t("admin.failedToFetchSessions")); + } + }), + ]).finally(() => setLoading(false)); }, []); React.useEffect(() => { @@ -332,6 +344,7 @@ export function AdminSettings({ style={wrapperStyle} className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden" > +

{t("admin.title")}

@@ -391,6 +404,13 @@ export function AdminSettings({ {t("admin.databaseSecurity")} + + + {t("admin.apiKeys.tabLabel")} + @@ -441,7 +461,11 @@ export function AdminSettings({ - + + + + +
@@ -459,7 +483,6 @@ export function AdminSettings({ user={selectedUserForEdit} currentUser={currentUser} onSuccess={handleEditUserSuccess} - allowPasswordLogin={allowPasswordLogin} /> void; - allowPasswordLogin: boolean; } export function UserEditDialog({ @@ -62,13 +58,11 @@ export function UserEditDialog({ user, currentUser, onSuccess, - allowPasswordLogin, }: UserEditDialogProps) { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); const [adminLoading, setAdminLoading] = useState(false); - const [passwordResetLoading, setPasswordResetLoading] = useState(false); const [sessionLoading, setSessionLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [rolesLoading, setRolesLoading] = useState(false); @@ -160,42 +154,6 @@ export function UserEditDialog({ } }; - const handlePasswordReset = async () => { - if (!user) return; - - const userToReset = user; - onOpenChange(false); - - const confirmed = await confirmWithToast({ - title: t("admin.resetUserPassword"), - description: `${t("admin.passwordResetWarning")} (${userToReset.username})`, - confirmText: t("admin.resetUserPassword"), - cancelText: t("common.cancel"), - variant: "destructive", - }); - - if (!confirmed) { - onOpenChange(true); - return; - } - - setPasswordResetLoading(true); - try { - await initiatePasswordReset(userToReset.username); - toast.success( - t("admin.passwordResetInitiated", { username: userToReset.username }), - ); - onSuccess(); - onOpenChange(true); - } catch (error) { - console.error("Failed to reset password:", error); - toast.error(t("admin.failedToResetPassword")); - onOpenChange(true); - } finally { - setPasswordResetLoading(false); - } - }; - const handleAssignRole = async (roleId: number) => { if (!user) return; @@ -342,9 +300,6 @@ export function UserEditDialog({ if (!user) return null; - const showPasswordReset = - allowPasswordLogin && (user.passwordHash || !user.isOidc); - return ( diff --git a/src/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx b/src/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx new file mode 100644 index 00000000..8e5a4a87 --- /dev/null +++ b/src/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx @@ -0,0 +1,507 @@ +import React from "react"; +import { Button } from "@/components/ui/button.tsx"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table.tsx"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog.tsx"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover.tsx"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Label } from "@/components/ui/label.tsx"; +import { Badge } from "@/components/ui/badge.tsx"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +import { + Key, + Plus, + Trash2, + Copy, + Check, + ChevronsUpDown, + AlertCircle, + RefreshCw, +} from "lucide-react"; +import { cn } from "@/lib/utils.ts"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; +import { useConfirmation } from "@/hooks/use-confirmation.ts"; +import { + getApiKeys, + createApiKey, + deleteApiKey, + getUserList, + type ApiKey, + type CreatedApiKey, +} from "@/ui/main-axios.ts"; + +interface UserOption { + id: string; + username: string; +} + +function UserCombobox({ + users, + value, + onChange, + disabled, +}: { + users: UserOption[]; + value: string; + onChange: (id: string) => void; + disabled?: boolean; +}) { + const { t } = useTranslation(); + const [open, setOpen] = React.useState(false); + const selected = users.find((u) => u.id === value); + + return ( + + + + + + + + + {t("admin.apiKeys.noUsersFound")} + + {users.map((user) => ( + { + onChange(user.id); + setOpen(false); + }} + > + + {user.username} + + ))} + + + + + + ); +} + +function CreateApiKeyDialog({ + open, + onOpenChange, + onCreated, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + onCreated: () => void; +}) { + const { t } = useTranslation(); + const [name, setName] = React.useState(""); + const [selectedUserId, setSelectedUserId] = React.useState(""); + const [expiresAt, setExpiresAt] = React.useState(""); + const [loading, setLoading] = React.useState(false); + const [usersLoading, setUsersLoading] = React.useState(false); + const [users, setUsers] = React.useState([]); + const [createdKey, setCreatedKey] = React.useState( + null, + ); + const [copied, setCopied] = React.useState(false); + + React.useEffect(() => { + if (!open) return; + if (createdKey) return; + + setUsersLoading(true); + getUserList() + .then((res) => + setUsers( + res.users.map((u) => ({ + id: (u as unknown as { id: string }).id, + username: u.username, + })), + ), + ) + .catch(() => toast.error(t("admin.failedToFetchUsers"))) + .finally(() => setUsersLoading(false)); + }, [open]); + + const handleClose = () => { + setCreatedKey(null); + setName(""); + setSelectedUserId(""); + setExpiresAt(""); + setCopied(false); + onOpenChange(false); + onCreated(); + }; + + const handleCreate = async () => { + if (!name.trim()) { + toast.error(t("admin.apiKeys.nameRequired")); + return; + } + if (!selectedUserId) { + toast.error(t("admin.apiKeys.userRequired")); + return; + } + + setLoading(true); + try { + const result = await createApiKey( + name.trim(), + selectedUserId, + expiresAt || undefined, + ); + setCreatedKey(result); + } catch (err: unknown) { + const e = err as { response?: { data?: { error?: string } } }; + toast.error( + e?.response?.data?.error || t("admin.apiKeys.failedToCreate"), + ); + } finally { + setLoading(false); + } + }; + + const handleCopy = async () => { + if (!createdKey) return; + await navigator.clipboard.writeText(createdKey.token); + setCopied(true); + toast.success(t("admin.apiKeys.tokenCopied")); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + { + if (!isOpen) handleClose(); + }} + > + + + + + {createdKey + ? t("admin.apiKeys.keyCreated") + : t("admin.apiKeys.createApiKey")} + + + {createdKey + ? t("admin.apiKeys.keyCreatedDescription") + : t("admin.apiKeys.createApiKeyDescription")} + + + + {!createdKey ? ( +
+
+ + setName(e.target.value)} + placeholder={t("admin.apiKeys.keyNamePlaceholder")} + disabled={loading} + autoFocus + /> +
+ +
+ + {usersLoading ? ( +

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

+ ) : ( + + )} +
+ +
+ + setExpiresAt(e.target.value)} + disabled={loading} + min={new Date().toISOString().split("T")[0]} + /> +

+ {t("admin.apiKeys.expiresAtHelp")} +

+
+
+ ) : ( +
+ + + {t("admin.apiKeys.copyWarningTitle")} + + {t("admin.apiKeys.copyWarningDescription")} + + +
+ +
+ + {createdKey.token} + + +
+
+
+ )} + + + {!createdKey ? ( + <> + + + + ) : ( + + )} + +
+
+ ); +} + +export function ApiKeysTab(): React.ReactElement { + const { t } = useTranslation(); + const { confirmWithToast } = useConfirmation(); + const [keys, setKeys] = React.useState([]); + const [loading, setLoading] = React.useState(false); + const [createDialogOpen, setCreateDialogOpen] = React.useState(false); + + const fetchKeys = React.useCallback(async () => { + setLoading(true); + try { + const data = await getApiKeys(); + setKeys(data.apiKeys); + } catch { + toast.error(t("admin.apiKeys.failedToFetch")); + } finally { + setLoading(false); + } + }, [t]); + + React.useEffect(() => { + fetchKeys(); + }, [fetchKeys]); + + const handleDelete = (keyId: string, keyName: string) => { + confirmWithToast( + t("admin.apiKeys.confirmRevoke", { name: keyName }), + async () => { + try { + await deleteApiKey(keyId); + toast.success(t("admin.apiKeys.revokedSuccessfully")); + fetchKeys(); + } catch { + toast.error(t("admin.apiKeys.failedToRevoke")); + } + }, + "destructive", + ); + }; + + const formatDate = (iso: string | null) => { + if (!iso) return t("admin.apiKeys.never"); + const d = new Date(iso); + return ( + d.toLocaleDateString() + + " " + + d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) + ); + }; + + const isExpired = (expiresAt: string | null) => + expiresAt ? new Date(expiresAt) < new Date() : false; + + return ( +
+
+

{t("admin.apiKeys.title")}

+
+ + + +
+
+ + {loading && keys.length === 0 ? ( +
+ {t("admin.loading")} +
+ ) : keys.length === 0 ? ( +
+ {t("admin.apiKeys.noKeys")} +
+ ) : ( + + + + {t("admin.apiKeys.name")} + {t("admin.user")} + {t("admin.apiKeys.prefix")} + {t("admin.created")} + {t("admin.expires")} + {t("admin.apiKeys.lastUsed")} + {t("admin.actions")} + + + + {keys.map((key) => ( + + + {key.name} + {!key.isActive && ( + + {t("admin.revoked")} + + )} + + + {key.username || key.userId} + + + + {key.tokenPrefix}… + + + + {formatDate(key.createdAt)} + + + + {formatDate(key.expiresAt)} + + + + {formatDate(key.lastUsedAt)} + + + + + + ))} + +
+ )} + + +
+ ); +} diff --git a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx b/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx index 8736c5a7..18bfe9f3 100644 --- a/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx +++ b/src/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx @@ -6,15 +6,7 @@ import { toast } from "sonner"; import { isElectron } from "@/ui/main-axios.ts"; import { getBasePath } from "@/lib/base-path"; -interface DatabaseSecurityTabProps { - currentUser: { - is_oidc: boolean; - } | null; -} - -export function DatabaseSecurityTab({ - currentUser, -}: DatabaseSecurityTabProps): React.ReactElement { +export function DatabaseSecurityTab(): React.ReactElement { const { t } = useTranslation(); const [exportLoading, setExportLoading] = React.useState(false); @@ -42,12 +34,6 @@ export function DatabaseSecurityTab({ const headers: Record = { "Content-Type": "application/json", }; - if (isElectron()) { - const token = localStorage.getItem("jwt"); - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - } const response = await fetch(apiUrl, { method: "POST", @@ -110,17 +96,8 @@ export function DatabaseSecurityTab({ const formData = new FormData(); formData.append("file", importFile); - const importHeaders: Record = {}; - if (isElectron()) { - const token = localStorage.getItem("jwt"); - if (token) { - importHeaders["Authorization"] = `Bearer ${token}`; - } - } - const response = await fetch(apiUrl, { method: "POST", - headers: importHeaders, credentials: "include", body: formData, }); diff --git a/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx b/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx index 606f12ce..cf4dcaac 100644 --- a/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx +++ b/src/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx @@ -73,7 +73,7 @@ export function GeneralSettingsTab({ const [logLevel, setLogLevel] = React.useState("info"); const [logLevelLoading, setLogLevelLoading] = React.useState(false); - const [sessionTimeoutHours, setSessionTimeoutHours] = React.useState(24); + const [, setSessionTimeoutHours] = React.useState(24); const [sessionTimeoutInput, setSessionTimeoutInput] = React.useState("24"); const [sessionTimeoutLoading, setSessionTimeoutLoading] = React.useState(false); diff --git a/src/ui/desktop/apps/admin/tabs/RolesTab.tsx b/src/ui/desktop/apps/admin/tabs/RolesTab.tsx index 825700bf..148f2100 100644 --- a/src/ui/desktop/apps/admin/tabs/RolesTab.tsx +++ b/src/ui/desktop/apps/admin/tabs/RolesTab.tsx @@ -108,7 +108,7 @@ export function RolesTab(): React.ReactElement { setRoleDialogOpen(false); loadRoles(); - } catch (error) { + } catch { toast.error(t("rbac.failedToSaveRole")); } }; @@ -129,7 +129,7 @@ export function RolesTab(): React.ReactElement { await deleteRole(role.id); toast.success(t("rbac.roleDeletedSuccessfully")); loadRoles(); - } catch (error) { + } catch { toast.error(t("rbac.failedToDeleteRole")); } }; diff --git a/src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx b/src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx index 95bd0953..344badfc 100644 --- a/src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx +++ b/src/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx @@ -12,11 +12,7 @@ import { Monitor, Smartphone, Globe, Trash2 } from "lucide-react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { - getCookie, - revokeSession, - revokeAllUserSessions, -} from "@/ui/main-axios.ts"; +import { revokeSession, revokeAllUserSessions } from "@/ui/main-axios.ts"; interface Session { id: string; @@ -27,8 +23,8 @@ interface Session { createdAt: string; expiresAt: string; lastActiveAt: string; - jwtToken: string; isRevoked?: boolean; + isCurrentSession?: boolean; } interface SessionManagementTabProps { @@ -46,9 +42,9 @@ export function SessionManagementTab({ const { confirmWithToast } = useConfirmation(); const handleRevokeSession = async (sessionId: string) => { - const currentJWT = getCookie("jwt"); - const currentSession = sessions.find((s) => s.jwtToken === currentJWT); - const isCurrentSession = currentSession?.id === sessionId; + const isCurrentSession = sessions.some( + (session) => session.id === sessionId && session.isCurrentSession, + ); confirmWithToast( t("admin.confirmRevokeSession"), diff --git a/src/ui/desktop/apps/command-palette/CommandPalette.tsx b/src/ui/desktop/apps/command-palette/CommandPalette.tsx index 43344022..fc11253a 100644 --- a/src/ui/desktop/apps/command-palette/CommandPalette.tsx +++ b/src/ui/desktop/apps/command-palette/CommandPalette.tsx @@ -8,13 +8,12 @@ import { } from "@/components/ui/command.tsx"; import React, { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; -import { Kbd, KbdGroup } from "@/components/ui/kbd"; +import { Kbd, KbdKey, KbdSeparator } from "@/components/ui/kbd"; import { Key, Server, Settings, User, - Github, Terminal, Monitor, Eye, @@ -28,6 +27,7 @@ import { import { useTranslation } from "react-i18next"; import { BiMoney, BiSupport } from "react-icons/bi"; import { BsDiscord } from "react-icons/bs"; +import { FaGithub } from "react-icons/fa"; import { GrUpdate } from "react-icons/gr"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { @@ -38,7 +38,6 @@ import { logActivity, } from "@/ui/main-axios.ts"; import type { RecentActivityItem } from "@/ui/main-axios.ts"; -import { toast } from "sonner"; import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; import { DropdownMenu, @@ -76,7 +75,7 @@ interface SSHHost { domain?: string; security?: string; ignoreCert?: boolean; - guacamoleConfig?: any; + guacamoleConfig?: unknown; showTerminalInSidebar?: boolean; showFileManagerInSidebar?: boolean; showTunnelInSidebar?: boolean; @@ -84,6 +83,28 @@ interface SSHHost { showServerStatsInSidebar?: boolean; } +function shouldShowMetrics(host: SSHHost): boolean { + try { + const statsConfig = host.statsConfig + ? JSON.parse(host.statsConfig) + : DEFAULT_STATS_CONFIG; + return statsConfig.metricsEnabled !== false; + } catch { + return true; + } +} + +function hasTunnelConnections(host: SSHHost): boolean { + try { + const tunnelConnections = Array.isArray(host.tunnelConnections) + ? host.tunnelConnections + : JSON.parse(host.tunnelConnections as string); + return Array.isArray(tunnelConnections) && tunnelConnections.length > 0; + } catch { + return false; + } +} + export function CommandPalette({ isOpen, setIsOpen, @@ -306,9 +327,6 @@ export function CommandPalette({ }; const handleHostEditClick = (host: SSHHost) => { - const title = host.name?.trim() - ? host.name - : `${host.username}@${host.ip}:${host.port}`; addTab({ type: "ssh_manager", title: t("commandPalette.hostManager"), @@ -391,32 +409,10 @@ export function CommandPalette({ ? host.name : `${host.username}@${host.ip}:${host.port}`; - let shouldShowMetrics = true; - try { - const statsConfig = host.statsConfig - ? JSON.parse(host.statsConfig) - : DEFAULT_STATS_CONFIG; - shouldShowMetrics = statsConfig.metricsEnabled !== false; - } catch { - shouldShowMetrics = true; - } - const isSSH = !host.connectionType || host.connectionType === "ssh"; - - let hasTunnelConnections = false; - try { - const tunnelConnections = Array.isArray( - host.tunnelConnections, - ) - ? host.tunnelConnections - : JSON.parse(host.tunnelConnections as string); - hasTunnelConnections = - Array.isArray(tunnelConnections) && - tunnelConnections.length > 0; - } catch { - hasTunnelConnections = false; - } + const showMetrics = shouldShowMetrics(host); + const hasTunnels = hasTunnelConnections(host); const visibleButtons = [ host.enableTerminal && (host.showTerminalInSidebar ?? true), @@ -425,13 +421,13 @@ export function CommandPalette({ (host.showFileManagerInSidebar ?? false), isSSH && host.enableTunnel && - hasTunnelConnections && + hasTunnels && (host.showTunnelInSidebar ?? false), isSSH && host.enableDocker && (host.showDockerInSidebar ?? false), isSSH && - shouldShowMetrics && + showMetrics && (host.showServerStatsInSidebar ?? false), ].filter(Boolean).length; @@ -493,7 +489,7 @@ export function CommandPalette({ {isSSH && host.enableTunnel && - hasTunnelConnections && + hasTunnels && (host.showTunnelInSidebar ?? false) && ( {alert.actionUrl && alert.actionText && ( - {currentAlertIndex + 1} of {alerts.length} + {currentAlertIndex + 1} {t("common.of")} {alerts.length}
)} diff --git a/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx b/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx index 0d4ac9d0..54434392 100644 --- a/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx +++ b/src/ui/desktop/apps/dashboard/cards/NetworkGraphCard.tsx @@ -13,6 +13,8 @@ import { getNetworkTopology, saveNetworkTopology, type SSHHostWithStatus, + type NetworkTopologyEdge, + type NetworkTopologyNode, } from "@/ui/main-axios"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -34,7 +36,6 @@ import { Label } from "@/components/ui/label"; import { Plus, Trash2, - Move3D, ZoomIn, ZoomOut, RotateCw, @@ -46,7 +47,6 @@ import { Edit, FolderInput, FolderMinus, - Settings2, Terminal, ArrowUp, NetworkIcon, @@ -104,13 +104,15 @@ interface NetworkGraphCardProps { embedded?: boolean; } +type NetworkElement = NetworkTopologyNode | NetworkTopologyEdge; + export function NetworkGraphCard({ embedded = true, }: NetworkGraphCardProps): React.ReactElement { const { t } = useTranslation(); const { addTab } = useTabs(); - const [elements, setElements] = useState([]); + const [elements, setElements] = useState([]); const [hosts, setHosts] = useState([]); const [hostMap, setHostMap] = useState({}); const hostMapRef = useRef({}); @@ -205,8 +207,8 @@ export function NetworkGraphCard({ }); setHostMap(newHostMap); - let nodes: any[] = []; - let edges: any[] = []; + let nodes: NetworkTopologyNode[] = []; + let edges: NetworkTopologyEdge[] = []; try { const topologyData = await getNetworkTopology(); @@ -215,7 +217,7 @@ export function NetworkGraphCard({ topologyData.nodes && Array.isArray(topologyData.nodes) ) { - nodes = topologyData.nodes.map((node: any) => { + nodes = topologyData.nodes.map((node) => { const host = newHostMap[node.data.id]; return { data: { @@ -232,12 +234,12 @@ export function NetworkGraphCard({ }); edges = topologyData.edges || []; } - } catch (topologyError) { + } catch { console.warn("Starting with empty topology"); } - const nodeIds = new Set(nodes.map((n: any) => n.data.id)); - const validEdges = edges.filter((edge: any) => { + const nodeIds = new Set(nodes.map((n) => n.data.id)); + const validEdges = edges.filter((edge) => { const sourceExists = nodeIds.has(edge.data.source); const targetExists = nodeIds.has(edge.data.target); return sourceExists && targetExists; @@ -315,7 +317,10 @@ export function NetworkGraphCard({ useEffect(() => { if (!cyRef.current || loading || elements.length === 0) return; const hasPositions = elements.some( - (el: any) => el.position && (el.position.x !== 0 || el.position.y !== 0), + (el) => + "position" in el && + el.position && + (el.position.x !== 0 || el.position.y !== 0), ); if (!hasPositions) { @@ -633,7 +638,7 @@ export function NetworkGraphCard({ setElements([...cyRef.current.elements().jsons()]); forceUpdate(); setShowAddNodeDialog(false); - } catch (err) { + } catch { setError(t("networkGraph.failedToAddNode")); } }; @@ -891,7 +896,7 @@ export function NetworkGraphCard({ if (fileInputRef.current) { fileInputRef.current.value = ""; } - } catch (err) { + } catch { setError(t("networkGraph.invalidFile")); } }; diff --git a/src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx b/src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx index 8a92c950..d030be15 100644 --- a/src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx +++ b/src/ui/desktop/apps/dashboard/cards/RecentActivityCard.tsx @@ -11,7 +11,6 @@ import { Monitor, Eye, MessagesSquare, - Network, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { type RecentActivityItem } from "@/ui/main-axios"; diff --git a/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx b/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx index 7cd9f482..4821e305 100644 --- a/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx +++ b/src/ui/desktop/apps/dashboard/cards/ServerOverviewCard.tsx @@ -14,7 +14,7 @@ import { UpdateLog } from "@/ui/desktop/apps/dashboard/apps/UpdateLog"; interface ServerOverviewCardProps { loggedIn: boolean; versionText: string; - versionStatus: "up_to_date" | "requires_update"; + versionStatus: "up_to_date" | "requires_update" | "beta"; uptime: string; dbHealth: "healthy" | "error"; totalServers: number; @@ -61,11 +61,13 @@ export function ServerOverviewCard({ diff --git a/src/ui/desktop/apps/dashboard/hooks/useDashboardPreferences.ts b/src/ui/desktop/apps/dashboard/hooks/useDashboardPreferences.ts index 313d1891..f77578bf 100644 --- a/src/ui/desktop/apps/dashboard/hooks/useDashboardPreferences.ts +++ b/src/ui/desktop/apps/dashboard/hooks/useDashboardPreferences.ts @@ -35,7 +35,7 @@ export function useDashboardPreferences(enabled: boolean = true) { } else { setLayout(DEFAULT_LAYOUT); } - } catch (error) { + } catch { setLayout(DEFAULT_LAYOUT); } finally { setLoading(false); diff --git a/src/ui/desktop/apps/features/docker/DockerManager.tsx b/src/ui/desktop/apps/features/docker/DockerManager.tsx index 4dec33fe..80ec4d53 100644 --- a/src/ui/desktop/apps/features/docker/DockerManager.tsx +++ b/src/ui/desktop/apps/features/docker/DockerManager.tsx @@ -1,14 +1,8 @@ import React from "react"; import { useSidebar } from "@/components/ui/sidebar.tsx"; import { Separator } from "@/components/ui/separator.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs.tsx"; + import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; import type { SSHHost, DockerContainer, DockerValidation } from "@/types"; import { connectDockerSession, @@ -33,6 +27,7 @@ import { useConnectionLog, } from "@/ui/desktop/navigation/connection-log/ConnectionLogContext.tsx"; import { ConnectionLog } from "@/ui/desktop/navigation/connection-log/ConnectionLog.tsx"; +import type { LogEntry } from "@/types/connection-log.ts"; interface DockerManagerProps { hostConfig?: SSHHost; @@ -43,10 +38,11 @@ interface DockerManagerProps { onClose?: () => void; } -interface TabData { - id: number; - type: string; - [key: string]: unknown; +type ConnectionLogInput = Omit; + +interface DockerConnectionError { + message?: string; + connectionLogs?: ConnectionLogInput[]; } function DockerManagerInner({ @@ -76,7 +72,6 @@ function DockerManagerInner({ string | null >(null); const [isConnecting, setIsConnecting] = React.useState(false); - const [activeTab, setActiveTab] = React.useState("containers"); const [dockerValidation, setDockerValidation] = React.useState(null); const [isValidating, setIsValidating] = React.useState(false); @@ -251,18 +246,19 @@ function DockerManagerInner({ logDockerActivity(); setTimeout(() => clearLogs(), 1000); } - } catch (error: any) { + } catch (error) { + const dockerError = error as DockerConnectionError; setIsConnecting(false); setIsValidating(false); setHasConnectionError(true); - if (error?.connectionLogs) { - setLogs(error.connectionLogs); + if (Array.isArray(dockerError.connectionLogs)) { + setLogs(dockerError.connectionLogs); } else { addLog({ type: "error", stage: "connection", - message: error?.message || t("docker.connectionFailed"), + message: dockerError.message || t("docker.connectionFailed"), }); } } finally { @@ -302,7 +298,7 @@ function DockerManagerInner({ try { const data = await listDockerContainers(sessionId, true); setContainers(data); - } catch (error) { + } catch { // Silently handle polling errors } }, [sessionId]); @@ -319,7 +315,7 @@ function DockerManagerInner({ if (!cancelled) { setContainers(data); } - } catch (error) { + } catch { // Silently handle polling errors } finally { if (!cancelled) { @@ -661,7 +657,7 @@ function DockerManagerInner({
{viewMode === "list" ? ( -
+
{sessionId ? ( isLoadingContainers && containers.length === 0 ? ( => { + if (window.electronClipboard) { + return window.electronClipboard.readText(); + } + return navigator.clipboard.readText(); + }; + + const writeTextToClipboard = async (text: string): Promise => { + if (window.electronClipboard) { + await window.electronClipboard.writeText(text); + return; + } + await navigator.clipboard.writeText(text); + }; + terminal.attachCustomKeyEventHandler((e: KeyboardEvent): boolean => { if (e.type !== "keydown") return true; if ( - ((e.ctrlKey && !e.altKey && !e.metaKey) || - (e.metaKey && !e.ctrlKey && !e.altKey)) && + ((e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) || + (e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey)) && e.key.toLowerCase() === "v" ) { e.preventDefault(); e.stopPropagation(); - navigator.clipboard - .readText() + readTextFromClipboard() .then((text) => { if (text) terminal.paste(text); }) @@ -96,7 +108,7 @@ export function ConsoleTerminal({ e.stopPropagation(); const selection = terminal.getSelection(); if (selection) { - navigator.clipboard.writeText(selection).catch(() => { + writeTextToClipboard(selection).catch(() => { toast.error(t("terminal.clipboardWriteFailed")); }); terminal.clearSelection(); @@ -105,23 +117,18 @@ export function ConsoleTerminal({ } if ( - ((e.ctrlKey && - e.shiftKey && - !e.altKey && - !e.metaKey && - e.key.toLowerCase() === "c") || - (e.ctrlKey && - !e.shiftKey && - !e.altKey && - !e.metaKey && - e.key === "Insert")) && + e.ctrlKey && + !e.shiftKey && + !e.altKey && + !e.metaKey && + e.key === "Insert" && terminal.hasSelection() ) { e.preventDefault(); e.stopPropagation(); const selection = terminal.getSelection(); if (selection) { - navigator.clipboard.writeText(selection).catch(() => { + writeTextToClipboard(selection).catch(() => { toast.error(t("terminal.clipboardWriteFailed")); }); } @@ -137,8 +144,7 @@ export function ConsoleTerminal({ ) { e.preventDefault(); e.stopPropagation(); - navigator.clipboard - .readText() + readTextFromClipboard() .then((text) => { if (text) terminal.paste(text); }) @@ -192,20 +198,24 @@ export function ConsoleTerminal({ if (wsRef.current) { try { wsRef.current.send(JSON.stringify({ type: "disconnect" })); - } catch (error) {} + } catch { + // Best-effort disconnect during cleanup. + } wsRef.current.close(); wsRef.current = null; } terminal.dispose(); }; - }, [terminal]); + }, [terminal, t]); const disconnect = React.useCallback(() => { if (wsRef.current) { try { wsRef.current.send(JSON.stringify({ type: "disconnect" })); - } catch (error) {} + } catch { + // Best-effort disconnect. + } wsRef.current.close(); wsRef.current = null; } @@ -213,9 +223,11 @@ export function ConsoleTerminal({ if (terminal) { try { terminal.clear(); - } catch (error) {} + } catch { + // Terminal clear can fail after disposal. + } } - }, [terminal, t]); + }, [terminal]); const connect = React.useCallback(() => { if (!terminal || containerState !== "running") { @@ -226,15 +238,6 @@ export function ConsoleTerminal({ setIsConnecting(true); try { - const token = isElectron() - ? localStorage.getItem("jwt") - : getCookie("jwt"); - if (!token) { - toast.error(t("docker.authenticationRequired")); - setIsConnecting(false); - return; - } - if (fitAddonRef.current) { fitAddonRef.current.fit(); } @@ -410,7 +413,9 @@ export function ConsoleTerminal({ if (wsRef.current) { try { wsRef.current.send(JSON.stringify({ type: "disconnect" })); - } catch (error) {} + } catch { + // Best-effort disconnect during cleanup. + } wsRef.current.close(); wsRef.current = null; } diff --git a/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx b/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx index 84e72df0..101375fe 100644 --- a/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx +++ b/src/ui/desktop/apps/features/docker/components/ContainerDetail.tsx @@ -110,7 +110,6 @@ export function ContainerDetail({ className="flex-1 overflow-hidden px-3 pb-3 mt-3" > +
@@ -117,7 +117,7 @@ export function ContainerList({
) : ( -
+
{filteredContainers.map((container) => ( void; } +type ConnectionLogPayload = Omit; + +type SSHConnectionError = Error & { + connectionLogs?: ConnectionLogPayload[]; + requires_totp?: boolean; + requires_warpgate?: boolean; + sessionId?: string; + prompt?: string; + url?: string; + securityKey?: string; + status?: string; + reason?: "no_keyboard" | "auth_failed" | "timeout"; +}; + interface CreateIntent { id: string; type: "file" | "directory"; @@ -165,7 +180,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { >("no_keyboard"); const [pinnedFiles, setPinnedFiles] = useState>(new Set()); const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0); - const [isClosing, setIsClosing] = useState(false); const [hasConnectionError, setHasConnectionError] = useState(false); const [contextMenu, setContextMenu] = useState<{ @@ -446,11 +460,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { } catch (dirError: unknown) { console.error("Failed to load initial directory:", dirError); } - } catch (error: any) { + } catch (error: unknown) { + const sshError = error as SSHConnectionError; console.error("SSH connection failed:", error); - if (error?.connectionLogs) { - error.connectionLogs.forEach((log: any) => { + if (sshError.connectionLogs) { + sshError.connectionLogs.forEach((log) => { addLog({ type: log.type, stage: log.stage, @@ -458,25 +473,25 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { details: log.details, }); }); - if (error.requires_totp) { + if (sshError.requires_totp) { setTotpRequired(true); - setTotpSessionId(error.sessionId || currentHost.id.toString()); + setTotpSessionId(sshError.sessionId || currentHost.id.toString()); setTotpPrompt( - error.prompt || t("fileManager.verificationCodePrompt"), + sshError.prompt || t("fileManager.verificationCodePrompt"), ); setIsLoading(false); return; } - if (error.requires_warpgate) { + if (sshError.requires_warpgate) { setWarpgateRequired(true); - setWarpgateSessionId(error.sessionId || currentHost.id.toString()); - setWarpgateUrl(error.url || ""); - setWarpgateSecurityKey(error.securityKey || "N/A"); + setWarpgateSessionId(sshError.sessionId || currentHost.id.toString()); + setWarpgateUrl(sshError.url || ""); + setWarpgateSecurityKey(sshError.securityKey || "N/A"); setIsLoading(false); return; } - if (error.status === "auth_required") { - setAuthDialogReason(error.reason || "no_keyboard"); + if (sshError.status === "auth_required") { + setAuthDialogReason(sshError.reason || "no_keyboard"); setShowAuthDialog(true); setIsLoading(false); return; @@ -485,7 +500,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { addLog({ type: "error", stage: "connection", - message: error?.message || t("fileManager.failedToConnect"), + message: + error instanceof Error + ? error.message + : t("fileManager.failedToConnect"), }); } @@ -539,25 +557,36 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { return true; } catch (error: unknown) { if (currentLoadingPathRef.current === resolvedPath) { - const axiosError = error as { + // ApiError has .status directly; raw axios errors have .response.status + const apiError = error as { + status?: number; + code?: string; response?: { status?: number; data?: { needsSudo?: boolean; error?: string; sudoFailed?: boolean; + disconnected?: boolean; }; }; message?: string; }; - if (axiosError.response?.data?.needsSudo) { + const httpStatus = apiError.status ?? apiError.response?.status; + + // 409 = concurrent request already in flight — silently drop + if (httpStatus === 409) { + return false; + } + + if (apiError.response?.data?.needsSudo) { if (!sudoDialogOpen) { setPendingSudoOperation({ type: "navigate", path: resolvedPath }); setSudoDialogOpen(true); } - if (axiosError.response.data.sudoFailed) { + if (apiError.response.data.sudoFailed) { toast.error(t("fileManager.sudoAuthFailed")); } else { toast.error(t("fileManager.permissionDenied")); @@ -568,24 +597,50 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { console.error("Failed to load directory:", error); const errorMessage = - axiosError.response?.data?.error || - axiosError.message || - String(error); + apiError.response?.data?.error || apiError.message || String(error); - if (initialLoadDoneRef.current) { + const isConnectionError = + // 500s from the file manager are SSH channel/session errors + httpStatus === 500 || + httpStatus === 503 || + apiError.response?.data?.disconnected === true || + errorMessage?.includes("channel open failure") || + errorMessage?.includes("open failed") || + errorMessage?.includes("SSH connection not established") || + errorMessage?.includes("SSH session") || + errorMessage?.toLowerCase().includes("not connected"); + + if (isConnectionError && sshSessionId && currentHost) { + setIsReconnecting(true); + setIsLoading(false); + setFiles([]); + currentLoadingPathRef.current = ""; + + void (async () => { + const delays = [1000, 2000, 3000, 5000, 5000]; + for (let attempt = 0; attempt < delays.length; attempt++) { + await new Promise((r) => setTimeout(r, delays[attempt])); + try { + await ensureSSHConnection(); + setIsReconnecting(false); + loadDirectory(resolvedPath); + return; + } catch { + // keep retrying + } + } + setIsReconnecting(false); + handleCloseWithError( + t("fileManager.failedToLoadDirectory") + ": " + errorMessage, + ); + })(); + + return false; + } else if (initialLoadDoneRef.current) { toast.error( t("fileManager.failedToLoadDirectory") + ": " + errorMessage, ); } - - if ( - errorMessage?.includes("connection") || - errorMessage?.includes("SSH") - ) { - handleCloseWithError( - t("fileManager.failedToLoadDirectory") + ": " + errorMessage, - ); - } } return false; } finally { @@ -595,7 +650,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { } } }, - [sshSessionId, isLoading, clearSelection, t, sudoDialogOpen], + [sshSessionId, isLoading, clearSelection, t, sudoDialogOpen, currentHost], ); const debouncedLoadDirectory = useCallback( @@ -699,19 +754,14 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { reader.onerror = () => reject(reader.error); reader.onload = () => { - if (reader.result instanceof ArrayBuffer) { - const bytes = new Uint8Array(reader.result); - let binary = ""; - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]); - } - const base64 = btoa(binary); + if (typeof reader.result === "string") { + const base64 = reader.result.split(",")[1] || ""; resolve(base64); } else { reject(new Error("Failed to read file")); } }; - reader.readAsArrayBuffer(file); + reader.readAsDataURL(file); }); await uploadSSHFile( @@ -1530,40 +1580,30 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { } async function ensureSSHConnection() { - if (!sshSessionId || !currentHost || isReconnecting) return; + if (!sshSessionId || !currentHost) return; - try { - const status = await getSSHStatus(sshSessionId); + const status = await getSSHStatus(sshSessionId); - if (!status.connected && !isReconnecting) { - setIsReconnecting(true); - await connectSSH(sshSessionId, { - hostId: currentHost.id, - ip: currentHost.ip, - port: currentHost.port, - username: currentHost.username, - password: currentHost.password, - sshKey: currentHost.key, - keyPassword: currentHost.keyPassword, - authType: currentHost.authType, - credentialId: currentHost.credentialId, - userId: currentHost.userId, - jumpHosts: currentHost.jumpHosts, - useSocks5: currentHost.useSocks5, - socks5Host: currentHost.socks5Host, - socks5Port: currentHost.socks5Port, - socks5Username: currentHost.socks5Username, - socks5Password: currentHost.socks5Password, - socks5ProxyChain: currentHost.socks5ProxyChain, - }); - } - } catch (error) { - handleCloseWithError( - `SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`, - ); - throw error; - } finally { - setIsReconnecting(false); + if (!status.connected) { + await connectSSH(sshSessionId, { + hostId: currentHost.id, + ip: currentHost.ip, + port: currentHost.port, + username: currentHost.username, + password: currentHost.password, + sshKey: currentHost.key, + keyPassword: currentHost.keyPassword, + authType: currentHost.authType, + credentialId: currentHost.credentialId, + userId: currentHost.userId, + jumpHosts: currentHost.jumpHosts, + useSocks5: currentHost.useSocks5, + socks5Host: currentHost.socks5Host, + socks5Port: currentHost.socks5Port, + socks5Username: currentHost.socks5Username, + socks5Password: currentHost.socks5Password, + socks5ProxyChain: currentHost.socks5ProxyChain, + }); } } diff --git a/src/ui/desktop/apps/features/file-manager/FileManagerContextMenu.tsx b/src/ui/desktop/apps/features/file-manager/FileManagerContextMenu.tsx index f1ea609c..431f0f19 100644 --- a/src/ui/desktop/apps/features/file-manager/FileManagerContextMenu.tsx +++ b/src/ui/desktop/apps/features/file-manager/FileManagerContextMenu.tsx @@ -20,7 +20,7 @@ import { FileArchive, } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { Kbd, KbdGroup } from "@/components/ui/kbd.tsx"; +import { Kbd, KbdKey, KbdSeparator } from "@/components/ui/kbd.tsx"; interface FileItem { name: string; @@ -491,11 +491,14 @@ export function FileManagerContextMenu({ return {keys[0]}; } return ( - + {keys.map((key, index) => ( - {key} + <> + {key} + {index < keys.length - 1 && } + ))} - + ); }; diff --git a/src/ui/desktop/apps/features/file-manager/FileManagerSidebar.tsx b/src/ui/desktop/apps/features/file-manager/FileManagerSidebar.tsx index 9f3f0bca..46431f42 100644 --- a/src/ui/desktop/apps/features/file-manager/FileManagerSidebar.tsx +++ b/src/ui/desktop/apps/features/file-manager/FileManagerSidebar.tsx @@ -1,15 +1,12 @@ -import React, { useState, useEffect } from "react"; +import React, { + useState, + useEffect, + useRef, + useCallback, + useMemo, +} from "react"; import { cn } from "@/lib/utils.ts"; -import { - ChevronRight, - ChevronDown, - Folder, - File, - Star, - Clock, - Bookmark, - FolderOpen, -} from "lucide-react"; +import { Star, Clock, Bookmark, File, Folder } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { SSHHost } from "@/types"; import { @@ -22,6 +19,9 @@ import { removeFolderShortcut, } from "@/ui/main-axios.ts"; import { toast } from "sonner"; +import FolderTree from "@/components/ui/folder.tsx"; + +// ─── Interfaces ──────────────────────────────────────────────────────────────── interface RecentFileData { id: number; @@ -71,6 +71,8 @@ interface FileManagerSidebarProps { refreshTrigger?: number; } +// ─── Component ───────────────────────────────────────────────────────────────── + export function FileManagerSidebar({ currentHost, currentPath, @@ -80,14 +82,22 @@ export function FileManagerSidebar({ refreshTrigger, }: FileManagerSidebarProps) { const { t } = useTranslation(); + + // ── Quick access state (API-backed) ────────────────────────────────────────── const [recentItems, setRecentItems] = useState([]); const [pinnedItems, setPinnedItems] = useState([]); const [shortcuts, setShortcuts] = useState([]); - const [directoryTree, setDirectoryTree] = useState([]); - const [expandedFolders, setExpandedFolders] = useState>( - new Set(["root"]), - ); + // ── Directory tree state ────────────────────────────────────────────────────── + const [directoryTree, setDirectoryTree] = useState([]); + + /** + * Tracks which folder paths have already been lazy-loaded so we don't + * re-fetch on every re-selection / collapse-reopen. + */ + const loadedFoldersRef = useRef>(new Set(["/"])); + + // ── Context menu state ──────────────────────────────────────────────────────── const [contextMenu, setContextMenu] = useState<{ x: number; y: number; @@ -100,16 +110,47 @@ export function FileManagerSidebar({ item: null, }); + // ─── Effects ────────────────────────────────────────────────────────────────── + useEffect(() => { loadQuickAccessData(); }, [currentHost, refreshTrigger]); useEffect(() => { if (sshSessionId) { + loadedFoldersRef.current = new Set(["/"]); loadDirectoryTree(); } }, [sshSessionId]); + // When currentPath changes externally (grid navigation), ensure the parent + // directory is loaded in the tree so the selection highlight can appear. + useEffect(() => { + if (!sshSessionId || currentPath === "/") return; + + const parentPath = + currentPath.substring(0, currentPath.lastIndexOf("/")) || "/"; + + const findByPath = (items: SidebarItem[]): SidebarItem | null => { + for (const item of items) { + if (item.path === parentPath) return item; + if (item.children) { + const found = findByPath(item.children); + if (found) return found; + } + } + return null; + }; + + const parent = findByPath(directoryTree); + if (parent && !loadedFoldersRef.current.has(parent.path)) { + loadedFoldersRef.current.add(parent.path); + loadSubdirectory(parent.id, parent.path); + } + }, [currentPath, sshSessionId]); + + // ─── API: Quick access ──────────────────────────────────────────────────────── + const loadQuickAccessData = async () => { if (!currentHost?.id) return; @@ -155,114 +196,13 @@ export function FileManagerSidebar({ } }; - const handleRemoveRecentFile = async (item: SidebarItem) => { - if (!currentHost?.id) return; + // ─── API: Directory tree ────────────────────────────────────────────────────── - try { - await removeRecentFile(currentHost.id, item.path); - loadQuickAccessData(); - toast.success( - t("fileManager.removedFromRecentFiles", { name: item.name }), - ); - } catch (error) { - console.error("Failed to remove recent file:", error); - toast.error(t("fileManager.removeFailed")); - } - }; - - const handleUnpinFile = async (item: SidebarItem) => { - if (!currentHost?.id) return; - - try { - await removePinnedFile(currentHost.id, item.path); - loadQuickAccessData(); - toast.success(t("fileManager.unpinnedSuccessfully", { name: item.name })); - } catch (error) { - console.error("Failed to unpin file:", error); - toast.error(t("fileManager.unpinFailed")); - } - }; - - const handleRemoveShortcut = async (item: SidebarItem) => { - if (!currentHost?.id) return; - - try { - await removeFolderShortcut(currentHost.id, item.path); - loadQuickAccessData(); - toast.success(t("fileManager.removedShortcut", { name: item.name })); - } catch (error) { - console.error("Failed to remove shortcut:", error); - toast.error(t("fileManager.removeShortcutFailed")); - } - }; - - const handleClearAllRecent = async () => { - if (!currentHost?.id || recentItems.length === 0) return; - - try { - await Promise.all( - recentItems.map((item) => removeRecentFile(currentHost.id, item.path)), - ); - loadQuickAccessData(); - toast.success(t("fileManager.clearedAllRecentFiles")); - } catch (error) { - console.error("Failed to clear recent files:", error); - toast.error(t("fileManager.clearFailed")); - } - }; - - const handleContextMenu = (e: React.MouseEvent, item: SidebarItem) => { - e.preventDefault(); - e.stopPropagation(); - - setContextMenu({ - x: e.clientX, - y: e.clientY, - isVisible: true, - item, - }); - }; - - const closeContextMenu = () => { - setContextMenu((prev) => ({ ...prev, isVisible: false, item: null })); - }; - - useEffect(() => { - if (!contextMenu.isVisible) return; - - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as Element; - const menuElement = document.querySelector("[data-sidebar-context-menu]"); - - if (!menuElement?.contains(target)) { - closeContextMenu(); - } - }; - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "Escape") { - closeContextMenu(); - } - }; - - const timeoutId = setTimeout(() => { - document.addEventListener("mousedown", handleClickOutside); - document.addEventListener("keydown", handleKeyDown); - }, 50); - - return () => { - clearTimeout(timeoutId); - document.removeEventListener("mousedown", handleClickOutside); - document.removeEventListener("keydown", handleKeyDown); - }; - }, [contextMenu.isVisible]); - - const loadDirectoryTree = async () => { + const loadDirectoryTree = async (attempt = 0) => { if (!sshSessionId) return; try { const response = await listSSHFiles(sshSessionId, "/"); - const rootFiles = (response.files || []) as DirectoryItemData[]; const rootFolders = rootFiles.filter( (item: DirectoryItemData) => item.type === "directory", @@ -287,7 +227,15 @@ export function FileManagerSidebar({ children: rootTreeItems, }, ]); - } catch (error) { + } catch (error: unknown) { + const status = + (error as { status?: number })?.status || + (error as { response?: { status?: number } })?.response?.status; + if (status === 409 && attempt < 3) { + // Another request was already listing "/" — retry after a short delay + setTimeout(() => loadDirectoryTree(attempt + 1), 600); + return; + } console.error("Failed to load directory tree:", error); setDirectoryTree([ { @@ -302,11 +250,116 @@ export function FileManagerSidebar({ } }; - const handleItemClick = (item: SidebarItem) => { - if (item.type === "folder") { - toggleFolder(item.id, item.path); - onPathChange(item.path); - } else if (item.type === "recent" || item.type === "pinned") { + /** + * Lazily fetches subdirectory contents and patches them into the tree state. + * Called the first time a folder is expanded via FolderTree's onSelect. + */ + const loadSubdirectory = useCallback( + async (folderId: string, folderPath: string) => { + if (!sshSessionId) return; + + try { + const subResponse = await listSSHFiles(sshSessionId, folderPath); + const subFiles = (subResponse.files || []) as DirectoryItemData[]; + const subFolders = subFiles.filter( + (item: DirectoryItemData) => item.type === "directory", + ); + + const subTreeItems = subFolders.map((folder: DirectoryItemData) => ({ + id: `folder-${folder.path.replace(/\//g, "-")}`, + name: folder.name, + path: folder.path, + type: "folder" as const, + isExpanded: false, + children: [], + })); + + setDirectoryTree((prevTree) => { + const updateChildren = (items: SidebarItem[]): SidebarItem[] => + items.map((item) => { + if (item.id === folderId) { + return { ...item, children: subTreeItems }; + } + if (item.children) { + return { ...item, children: updateChildren(item.children) }; + } + return item; + }); + return updateChildren(prevTree); + }); + } catch (error: unknown) { + const status = + (error as { status?: number })?.status || + (error as { response?: { status?: number } })?.response?.status; + if (status === 409) { + // Another request was listing this path — retry after the lock clears + setTimeout(() => loadSubdirectory(folderId, folderPath), 600); + return; + } + console.error("Failed to load subdirectory:", error); + } + }, + [sshSessionId], + ); + + // ─── Quick-access mutation handlers ────────────────────────────────────────── + + const handleRemoveRecentFile = async (item: SidebarItem) => { + if (!currentHost?.id) return; + try { + await removeRecentFile(currentHost.id, item.path); + loadQuickAccessData(); + toast.success( + t("fileManager.removedFromRecentFiles", { name: item.name }), + ); + } catch (error) { + console.error("Failed to remove recent file:", error); + toast.error(t("fileManager.removeFailed")); + } + }; + + const handleUnpinFile = async (item: SidebarItem) => { + if (!currentHost?.id) return; + try { + await removePinnedFile(currentHost.id, item.path); + loadQuickAccessData(); + toast.success(t("fileManager.unpinnedSuccessfully", { name: item.name })); + } catch (error) { + console.error("Failed to unpin file:", error); + toast.error(t("fileManager.unpinFailed")); + } + }; + + const handleRemoveShortcut = async (item: SidebarItem) => { + if (!currentHost?.id) return; + try { + await removeFolderShortcut(currentHost.id, item.path); + loadQuickAccessData(); + toast.success(t("fileManager.removedShortcut", { name: item.name })); + } catch (error) { + console.error("Failed to remove shortcut:", error); + toast.error(t("fileManager.removeShortcutFailed")); + } + }; + + const handleClearAllRecent = async () => { + if (!currentHost?.id || recentItems.length === 0) return; + try { + await Promise.all( + recentItems.map((item) => removeRecentFile(currentHost.id, item.path)), + ); + loadQuickAccessData(); + toast.success(t("fileManager.clearedAllRecentFiles")); + } catch (error) { + console.error("Failed to clear recent files:", error); + toast.error(t("fileManager.clearFailed")); + } + }; + + // ─── Quick-access item click ────────────────────────────────────────────────── + + const handleQuickAccessClick = (item: SidebarItem) => { + if (item.type === "recent" || item.type === "pinned") { if (onFileOpen) { onFileOpen(item); } else { @@ -319,132 +372,180 @@ export function FileManagerSidebar({ } }; - const toggleFolder = async (folderId: string, folderPath?: string) => { - const newExpanded = new Set(expandedFolders); + // ─── FolderTree directory selection (onSelect callback) ────────────────────── - if (newExpanded.has(folderId)) { - newExpanded.delete(folderId); - } else { - newExpanded.add(folderId); - - if (sshSessionId && folderPath && folderPath !== "/") { - try { - const subResponse = await listSSHFiles(sshSessionId, folderPath); - - const subFiles = (subResponse.files || []) as DirectoryItemData[]; - const subFolders = subFiles.filter( - (item: DirectoryItemData) => item.type === "directory", - ); - - const subTreeItems = subFolders.map((folder: DirectoryItemData) => ({ - id: `folder-${folder.path.replace(/\//g, "-")}`, - name: folder.name, - path: folder.path, - type: "folder" as const, - isExpanded: false, - children: [], - })); - - setDirectoryTree((prevTree) => { - const updateChildren = (items: SidebarItem[]): SidebarItem[] => { - return items.map((item) => { - if (item.id === folderId) { - return { ...item, children: subTreeItems }; - } else if (item.children) { - return { ...item, children: updateChildren(item.children) }; - } - return item; - }); - }; - return updateChildren(prevTree); - }); - } catch (error) { - console.error("Failed to load subdirectory:", error); + /** + * Called by FolderTree whenever the user selects (clicks) a tree item. + * We navigate to the folder and lazily load children on first visit. + */ + const handleDirectorySelect = useCallback( + async (id: string) => { + // Walk the tree to find the item by id + const findItem = (items: SidebarItem[]): SidebarItem | null => { + for (const item of items) { + if (item.id === id) return item; + if (item.children) { + const found = findItem(item.children); + if (found) return found; + } } - } - } + return null; + }; - setExpandedFolders(newExpanded); + const item = findItem(directoryTree); + if (!item) return; + + // Navigate to path + onPathChange(item.path); + + // Lazy-load children the first time this folder is expanded + if ( + sshSessionId && + item.path !== "/" && + !loadedFoldersRef.current.has(item.path) + ) { + loadedFoldersRef.current.add(item.path); + await loadSubdirectory(id, item.path); + } + }, + [directoryTree, onPathChange, sshSessionId, loadSubdirectory], + ); + + // ─── Context menu ───────────────────────────────────────────────────────────── + + const handleContextMenu = (e: React.MouseEvent, item: SidebarItem) => { + e.preventDefault(); + e.stopPropagation(); + setContextMenu({ x: e.clientX, y: e.clientY, isVisible: true, item }); }; - const renderSidebarItem = (item: SidebarItem, level: number = 0) => { - const isExpanded = expandedFolders.has(item.id); - const isActive = currentPath === item.path; + const closeContextMenu = () => { + setContextMenu((prev) => ({ ...prev, isVisible: false, item: null })); + }; + + useEffect(() => { + if (!contextMenu.isVisible) return; + + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + const menuElement = document.querySelector("[data-sidebar-context-menu]"); + if (!menuElement?.contains(target)) closeContextMenu(); + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") closeContextMenu(); + }; + + const timeoutId = setTimeout(() => { + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + }, 50); + + return () => { + clearTimeout(timeoutId); + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [contextMenu.isVisible]); + + // ─── Derive selected tree node + ancestors from currentPath ────────────────── + + const { selectedTreeId, ancestorIds } = useMemo(() => { + if (currentPath === "/") + return { selectedTreeId: "root", ancestorIds: new Set() }; + + const ancestors: string[] = []; + const findByPath = ( + items: SidebarItem[], + path: string[], + ): string | null => { + for (const item of items) { + if (item.path === currentPath) { + ancestors.push(...path, "root"); + return item.id; + } + if (item.children) { + const found = findByPath(item.children, [...path, item.id]); + if (found) return found; + } + } + return null; + }; + + const id = findByPath(directoryTree, []); + return { selectedTreeId: id, ancestorIds: new Set(ancestors) }; + }, [currentPath, directoryTree]); + + // ─── Render helpers ─────────────────────────────────────────────────────────── + + /** + * Recursively renders directory tree items using FolderTree.Item + Content. + * + * FolderTree.Item detects "has children" via React.Children.count > 0. + * By always wrapping children in (even when the + * children array is empty), every directory shows the expand chevron. + * FolderTree.Content internally shows nothing when its own children are + * absent, so an unloaded folder simply expands to an empty state while + * the async fetch fills it in. + */ + const renderFolderTreeItem = (item: SidebarItem): React.ReactNode => ( + + + {item.children?.map((child) => renderFolderTreeItem(child))} + + + ); + + /** + * Styled quick-access row (recent / pinned / shortcut). + * Mirrors FolderTree.Item's visual language but adds onContextMenu support. + */ + const renderQuickAccessItem = (item: SidebarItem, icon: React.ReactNode) => { + const dirPath = + item.type === "shortcut" + ? item.path + : item.path.substring(0, item.path.lastIndexOf("/")) || "/"; + const isActive = currentPath === dirPath; return ( -
-
handleItemClick(item)} - onContextMenu={(e) => { - if ( - item.type === "recent" || - item.type === "pinned" || - item.type === "shortcut" - ) { - handleContextMenu(e, item); - } - }} - > - {item.type === "folder" && ( - - )} - - {item.type === "folder" ? ( - isExpanded ? ( - - ) : ( - - ) - ) : ( - - )} - - {item.name} -
- - {item.type === "folder" && isExpanded && item.children && ( -
- {item.children.map((child) => renderSidebarItem(child, level + 1))} -
+
handleQuickAccessClick(item)} + onContextMenu={(e) => handleContextMenu(e, item)} + title={item.path} + > + {/* indent spacer matching FolderTree level-1 padding */} +
); }; + /** + * Section header + items list. + * Returns null when items is empty so empty sections are hidden. + */ const renderSection = ( title: string, - icon: React.ReactNode, + headerIcon: React.ReactNode, items: SidebarItem[], + renderItem: (item: SidebarItem) => React.ReactNode, ) => { if (items.length === 0) return null; - return ( -
-
- {icon} +
+
+ {headerIcon} {title}
-
- {items.map((item) => renderSidebarItem(item))} -
+
{items.map((item) => renderItem(item))}
); }; @@ -452,53 +553,126 @@ export function FileManagerSidebar({ const hasQuickAccessItems = recentItems.length > 0 || pinnedItems.length > 0 || shortcuts.length > 0; + // ─── Render ─────────────────────────────────────────────────────────────────── + return ( <>
-
+
+ {/* ── Recent files ──────────────────────────────────────── */} {renderSection( t("fileManager.recent"), , recentItems, + (item) => + renderQuickAccessItem( + item, + , + ), )} + + {/* ── Pinned files ───────────────────────────────────────── */} {renderSection( t("fileManager.pinned"), , pinnedItems, + (item) => + renderQuickAccessItem( + item, + , + ), )} + + {/* ── Folder shortcuts ───────────────────────────────────── */} {renderSection( t("fileManager.folderShortcuts"), , shortcuts, + (item) => + renderQuickAccessItem( + item, + , + ), )} + {/* ── Directory tree ─────────────────────────────────────── */}
-
+
{t("fileManager.directories")}
-
- {directoryTree.map((item) => renderSidebarItem(item))} + +
+ {/* + * FolderTree.Root manages its own expansion state internally. + * We pass `onSelect` to be notified of clicks so we can: + * 1. Navigate to the selected path (onPathChange) + * 2. Lazy-load subdirectory children on first expand + * + * The root "/" folder is pre-expanded via defaultExpanded. + * + * className overrides strip the default card styling so the + * tree blends seamlessly into the sidebar panel. + */} + handleDirectorySelect(id)} + className="bg-transparent border-0 rounded-none shadow-none" + > + {directoryTree.map((item) => renderFolderTreeItem(item))} +
+ {/* ── Context menu ─────────────────────────────────────────────── */} {contextMenu.isVisible && contextMenu.item && ( <> + {/* Transparent backdrop to capture outside clicks */}
+
+ {/* Recent item actions */} {contextMenu.item.type === "recent" && ( <> + {recentItems.length > 1 && ( <>
)} + {/* Shortcut item actions */} {contextMenu.item.type === "shortcut" && (
@@ -583,51 +525,67 @@ export function FileViewer({
{t("fileManager.save")} - - Ctrl+S - + + + Ctrl+S + +
{t("fileManager.selectAll")} - - Ctrl+A - + + + Ctrl+A + +
{t("fileManager.undo")} - - Ctrl+Z - + + + Ctrl+Z + +
{t("fileManager.redo")} - - Ctrl+Y - + + + Ctrl+Y + +
{t("fileManager.toggleComment")} - - Ctrl+/ - + + + Ctrl+/ + +
{t("fileManager.autoComplete")} - - Ctrl+Space - + + + Ctrl+Space + +
{t("fileManager.moveLineUp")} - - Alt+↑ - + + + Alt+↑ + +
{t("fileManager.moveLineDown")} - - Alt+↓ - + + + Alt+↓ + +
@@ -700,136 +658,32 @@ export function FileViewer({ )} {fileTypeInfo.type === "image" && !showLargeFileWarning && ( -
- {imageLoadError ? ( -
- -

- {t("fileManager.imageLoadError")} -

-

{file.name}

- {onDownload && ( - - )} -
- ) : ( - - - {file.name} { - setImageLoading(false); - setImageLoadError(false); - - const img = e.currentTarget; - if ( - onMediaDimensionsChange && - img.naturalWidth && - img.naturalHeight - ) { - onMediaDimensionsChange({ - width: img.naturalWidth, - height: img.naturalHeight, - }); - } - }} - onError={() => { - setImageLoading(false); - setImageLoadError(true); - }} - /> - - - )} - - {imageLoading && !imageLoadError && ( -
-
-
-

- Loading image... -

-
-
- )} -
+ }> + + )} {shouldShowAsText && !showLargeFileWarning && (
{isEditable ? ( - handleContentChange(value)} - onFocus={() => setEditorFocused(true)} - onBlur={() => setEditorFocused(false)} - extensions={[ - ...(getLanguageExtension(file.name) - ? [getLanguageExtension(file.name)!] - : []), - history(), - search(), - autocompletion(), - keymap.of([ - ...defaultKeymap, - ...searchKeymap, - ...historyKeymap, - ...completionKeymap, - { - key: "Mod-/", - run: toggleComment, - preventDefault: true, - }, - { - key: "Mod-h", - run: () => { - return false; - }, - preventDefault: true, - }, - ]), - EditorView.theme({ - "&": { - height: "100%", - }, - ".cm-scroller": { - overflow: "auto", - scrollbarWidth: "thin", - scrollbarColor: - "var(--scrollbar-thumb) var(--scrollbar-track)", - }, - ".cm-editor": { - height: "100%", - }, - }), - ]} - theme={oneDark} - placeholder={t("fileManager.startTyping")} - className="h-full" - basicSetup={{ - lineNumbers: true, - foldGutter: true, - dropCursor: false, - allowMultipleSelections: false, - indentOnInput: true, - bracketMatching: true, - closeBrackets: true, - autocompletion: true, - highlightSelectionMatches: false, - scrollPastEnd: false, - }} - /> + } + > + setEditorFocused(true)} + onBlur={() => setEditorFocused(false)} + placeholder={t("fileManager.startTyping")} + /> + ) : (
{editedContent || content || t("fileManager.fileIsEmpty")} @@ -967,226 +821,27 @@ export function FileViewer({
- - {String(children).replace(/\n$/, "")} - - ) : ( - - {children} - - ); - }, - h1: ({ children }) => ( -

- {children} -

- ), - h2: ({ children }) => ( -

- {children} -

- ), - h3: ({ children }) => ( -

- {children} -

- ), - h4: ({ children }) => ( -

- {children} -

- ), - p: ({ children }) => ( -

- {children} -

- ), - ul: ({ children }) => ( -
    - {children} -
- ), - ol: ({ children }) => ( -
    - {children} -
- ), - li: ({ children }) => ( -
  • {children}
  • - ), - blockquote: ({ children }) => ( -
    - {children} -
    - ), - table: ({ children }) => ( -
    - - {children} -
    -
    - ), - thead: ({ children }) => ( - {children} - ), - tbody: ({ children }) => {children}, - tr: ({ children }) => ( - - {children} - - ), - th: ({ children }) => ( - - {children} - - ), - td: ({ children }) => ( - - {children} - - ), - a: ({ href, children }) => ( - - {children} - - ), - }} + + } > - {editedContent || "Nothing to preview yet..."} -
    + +
    ) : (
    - - {String(children).replace(/\n$/, "")} - - ) : ( - - {children} - - ); - }, - h1: ({ children }) => ( -

    - {children} -

    - ), - h2: ({ children }) => ( -

    - {children} -

    - ), - h3: ({ children }) => ( -

    - {children} -

    - ), - h4: ({ children }) => ( -

    - {children} -

    - ), - p: ({ children }) => ( -

    - {children} -

    - ), - ul: ({ children }) => ( -
      - {children} -
    - ), - ol: ({ children }) => ( -
      - {children} -
    - ), - li: ({ children }) => ( -
  • {children}
  • - ), - blockquote: ({ children }) => ( -
    - {children} -
    - ), - table: ({ children }) => ( -
    - - {children} -
    -
    - ), - thead: ({ children }) => ( - {children} - ), - tbody: ({ children }) => {children}, - tr: ({ children }) => ( - {children} - ), - th: ({ children }) => ( - - {children} - - ), - td: ({ children }) => ( - - {children} - - ), - a: ({ href, children }) => ( - - {children} - - ), - }} + } > - {editedContent} -
    + +
    )} @@ -1195,202 +850,28 @@ export function FileViewer({ )} {fileTypeInfo.type === "pdf" && !showLargeFileWarning && ( -
    -
    -
    -
    -
    - - - {t("fileManager.pageXOfY", { - current: pageNumber, - total: numPages || 0, - })} - - -
    -
    - - - {Math.round(pdfScale * 100)}% - - -
    -
    -
    -
    - -
    -
    - {pdfError ? ( -
    - -

    - Cannot load PDF -

    -

    - There was an error loading this PDF file. -

    - {onDownload && ( - - )} -
    - ) : ( - { - setNumPages(numPages); - setPdfError(false); - - if (onMediaDimensionsChange) { - onMediaDimensionsChange({ - width: 800, - height: 600, - }); - } - }} - onLoadError={(error) => { - console.error("PDF load error:", error); - setPdfError(true); - }} - loading={ -
    -
    -

    - Loading PDF... -

    -
    - } - > - -
    -

    - Loading page... -

    -
    - } - /> - - )} -
    -
    -
    + } + > + + )} {fileTypeInfo.type === "audio" && !showLargeFileWarning && ( -
    -
    - {(() => { - const ext = file.name.split(".").pop()?.toLowerCase() || ""; - const mimeType = (() => { - switch (ext) { - case "mp3": - return "audio/mpeg"; - case "wav": - return "audio/wav"; - case "flac": - return "audio/flac"; - case "ogg": - return "audio/ogg"; - case "aac": - return "audio/aac"; - case "m4a": - return "audio/mp4"; - default: - return "audio/mpeg"; - } - })(); - - const audioUrl = `data:${mimeType};base64,${content}`; - - return ( -
    -
    -
    - -
    -
    - -
    -

    - {file.name.replace(/\.[^/.]+$/, "")} -

    -

    - {ext.toUpperCase()} • {formatFileSize(file.size, t)} -

    -
    - -
    - { - if (onMediaDimensionsChange) { - onMediaDimensionsChange({ - width: 600, - height: 400, - }); - } - }} - onError={(e) => { - console.error("Audio playback error:", e); - }} - showJumpControls={false} - showSkipControls={false} - showDownloadProgress={true} - customAdditionalControls={[]} - customVolumeControls={[]} - /> -
    -
    - ); - })()} -
    -
    + } + > + + )} {fileTypeInfo.type === "unknown" && diff --git a/src/ui/desktop/apps/features/file-manager/components/ImagePreview.tsx b/src/ui/desktop/apps/features/file-manager/components/ImagePreview.tsx new file mode 100644 index 00000000..8a43c921 --- /dev/null +++ b/src/ui/desktop/apps/features/file-manager/components/ImagePreview.tsx @@ -0,0 +1,112 @@ +import React, { useState } from "react"; +import { PhotoProvider, PhotoView } from "react-photo-view"; +import "react-photo-view/dist/react-photo-view.css"; +import { AlertCircle, Download } from "lucide-react"; +import { Button } from "@/components/ui/button.tsx"; +import { useTranslation } from "react-i18next"; + +interface ImagePreviewProps { + content: string; + fileName: string; + onDownload?: () => void; + onMediaDimensionsChange?: (dimensions: { + width: number; + height: number; + }) => void; +} + +function getImageDataUrl(content: string, fileName: string): string { + const ext = fileName.split(".").pop()?.toLowerCase() || ""; + + const mimeTypes: Record = { + svg: "image/svg+xml", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + bmp: "image/bmp", + ico: "image/x-icon", + tiff: "image/tiff", + tif: "image/tiff", + }; + + const mimeType = mimeTypes[ext] || "image/png"; + return `data:${mimeType};base64,${content}`; +} + +export function ImagePreview({ + content, + fileName, + onDownload, + onMediaDimensionsChange, +}: ImagePreviewProps) { + const { t } = useTranslation(); + const [imageLoadError, setImageLoadError] = useState(false); + const [imageLoading, setImageLoading] = useState(true); + const imageUrl = getImageDataUrl(content, fileName); + + return ( +
    + {imageLoadError ? ( +
    + +

    + {t("fileManager.imageLoadError")} +

    +

    {fileName}

    + {onDownload && ( + + )} +
    + ) : ( + + + {fileName} { + setImageLoading(false); + setImageLoadError(false); + + const img = e.currentTarget; + if ( + onMediaDimensionsChange && + img.naturalWidth && + img.naturalHeight + ) { + onMediaDimensionsChange({ + width: img.naturalWidth, + height: img.naturalHeight, + }); + } + }} + onError={() => { + setImageLoading(false); + setImageLoadError(true); + }} + /> + + + )} + + {imageLoading && !imageLoadError && ( +
    +
    +
    +

    Loading image...

    +
    +
    + )} +
    + ); +} diff --git a/src/ui/desktop/apps/features/file-manager/components/MarkdownRenderer.tsx b/src/ui/desktop/apps/features/file-manager/components/MarkdownRenderer.tsx new file mode 100644 index 00000000..ced15600 --- /dev/null +++ b/src/ui/desktop/apps/features/file-manager/components/MarkdownRenderer.tsx @@ -0,0 +1,119 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { oneDark as syntaxTheme } from "react-syntax-highlighter/dist/esm/styles/prism"; + +interface MarkdownRendererProps { + content: string; + compact?: boolean; +} + +export function MarkdownRenderer({ + content, + compact = false, +}: MarkdownRendererProps) { + const h1Class = compact + ? "text-2xl font-bold mb-4 mt-6 text-foreground border-b border-border pb-2" + : "text-3xl font-bold mb-6 mt-8 text-foreground border-b border-border pb-2"; + const h2Class = compact + ? "text-xl font-semibold mb-3 mt-5 text-foreground border-b border-border pb-1" + : "text-2xl font-semibold mb-4 mt-6 text-foreground border-b border-border pb-1"; + const h3Class = compact + ? "text-lg font-semibold mb-2 mt-4 text-foreground" + : "text-xl font-semibold mb-3 mt-4 text-foreground"; + const h4Class = compact + ? "text-base font-semibold mb-2 mt-3 text-foreground" + : "text-lg font-semibold mb-2 mt-3 text-foreground"; + const pClass = compact + ? "mb-3 text-foreground leading-relaxed" + : "mb-4 text-foreground leading-relaxed"; + const listClass = compact + ? "mb-3 ml-4 text-foreground" + : "mb-4 ml-6 text-foreground"; + const quoteClass = compact + ? "border-l-4 border-blue-500 pl-3 mb-3 italic text-muted-foreground bg-muted/30 py-1" + : "border-l-4 border-blue-500 pl-4 mb-4 italic text-muted-foreground bg-muted/30 py-2"; + const tableWrapClass = compact + ? "mb-3 overflow-x-auto thin-scrollbar" + : "mb-4 overflow-x-auto thin-scrollbar"; + const tableClass = compact + ? "min-w-full border border-border rounded-lg text-sm" + : "min-w-full border border-border rounded-lg"; + const thClass = compact + ? "px-3 py-2 text-left font-semibold text-foreground" + : "px-4 py-2 text-left font-semibold text-foreground"; + const tdClass = compact + ? "px-3 py-2 text-foreground" + : "px-4 py-2 text-foreground"; + + return ( + + {String(children).replace(/\n$/, "")} + + ) : ( + + {children} + + ); + }, + h1: ({ children }) =>

    {children}

    , + h2: ({ children }) =>

    {children}

    , + h3: ({ children }) =>

    {children}

    , + h4: ({ children }) =>

    {children}

    , + p: ({ children }) =>

    {children}

    , + ul: ({ children }) => ( +
      {children}
    + ), + ol: ({ children }) => ( +
      {children}
    + ), + li: ({ children }) => ( +
  • {children}
  • + ), + blockquote: ({ children }) => ( +
    {children}
    + ), + table: ({ children }) => ( +
    + {children}
    +
    + ), + thead: ({ children }) => {children}, + tbody: ({ children }) => {children}, + tr: ({ children }) => ( + {children} + ), + th: ({ children }) => {children}, + td: ({ children }) => {children}, + a: ({ href, children }) => ( + + {children} + + ), + }} + > + {content} +
    + ); +} diff --git a/src/ui/desktop/apps/features/file-manager/components/PdfPreview.tsx b/src/ui/desktop/apps/features/file-manager/components/PdfPreview.tsx new file mode 100644 index 00000000..e16949db --- /dev/null +++ b/src/ui/desktop/apps/features/file-manager/components/PdfPreview.tsx @@ -0,0 +1,147 @@ +import React, { useState } from "react"; +import { Document, Page, pdfjs } from "react-pdf"; +import { AlertCircle, Download } from "lucide-react"; +import { Button } from "@/components/ui/button.tsx"; +import { useTranslation } from "react-i18next"; + +pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.js"; + +interface PdfPreviewProps { + content: string; + onDownload?: () => void; + onMediaDimensionsChange?: (dimensions: { + width: number; + height: number; + }) => void; +} + +export function PdfPreview({ + content, + onDownload, + onMediaDimensionsChange, +}: PdfPreviewProps) { + const { t } = useTranslation(); + const [numPages, setNumPages] = useState(null); + const [pageNumber, setPageNumber] = useState(1); + const [pdfScale, setPdfScale] = useState(1.2); + const [pdfError, setPdfError] = useState(false); + + return ( +
    +
    +
    +
    +
    + + + {t("fileManager.pageXOfY", { + current: pageNumber, + total: numPages || 0, + })} + + +
    +
    + + + {Math.round(pdfScale * 100)}% + + +
    +
    +
    +
    + +
    +
    + {pdfError ? ( +
    + +

    Cannot load PDF

    +

    + There was an error loading this PDF file. +

    + {onDownload && ( + + )} +
    + ) : ( + { + setNumPages(numPages); + setPdfError(false); + + onMediaDimensionsChange?.({ + width: 800, + height: 600, + }); + }} + onLoadError={(error) => { + console.error("PDF load error:", error); + setPdfError(true); + }} + loading={ +
    +
    +

    + Loading PDF... +

    +
    + } + > + +
    +

    + Loading page... +

    +
    + } + /> + + )} +
    +
    +
    + ); +} diff --git a/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx b/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx index eab5965a..b5faa7ab 100644 --- a/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx +++ b/src/ui/desktop/apps/features/file-manager/components/PermissionsDialog.tsx @@ -10,7 +10,6 @@ import { import { Button } from "@/components/ui/button.tsx"; import { Label } from "@/components/ui/label.tsx"; import { Checkbox } from "@/components/ui/checkbox.tsx"; -import { Input } from "@/components/ui/input.tsx"; import { useTranslation } from "react-i18next"; import { Shield } from "lucide-react"; diff --git a/src/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx b/src/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx index 22715dcb..67c8f955 100644 --- a/src/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx +++ b/src/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx @@ -3,6 +3,7 @@ import { GuacamoleDisplay } from "@/ui/desktop/apps/features/guacamole/Guacamole import { FullScreenAppWrapper } from "@/ui/desktop/apps/FullScreenAppWrapper.tsx"; import { getGuacamoleTokenFromHost } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; +import type { SSHHost } from "@/types"; interface GuacamoleAppProps { hostId?: string; @@ -46,7 +47,7 @@ const GuacamoleApp: React.FC = ({ hostId }) => { interface GuacamoleAppInnerProps { hostId: number; - hostConfig: any; + hostConfig: Pick; } const GuacamoleAppInner: React.FC = ({ diff --git a/src/ui/desktop/apps/features/guacamole/GuacamoleDisplay.tsx b/src/ui/desktop/apps/features/guacamole/GuacamoleDisplay.tsx index ca067687..372334f0 100644 --- a/src/ui/desktop/apps/features/guacamole/GuacamoleDisplay.tsx +++ b/src/ui/desktop/apps/features/guacamole/GuacamoleDisplay.tsx @@ -8,7 +8,11 @@ import { } from "react"; import Guacamole from "guacamole-common-js"; import { useTranslation } from "react-i18next"; -import { getCookie, isElectron, isEmbeddedMode } from "@/ui/main-axios.ts"; +import { + getGuacamoleToken, + isElectron, + isEmbeddedMode, +} from "@/ui/main-axios.ts"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet"; @@ -64,7 +68,6 @@ export const GuacamoleDisplay = forwardRef< const windowFocusedRef = useRef( typeof document === "undefined" ? true : document.hasFocus(), ); - const [isConnecting, setIsConnecting] = useState(false); const [isReady, setIsReady] = useState(false); useImperativeHandle(ref, () => ({ @@ -108,45 +111,35 @@ export const GuacamoleDisplay = forwardRef< ): Promise => { try { let token: string; + const protocol = connectionConfig.protocol ?? connectionConfig.type; if (connectionConfig.token) { token = connectionConfig.token; } else { - const jwtToken = getCookie("jwt"); - if (!jwtToken) { - onError?.("Authentication required"); - return null; - } - - const baseUrl = isDev - ? "http://localhost:30001" - : isElectron() - ? (window as { configuredServerUrl?: string }) - .configuredServerUrl || "http://127.0.0.1:30001" - : `${window.location.origin}`; - - const response = await fetch(`${baseUrl}/guacamole/token`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${jwtToken}`, - }, - body: JSON.stringify(connectionConfig), - credentials: "include", + const data = await getGuacamoleToken({ + protocol: protocol ?? "rdp", + hostname: String(connectionConfig.hostname ?? ""), + port: connectionConfig.port, + username: connectionConfig.username, + password: connectionConfig.password, + domain: connectionConfig.domain, + security: + typeof connectionConfig.security === "string" + ? connectionConfig.security + : undefined, + ignoreCert: + typeof connectionConfig.ignoreCert === "boolean" + ? connectionConfig.ignoreCert + : undefined, + guacamoleConfig: connectionConfig.guacamoleConfig as Parameters< + typeof getGuacamoleToken + >[0]["guacamoleConfig"], }); - - if (!response.ok) { - const err = await response.json(); - throw new Error(err.error || "Failed to get connection token"); - } - - const data = await response.json(); token = data.token; } const width = connectionConfig.width ?? containerWidth ?? 1280; const height = connectionConfig.height ?? containerHeight ?? 720; - const protocol = connectionConfig.protocol ?? connectionConfig.type; const dpi = protocol === "rdp" ? (connectionConfig.dpi ?? 96) : null; const wsBase = isDev @@ -281,7 +274,6 @@ export const GuacamoleDisplay = forwardRef< const connect = useCallback(async () => { if (isConnectingRef.current) return; isConnectingRef.current = true; - setIsConnecting(true); setIsReady(false); let containerWidth = containerRef.current?.clientWidth || 0; @@ -295,7 +287,6 @@ export const GuacamoleDisplay = forwardRef< const wsUrl = await getWebSocketUrl(containerWidth, containerHeight); if (!wsUrl) { isConnectingRef.current = false; - setIsConnecting(false); return; } @@ -364,19 +355,16 @@ export const GuacamoleDisplay = forwardRef< case 0: break; case 1: - setIsConnecting(true); break; case 2: break; case 3: - setIsConnecting(false); setIsReady(true); onConnect?.(); break; case 4: break; case 5: - setIsConnecting(false); setIsReady(false); hasKeyboardFocusRef.current = false; refreshKeyboardHandlers(); @@ -387,7 +375,6 @@ export const GuacamoleDisplay = forwardRef< client.onerror = (error: Guacamole.Status) => { const errorMessage = error.message || "Connection error"; - setIsConnecting(false); setIsReady(false); onError?.(errorMessage); }; diff --git a/src/ui/desktop/apps/features/server-stats/ServerStats.tsx b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx index 7ac0062d..c9b082f3 100644 --- a/src/ui/desktop/apps/features/server-stats/ServerStats.tsx +++ b/src/ui/desktop/apps/features/server-stats/ServerStats.tsx @@ -37,18 +37,25 @@ import { FirewallWidget, } from "./widgets"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; -import { RefreshCcw, RefreshCw, RefreshCwOff } from "lucide-react"; +import { RefreshCw } from "lucide-react"; import { ConnectionLogProvider, useConnectionLog, } from "@/ui/desktop/navigation/connection-log/ConnectionLogContext.tsx"; import { ConnectionLog } from "@/ui/desktop/navigation/connection-log/ConnectionLog.tsx"; +import type { LogEntry } from "@/types/connection-log.ts"; interface QuickAction { name: string; snippetId: number; } +type ConnectionLogPayload = Omit; + +type ConnectionLogError = Error & { + connectionLogs?: ConnectionLogPayload[]; +}; + interface HostConfig { id: number; name: string; @@ -62,14 +69,6 @@ interface HostConfig { [key: string]: unknown; } -interface TabData { - id: number; - type: string; - title?: string; - hostConfig?: HostConfig; - [key: string]: unknown; -} - interface ServerProps { hostConfig?: HostConfig; title?: string; @@ -92,9 +91,7 @@ function ServerStatsInner({ clearLogs, isExpanded: isConnectionLogExpanded, } = useConnectionLog(); - const { addTab, tabs, currentTab, removeTab } = useTabs() as { - addTab: (tab: { type: string; [key: string]: unknown }) => number; - tabs: TabData[]; + const { currentTab, removeTab } = useTabs() as { currentTab: number | null; removeTab: (tabId: number) => void; }; @@ -419,7 +416,7 @@ function ServerStatsInner({ if (cancelled) return; if (result?.connectionLogs) { - result.connectionLogs.forEach((log: any) => { + result.connectionLogs.forEach((log) => { addLog({ type: log.type, stage: log.stage, @@ -451,7 +448,7 @@ function ServerStatsInner({ try { data = await getServerMetricsById(currentHostConfig.id); break; - } catch (error: any) { + } catch (error: unknown) { retryCount++; if (retryCount === 1) { const initialDelay = totpVerified ? 3000 : 5000; @@ -492,14 +489,15 @@ function ServerStatsInner({ } } }, statsConfig.metricsInterval * 1000); - } catch (error: any) { + } catch (error: unknown) { if (!cancelled) { + const logError = error as ConnectionLogError; console.error("Failed to start metrics polling:", error); setIsLoadingMetrics(false); setHasConnectionError(true); - if (error?.connectionLogs) { - error.connectionLogs.forEach((log: any) => { + if (logError.connectionLogs) { + logError.connectionLogs.forEach((log) => { addLog({ type: log.type, stage: log.stage, @@ -511,7 +509,10 @@ function ServerStatsInner({ addLog({ type: "error", stage: "connection", - message: error?.message || t("serverStats.connectionFailed"), + message: + error instanceof Error + ? error.message + : t("serverStats.connectionFailed"), }); } } @@ -566,15 +567,6 @@ function ServerStatsInner({ const leftMarginPx = sidebarState === "collapsed" ? 16 : 8; const bottomMarginPx = 8; - const isFileManagerAlreadyOpen = React.useMemo(() => { - if (!currentHostConfig) return false; - return tabs.some( - (tab: TabData) => - tab.type === "file_manager" && - tab.hostConfig?.id === currentHostConfig.id, - ); - }, [tabs, currentHostConfig]); - const wrapperStyle: React.CSSProperties = embedded ? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" } : { @@ -775,7 +767,7 @@ function ServerStatsInner({ }, ); } - } catch (error: any) { + } catch (error: unknown) { toast.error( t("serverStats.quickActionError", { name: action.name, @@ -783,7 +775,9 @@ function ServerStatsInner({ { id: `quick-action-${action.snippetId}`, description: - error?.message || "Unknown error", + error instanceof Error + ? error.message + : "Unknown error", duration: 5000, }, ); diff --git a/src/ui/desktop/apps/features/terminal/Terminal.tsx b/src/ui/desktop/apps/features/terminal/Terminal.tsx index c2219f73..f493e460 100644 --- a/src/ui/desktop/apps/features/terminal/Terminal.tsx +++ b/src/ui/desktop/apps/features/terminal/Terminal.tsx @@ -27,6 +27,7 @@ import { } from "@/ui/main-axios.ts"; import { TOTPDialog } from "@/ui/desktop/navigation/dialogs/TOTPDialog.tsx"; import { SSHAuthDialog } from "@/ui/desktop/navigation/dialogs/SSHAuthDialog.tsx"; +import { PassphraseDialog } from "@/ui/desktop/navigation/dialogs/PassphraseDialog.tsx"; import { WarpgateDialog } from "@/ui/desktop/navigation/dialogs/WarpgateDialog.tsx"; import { OPKSSHDialog } from "@/ui/desktop/navigation/dialogs/OPKSSHDialog.tsx"; import { HostKeyVerificationDialog } from "@/ui/desktop/navigation/dialogs/HostKeyVerificationDialog.tsx"; @@ -40,7 +41,6 @@ import type { TerminalConfig } from "@/types"; import { useTheme } from "@/components/theme-provider.tsx"; import { useCommandTracker } from "@/ui/hooks/useCommandTracker.ts"; import { highlightTerminalOutput } from "@/lib/terminal-syntax-highlighter.ts"; -import { useCommandHistory as useCommandHistoryHook } from "@/ui/hooks/useCommandHistory.ts"; import { useCommandHistory } from "@/ui/desktop/apps/features/terminal/command-history/CommandHistoryContext.tsx"; import { CommandAutocomplete } from "./command-history/CommandAutocomplete.tsx"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; @@ -78,6 +78,11 @@ interface TerminalHandle { refresh: () => void; } +type HostKeyVerificationData = Omit< + React.ComponentProps, + "isOpen" | "scenario" | "onAccept" | "onReject" | "backgroundColor" +>; + interface SSHTerminalProps { hostConfig: HostConfig; isVisible: boolean; @@ -88,112 +93,10 @@ interface SSHTerminalProps { onTitleChange?: (title: string) => void; initialPath?: string; executeCommand?: string; - onOpenFileManager?: () => void; + onOpenFileManager?: (path?: string) => void; previewTheme?: string | null; } -function TerminalContextMenu({ - x, - y, - hasSelection, - showCopyPaste, - showOpenFileManager, - onCopy, - onPaste, - onOpenFileManager, - onClose, -}: { - x: number; - y: number; - hasSelection: boolean; - showCopyPaste: boolean; - showOpenFileManager: boolean; - onCopy: () => void; - onPaste: () => void; - onOpenFileManager: () => void; - onClose: () => void; -}) { - const { t } = useTranslation(); - const menuRef = useRef(null); - - const menuX = x + 180 > window.innerWidth ? window.innerWidth - 190 : x; - const menuY = y + 150 > window.innerHeight ? window.innerHeight - 160 : y; - - useEffect(() => { - let cleanup: (() => void) | null = null; - const timeoutId = setTimeout(() => { - const handleClose = (e: MouseEvent) => { - if (!menuRef.current?.contains(e.target as Element)) onClose(); - }; - const handleRightClick = (e: MouseEvent) => { - e.preventDefault(); - onClose(); - }; - const handleKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - document.addEventListener("mousedown", handleClose, true); - document.addEventListener("contextmenu", handleRightClick); - document.addEventListener("keydown", handleKey); - window.addEventListener("blur", onClose); - - cleanup = () => { - document.removeEventListener("mousedown", handleClose, true); - document.removeEventListener("contextmenu", handleRightClick); - document.removeEventListener("keydown", handleKey); - window.removeEventListener("blur", onClose); - }; - }, 50); - return () => { - clearTimeout(timeoutId); - cleanup?.(); - }; - }, [onClose]); - - const items: { label: string; action: () => void; disabled?: boolean }[] = []; - - if (showCopyPaste) { - items.push( - { label: t("terminal.copy"), action: onCopy, disabled: !hasSelection }, - { label: t("terminal.paste"), action: onPaste }, - ); - } - - if (showOpenFileManager) { - items.push({ - label: t("terminal.openFileManagerHere"), - action: onOpenFileManager, - }); - } - - return ( - <> -
    -
    - {items.map((item, i) => ( - - ))} -
    - - ); -} - const TerminalInner = forwardRef( function SSHTerminal( { @@ -201,7 +104,6 @@ const TerminalInner = forwardRef( isVisible, splitScreen = false, onClose, - onTitleChange, initialPath, executeCommand, onOpenFileManager, @@ -209,16 +111,6 @@ const TerminalInner = forwardRef( }, ref, ) { - if ( - typeof window !== "undefined" && - !(window as { testJWT?: () => string | null }).testJWT - ) { - (window as { testJWT?: () => string | null }).testJWT = () => { - const jwt = getCookie("jwt"); - return jwt; - }; - } - const { t } = useTranslation(); const { instance: terminal, ref: xtermRef } = useXTerm(); const commandHistoryContext = useCommandHistory(); @@ -288,6 +180,7 @@ const TerminalInner = forwardRef( const [authDialogReason, setAuthDialogReason] = useState< "no_keyboard" | "auth_failed" | "timeout" >("no_keyboard"); + const [showPassphraseDialog, setShowPassphraseDialog] = useState(false); const [keyboardInteractiveDetected, setKeyboardInteractiveDetected] = useState(false); const [warpgateAuthRequired, setWarpgateAuthRequired] = useState(false); @@ -305,19 +198,14 @@ const TerminalInner = forwardRef( } | null>(null); const opksshTimeoutRef = useRef(null); - const [contextMenu, setContextMenu] = useState<{ - x: number; - y: number; - hasSelection: boolean; - } | null>(null); const opksshFailedRef = useRef(false); const currentHostIdRef = useRef(null); - const currentHostConfigRef = useRef(null); + const currentHostConfigRef = useRef(null); const [hostKeyVerification, setHostKeyVerification] = useState<{ isOpen: boolean; scenario: "new" | "changed"; - data: any; + data: HostKeyVerificationData; } | null>(null); const sessionIdRef = useRef(null); @@ -344,6 +232,7 @@ const TerminalInner = forwardRef( const isReconnectingRef = useRef(false); const isConnectingRef = useRef(false); const wasConnectedRef = useRef(false); + const closeAfterDisconnectRef = useRef(false); useEffect(() => { isUnmountingRef.current = false; @@ -353,6 +242,7 @@ const TerminalInner = forwardRef( reconnectAttempts.current = 0; wasConnectedRef.current = false; isAttachingSessionRef.current = false; + closeAfterDisconnectRef.current = false; return () => {}; }, [hostConfig.id]); @@ -360,7 +250,6 @@ const TerminalInner = forwardRef( const totpTimeoutRef = useRef(null); const connectionTimeoutRef = useRef(null); const activityLoggedRef = useRef(false); - const keyHandlerAttachedRef = useRef(false); const [commandHistoryTrackingEnabled, setCommandHistoryTrackingEnabled] = useState( () => localStorage.getItem("commandHistoryTracking") === "true", @@ -425,9 +314,9 @@ const TerminalInner = forwardRef( const autocompleteSuggestionsRef = useRef([]); const autocompleteSelectedIndexRef = useRef(0); - const [showHistoryDialog, setShowHistoryDialog] = useState(false); - const [commandHistory, setCommandHistory] = useState([]); - const [isLoadingHistory, setIsLoadingHistory] = useState(false); + const [showHistoryDialog] = useState(false); + const [, setCommandHistory] = useState([]); + const [, setIsLoadingHistory] = useState(false); const setIsLoadingRef = useRef(commandHistoryContext.setIsLoading); const setCommandHistoryContextRef = useRef( @@ -535,12 +424,9 @@ const TerminalInner = forwardRef( useEffect(() => { const checkAuth = () => { - const jwtToken = getCookie("jwt"); - const isAuth = !!(jwtToken && jwtToken.trim() !== ""); - setIsAuthenticated((prev) => { - if (prev !== isAuth) { - return isAuth; + if (!prev) { + return true; } return prev; }); @@ -703,6 +589,32 @@ const TerminalInner = forwardRef( if (onClose) onClose(); } + function handlePassphraseSubmit(passphrase: string) { + if (webSocketRef.current && terminal) { + webSocketRef.current.send( + JSON.stringify({ + type: "reconnect_with_credentials", + data: { + cols: terminal.cols, + rows: terminal.rows, + keyPassword: passphrase, + hostConfig: { + ...hostConfig, + keyPassword: passphrase, + }, + }, + }), + ); + setShowPassphraseDialog(false); + setIsConnecting(true); + } + } + + function handlePassphraseCancel() { + setShowPassphraseDialog(false); + if (onClose) onClose(); + } + function scheduleNotify(cols: number, rows: number) { if (!(cols > 0 && rows > 0)) return; pendingSizeRef.current = { cols, rows }; @@ -802,12 +714,19 @@ const TerminalInner = forwardRef( } }, refresh: () => hardRefresh(), + openFileManager: () => { + if (webSocketRef.current?.readyState === WebSocket.OPEN) { + webSocketRef.current.send(JSON.stringify({ type: "get_cwd" })); + } else { + onOpenFileManager?.("/"); + } + }, }), [terminal], ); function getUseRightClickCopyPaste() { - return getCookie("rightClickCopyPaste") === "true"; + return getCookie("rightClickCopyPaste") !== "false"; } function attemptReconnection() { @@ -873,21 +792,6 @@ const TerminalInner = forwardRef( return; } - const jwtToken = getCookie("jwt"); - if (!jwtToken || jwtToken.trim() === "") { - console.warn("Reconnection cancelled - no authentication token"); - isReconnectingRef.current = false; - updateConnectionError(t("terminal.authenticationRequired")); - setIsConnecting(false); - shouldNotReconnectRef.current = true; - addLog({ - type: "error", - stage: "auth", - message: t("terminal.authenticationRequired"), - }); - return; - } - if (terminal && hostConfig) { if (!isAttachingSessionRef.current) { terminal.clear(); @@ -922,17 +826,6 @@ const TerminalInner = forwardRef( 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); - updateConnectionError("Authentication required"); - isConnectingRef.current = false; - return; - } - let baseWsUrl: string; if (isDev) { @@ -1053,7 +946,7 @@ const TerminalInner = forwardRef( const savedSessionId = persistenceEnabled ? localStorage.getItem(`termix_session_${tabId}`) : null; - if (savedSessionId && !isReconnectingRef.current) { + if (savedSessionId) { sessionIdRef.current = savedSessionId; isAttachingSessionRef.current = true; @@ -1304,13 +1197,27 @@ const TerminalInner = forwardRef( ); } }, 100); + } else if (msg.type === "session_ended") { + wasDisconnectedBySSH.current = true; + setIsConnected(false); + setIsConnecting(false); + shouldNotReconnectRef.current = true; + if (onClose) { + onClose(); + } } else if (msg.type === "disconnected") { wasDisconnectedBySSH.current = true; + shouldNotReconnectRef.current = true; setIsConnected(false); setIsConnecting(false); if (wasConnectedRef.current) { wasConnectedRef.current = false; - setShowDisconnectedOverlay(true); + setShowDisconnectedOverlay(false); + if (onClose && !closeAfterDisconnectRef.current) { + closeAfterDisconnectRef.current = true; + isUnmountingRef.current = true; + window.setTimeout(onClose, 0); + } } else if (!connectionErrorRef.current) { updateConnectionError( msg.message || t("terminal.connectionRejected"), @@ -1334,6 +1241,7 @@ const TerminalInner = forwardRef( } }, 180000); } else if (msg.type === "totp_retry") { + // Existing prompt remains visible while the backend asks for another code. } else if (msg.type === "password_required") { setTotpRequired(true); setTotpPrompt(msg.prompt || t("common.password")); @@ -1502,6 +1410,15 @@ const TerminalInner = forwardRef( clearTimeout(connectionTimeoutRef.current); connectionTimeoutRef.current = null; } + } else if (msg.type === "cwd") { + onOpenFileManager?.(msg.path as string); + } else if (msg.type === "passphrase_required") { + setShowPassphraseDialog(true); + setIsConnecting(false); + if (connectionTimeoutRef.current) { + clearTimeout(connectionTimeoutRef.current); + connectionTimeoutRef.current = null; + } } else if (msg.type === "host_key_verification_required") { setHostKeyVerification({ isOpen: true, @@ -1691,8 +1608,6 @@ const TerminalInner = forwardRef( setIsConnecting(false); shouldNotReconnectRef.current = true; - localStorage.removeItem("jwt"); - return; } @@ -1752,6 +1667,10 @@ const TerminalInner = forwardRef( async function writeTextToClipboard(text: string): Promise { try { + if (window.electronClipboard) { + await window.electronClipboard.writeText(text); + return true; + } if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return true; @@ -1778,6 +1697,9 @@ const TerminalInner = forwardRef( async function readTextFromClipboard(): Promise { try { + if (window.electronClipboard) { + return window.electronClipboard.readText(); + } if (navigator.clipboard && navigator.clipboard.readText) { return await navigator.clipboard.readText(); } @@ -1870,7 +1792,7 @@ const TerminalInner = forwardRef( const config = { ...DEFAULT_TERMINAL_CONFIG, - ...(hostConfig.terminalConfig as any), + ...hostConfig.terminalConfig, }; let themeColors; @@ -1954,7 +1876,7 @@ const TerminalInner = forwardRef( const config = { ...DEFAULT_TERMINAL_CONFIG, - ...(hostConfig.terminalConfig as any), + ...hostConfig.terminalConfig, }; const fontConfig = TERMINAL_FONTS.find( @@ -2049,29 +1971,26 @@ const TerminalInner = forwardRef( const element = xtermRef.current; const handleContextMenu = (e: MouseEvent) => { - if (getUseRightClickCopyPaste()) { + if (e.ctrlKey && onOpenFileManager) { + e.preventDefault(); + e.stopPropagation(); + onOpenFileManager(); + return; + } + + if (isElectron() && getUseRightClickCopyPaste()) { e.preventDefault(); e.stopPropagation(); if (terminal.hasSelection()) { const text = terminal.getSelection(); - navigator.clipboard - .writeText(text) - .then(() => terminal.clearSelection()); + writeTextToClipboard(text).then(() => terminal.clearSelection()); } else { - navigator.clipboard.readText().then((text) => { + readTextFromClipboard().then((text) => { if (text) terminal.paste(text); }); } return; } - if (!onOpenFileManager) return; - e.preventDefault(); - e.stopPropagation(); - setContextMenu({ - x: e.clientX, - y: e.clientY, - hasSelection: terminal.hasSelection(), - }); }; element?.addEventListener("contextmenu", handleContextMenu); @@ -2111,7 +2030,7 @@ const TerminalInner = forwardRef( const config = { ...DEFAULT_TERMINAL_CONFIG, - ...(hostConfig.terminalConfig as any), + ...hostConfig.terminalConfig, }; if (config.backspaceMode !== "control-h") return; @@ -2236,8 +2155,7 @@ const TerminalInner = forwardRef( } if ( - ((e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey) || - (e.metaKey && !e.ctrlKey && !e.altKey) || + ((e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey) || (e.ctrlKey && !e.shiftKey && !e.altKey && @@ -2629,6 +2547,19 @@ const TerminalInner = forwardRef( backgroundColor={backgroundColor} /> + + ( position={autocompletePosition} onSelect={handleAutocompleteSelect} /> - - {contextMenu && ( - { - const selection = terminal?.getSelection(); - if (selection) { - await writeTextToClipboard(selection); - terminal?.clearSelection(); - } - }} - onPaste={async () => { - const text = await readTextFromClipboard(); - if (text) terminal?.paste(text); - }} - onOpenFileManager={() => onOpenFileManager?.()} - onClose={() => setContextMenu(null)} - /> - )}
    ); }, diff --git a/src/ui/desktop/apps/features/terminal/TerminalPreview.tsx b/src/ui/desktop/apps/features/terminal/TerminalPreview.tsx index 972cadbe..9559e0e8 100644 --- a/src/ui/desktop/apps/features/terminal/TerminalPreview.tsx +++ b/src/ui/desktop/apps/features/terminal/TerminalPreview.tsx @@ -1,4 +1,3 @@ -import type { TerminalTheme } from "@/constants/terminal-themes.ts"; import { TERMINAL_THEMES, TERMINAL_FONTS, diff --git a/src/ui/desktop/apps/features/tunnel/Tunnel.tsx b/src/ui/desktop/apps/features/tunnel/Tunnel.tsx index 32a45ee0..0de2bd7a 100644 --- a/src/ui/desktop/apps/features/tunnel/Tunnel.tsx +++ b/src/ui/desktop/apps/features/tunnel/Tunnel.tsx @@ -1,9 +1,8 @@ import React, { useState, useEffect, useCallback } from "react"; -import { useTranslation } from "react-i18next"; import { TunnelViewer } from "@/ui/desktop/apps/features/tunnel/TunnelViewer.tsx"; import { getSSHHosts, - getTunnelStatuses, + subscribeTunnelStatuses, connectTunnel, disconnectTunnel, cancelTunnel, @@ -17,7 +16,6 @@ import type { } from "../../../types/index.js"; export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { - const { t } = useTranslation(); const [allHosts, setAllHosts] = useState([]); const [visibleHosts, setVisibleHosts] = useState([]); const [tunnelStatuses, setTunnelStatuses] = useState< @@ -110,11 +108,6 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { } }; - const fetchTunnelStatuses = useCallback(async () => { - const statusData = await getTunnelStatuses(); - setTunnelStatuses(statusData); - }, []); - useEffect(() => { fetchHosts(); const interval = setInterval(fetchHosts, 5000); @@ -137,10 +130,10 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { }, [fetchHosts]); useEffect(() => { - fetchTunnelStatuses(); - const interval = setInterval(fetchTunnelStatuses, 1000); - return () => clearInterval(interval); - }, [fetchTunnelStatuses]); + return subscribeTunnelStatuses(setTunnelStatuses, () => { + // The view remains usable if the stream reconnects or is unavailable. + }); + }, []); useEffect(() => { if (visibleHosts.length > 0 && visibleHosts[0]) { @@ -168,6 +161,15 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { const tunnelConfig = { name: tunnelName, + scope: tunnel.scope || "s2s", + mode: tunnel.mode || tunnel.tunnelType || "remote", + bindHost: tunnel.bindHost, + targetHost: tunnel.targetHost, + tunnelType: + tunnel.tunnelType || + (tunnel.mode === "local" || tunnel.mode === "remote" + ? tunnel.mode + : "remote"), sourceHostId: host.id, tunnelIndex: tunnelIndex, hostName: host.name || `${host.username}@${host.ip}`, @@ -221,8 +223,6 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { } else if (action === "cancel") { await cancelTunnel(tunnelName); } - - await fetchTunnelStatuses(); } catch (error) { console.error("Tunnel action failed:", { action, diff --git a/src/ui/desktop/apps/features/tunnel/TunnelInlineControls.tsx b/src/ui/desktop/apps/features/tunnel/TunnelInlineControls.tsx new file mode 100644 index 00000000..14b503f9 --- /dev/null +++ b/src/ui/desktop/apps/features/tunnel/TunnelInlineControls.tsx @@ -0,0 +1,156 @@ +import { Button } from "@/components/ui/button.tsx"; +import type { TunnelStatus } from "@/types/index.js"; +import { + AlertCircle, + Loader2, + Play, + Square, + Wifi, + WifiOff, +} from "lucide-react"; +import { useTranslation } from "react-i18next"; + +type TunnelInlineControlsProps = { + status?: TunnelStatus; + loading?: boolean; + onStart?: () => void; + onStop?: () => void; + startDisabled?: boolean; + startDisabledReason?: string; +}; + +function getStatusKind(status?: TunnelStatus) { + const value = status?.status?.toUpperCase() || "DISCONNECTED"; + + if (value === "CONNECTED") return "connected"; + if (value === "ERROR" || value === "FAILED") return "error"; + if ( + value === "CONNECTING" || + value === "DISCONNECTING" || + value === "RETRYING" || + value === "WAITING" + ) { + return "connecting"; + } + + return "disconnected"; +} + +function getStatusTitle( + status: TunnelStatus | undefined, + statusText: string, + t: ReturnType["t"], +) { + if (!status) return statusText; + + const details = []; + if (status.reason) details.push(status.reason); + if (status.retryCount && status.maxRetries) { + details.push( + t("tunnels.attempt", { + current: status.retryCount, + max: status.maxRetries, + }), + ); + } + if (status.nextRetryIn) { + details.push( + t("tunnels.nextRetryIn", { + seconds: status.nextRetryIn, + }), + ); + } + if (status.errorType && !status.reason) details.push(status.errorType); + + return details.length > 0 ? details.join("\n") : statusText; +} + +export function TunnelInlineControls({ + status, + loading = false, + onStart, + onStop, + startDisabled, + startDisabledReason, +}: TunnelInlineControlsProps) { + const { t } = useTranslation(); + const kind = getStatusKind(status); + const isDisconnected = kind === "disconnected"; + const statusText = + kind === "connected" + ? t("tunnels.connected") + : kind === "connecting" + ? t("tunnels.connecting") + : kind === "error" + ? t("tunnels.error") + : t("tunnels.disconnected"); + const title = getStatusTitle(status, statusText, t); + + const statusClass = + kind === "connected" + ? "text-green-600 dark:text-green-400 bg-green-500/10 border-green-500/20" + : kind === "connecting" + ? "text-blue-600 dark:text-blue-400 bg-blue-500/10 border-blue-500/20" + : kind === "error" + ? "text-red-600 dark:text-red-400 bg-red-500/10 border-red-500/20" + : "text-muted-foreground bg-muted/30 border-border"; + + const statusIcon = + kind === "connected" ? ( + + ) : kind === "connecting" ? ( + + ) : kind === "error" ? ( + + ) : ( + + ); + + return ( +
    + + {statusIcon} + {statusText} + + {loading ? ( + + ) : isDisconnected ? ( + + ) : ( + + )} +
    + ); +} diff --git a/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx b/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx index ef0c7eb8..bbde6bae 100644 --- a/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx +++ b/src/ui/desktop/apps/features/tunnel/TunnelManager.tsx @@ -1,9 +1,11 @@ import React from "react"; import { useSidebar } from "@/components/ui/sidebar.tsx"; import { Separator } from "@/components/ui/separator.tsx"; +import { Button } from "@/components/ui/button.tsx"; import { Tunnel } from "@/ui/desktop/apps/features/tunnel/Tunnel.tsx"; import { useTranslation } from "react-i18next"; import { getSSHHosts } from "@/ui/main-axios.ts"; +import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; interface HostConfig { id: number; @@ -33,7 +35,29 @@ export function TunnelManager({ }: TunnelManagerProps): React.ReactElement { const { t } = useTranslation(); const { state: sidebarState } = useSidebar(); + const { tabs, addTab, setCurrentTab, updateTab } = useTabs(); const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); + const isElectron = + typeof window !== "undefined" && window.electronAPI?.isElectron === true; + + const openC2SPresets = React.useCallback(() => { + const profileTab = tabs.find((tab) => tab.type === "user_profile"); + if (profileTab) { + updateTab(profileTab.id, { + initialTab: "c2s-tunnels", + _updateTimestamp: Date.now(), + }); + setCurrentTab(profileTab.id); + return; + } + + const id = addTab({ + type: "user_profile", + title: t("profile.title"), + initialTab: "c2s-tunnels", + }); + setCurrentTab(id); + }, [addTab, setCurrentTab, t, tabs, updateTab]); React.useEffect(() => { if (hostConfig?.id !== currentHostConfig?.id) { @@ -107,6 +131,11 @@ export function TunnelManager({
    + {isElectron && ( + + )}
    @@ -127,10 +156,10 @@ export function TunnelManager({

    - {t("tunnel.noTunnelsConfigured")} + {t("tunnels.noTunnelsConfigured")}

    - {t("tunnel.configureTunnelsInHostSettings")} + {t("tunnels.configureTunnelsInHostSettings")}

    diff --git a/src/ui/desktop/apps/features/tunnel/TunnelModeSelector.tsx b/src/ui/desktop/apps/features/tunnel/TunnelModeSelector.tsx new file mode 100644 index 00000000..44c31f16 --- /dev/null +++ b/src/ui/desktop/apps/features/tunnel/TunnelModeSelector.tsx @@ -0,0 +1,72 @@ +import { useTranslation } from "react-i18next"; +import type { TunnelMode } from "@/types/index.js"; + +type TunnelModeSelectorProps = { + mode: TunnelMode; + scope: "client" | "server"; + onChange: (mode: TunnelMode) => void; +}; + +export function TunnelModeSelector({ + mode, + scope, + onChange, +}: TunnelModeSelectorProps) { + const { t } = useTranslation(); + + const options: Array<{ + value: TunnelMode; + label: string; + description: string; + }> = [ + { + value: "local", + label: t("tunnels.typeLocal"), + description: + scope === "client" + ? t("tunnels.typeClientLocalDesc") + : t("tunnels.typeServerLocalDesc"), + }, + { + value: "remote", + label: t("tunnels.typeRemote"), + description: + scope === "client" + ? t("tunnels.typeClientRemoteDesc") + : t("tunnels.typeServerRemoteDesc"), + }, + { + value: "dynamic", + label: t("tunnels.typeDynamic"), + description: + scope === "client" + ? t("tunnels.typeClientDynamicDesc") + : t("tunnels.typeDynamicDesc"), + }, + ]; + + return ( +
    + {options.map((option) => ( + + ))} +
    + ); +} diff --git a/src/ui/desktop/apps/features/tunnel/TunnelObject.tsx b/src/ui/desktop/apps/features/tunnel/TunnelObject.tsx index 979bb040..65bd25dd 100644 --- a/src/ui/desktop/apps/features/tunnel/TunnelObject.tsx +++ b/src/ui/desktop/apps/features/tunnel/TunnelObject.tsx @@ -172,7 +172,7 @@ export function TunnelObject({ className="h-7 px-2 text-red-600 dark:text-red-400 border-red-500/30 dark:border-red-400/30 hover:bg-red-500/10 dark:hover:bg-red-400/10 hover:border-red-500/50 dark:hover:border-red-400/50 text-xs" > - {t("tunnels.disconnect")} + {t("tunnels.stop")} ) : isRetrying || isWaiting ? ( @@ -198,7 +198,7 @@ export function TunnelObject({ className="h-7 px-2 text-green-600 dark:text-green-400 border-green-500/30 dark:border-green-400/30 hover:bg-green-500/10 dark:hover:bg-green-400/10 hover:border-green-500/50 dark:hover:border-green-400/50 text-xs" > - {t("tunnels.connect")} + {t("tunnels.start")} )}
    @@ -406,7 +406,7 @@ export function TunnelObject({ className="h-7 px-2 text-red-600 dark:text-red-400 border-red-500/30 dark:border-red-400/30 hover:bg-red-500/10 dark:hover:bg-red-400/10 hover:border-red-500/50 dark:hover:border-red-400/50 text-xs" > - {t("tunnels.disconnect")} + {t("tunnels.stop")} ) : isRetrying || isWaiting ? ( @@ -432,7 +432,7 @@ export function TunnelObject({ className="h-7 px-2 text-green-600 dark:text-green-400 border-green-500/30 dark:border-green-400/30 hover:bg-green-500/10 dark:hover:bg-green-400/10 hover:border-green-500/50 dark:hover:border-green-400/50 text-xs" > - {t("tunnels.connect")} + {t("tunnels.start")} )}
    diff --git a/src/ui/desktop/apps/features/tunnel/tunnel-form-utils.ts b/src/ui/desktop/apps/features/tunnel/tunnel-form-utils.ts new file mode 100644 index 00000000..1a7cbda1 --- /dev/null +++ b/src/ui/desktop/apps/features/tunnel/tunnel-form-utils.ts @@ -0,0 +1,62 @@ +import type { TunnelMode } from "@/types/index.js"; + +type Translate = ( + key: string, + options?: Record, +) => string; + +export function getTunnelTypeForMode(mode: TunnelMode): "local" | "remote" { + return mode === "remote" ? "remote" : "local"; +} + +export function getTunnelPortLabels( + scope: "client" | "server", + mode: TunnelMode, + t: Translate, +) { + if (scope === "client") { + return { + sourcePortLabel: + mode === "remote" ? t("tunnels.remotePort") : t("tunnels.localPort"), + endpointPortLabel: + mode === "remote" ? t("tunnels.localPort") : t("tunnels.remotePort"), + }; + } + + return { + sourcePortLabel: t("tunnels.currentHostPort"), + endpointPortLabel: t("tunnels.endpointPort"), + }; +} + +export function getTunnelModeDescription( + scope: "client" | "server", + mode: TunnelMode, + ports: { + sourcePort: string | number; + endpointPort: string | number; + }, + t: Translate, +) { + if (scope === "client") { + if (mode === "dynamic") { + return t("tunnels.forwardDescriptionClientDynamic", { + sourcePort: ports.sourcePort, + }); + } + if (mode === "local") { + return t("tunnels.forwardDescriptionClientLocal", ports); + } + return t("tunnels.forwardDescriptionClientRemote", ports); + } + + if (mode === "dynamic") { + return t("tunnels.forwardDescriptionServerDynamic", { + sourcePort: ports.sourcePort, + }); + } + if (mode === "local") { + return t("tunnels.forwardDescriptionServerLocal", ports); + } + return t("tunnels.forwardDescriptionServerRemote", ports); +} diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx index 7b3ddc10..a9034908 100644 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialEditor.tsx @@ -95,6 +95,7 @@ export function CredentialEditor({ setFolders(uniqueFolders); } catch { + // Keep the editor usable even if credentials cannot be loaded. } finally { setLoading(false); } diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx index df98bf6e..fab27109 100644 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx @@ -92,15 +92,6 @@ export function CredentialSelector({ setSearchQuery(""); }; - const handleClear = () => { - onValueChange(null); - if (onCredentialSelect) { - onCredentialSelect(null); - } - setDropdownOpen(false); - setSearchQuery(""); - }; - return ( {t("hosts.selectCredential")} diff --git a/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx b/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx index 6801fd5a..f240e445 100644 --- a/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/CredentialsManager.tsx @@ -48,6 +48,7 @@ import { Terminal, Copy, Plus, + RefreshCw, } from "lucide-react"; import { cn } from "@/lib/utils.ts"; import { @@ -97,6 +98,7 @@ export function CredentialsManager({ >([]); const [selectedHostId, setSelectedHostId] = useState(""); const [deployLoading, setDeployLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); const [hostComboboxOpen, setHostComboboxOpen] = useState(false); const [showCopyCommandDialog, setShowCopyCommandDialog] = useState(false); const [copyCommandCredential, setCopyCommandCredential] = @@ -124,16 +126,29 @@ export function CredentialsManager({ } }; - const fetchCredentials = async () => { + const fetchCredentials = async (options: { showLoading?: boolean } = {}) => { + const { showLoading = true } = options; try { - setLoading(true); + if (showLoading) setLoading(true); const data = await getCredentials(); setCredentials(data); setError(null); } catch { setError(t("credentials.failedToFetchCredentials")); } finally { - setLoading(false); + if (showLoading) setLoading(false); + } + }; + + const handleRefreshCredentials = async () => { + setRefreshing(true); + try { + await Promise.all([ + fetchCredentials({ showLoading: false }), + fetchHosts(), + ]); + } finally { + setRefreshing(false); } }; @@ -471,7 +486,15 @@ export function CredentialsManager({

    -
    @@ -523,7 +546,15 @@ export function CredentialsManager({

    -
    diff --git a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx index 1a75a48c..60833b3d 100644 --- a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx +++ b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx @@ -12,7 +12,6 @@ import type { CredentialGeneralTabProps } from "./shared/tab-types"; export function CredentialGeneralTab({ form, - folders, tagInput, setTagInput, folderDropdownOpen, diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx index 03dfe06a..0545f8b6 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManager.tsx @@ -144,9 +144,21 @@ export function HostManager({ lastProcessedHostIdRef.current = undefined; }; - const handleFormSubmit = () => { + const handleFormSubmit = (savedHost?: SSHHost) => { ignoreNextHostConfigChangeRef.current = true; - const savedHostId = editingHost?.id; + const isUpdatingHost = Boolean(editingHost?.id && savedHost?.id); + + if (isUpdatingHost) { + setEditingHost((current) => ({ ...current, ...savedHost }) as SSHHost); + setIsAddingHost(false); + lastProcessedHostIdRef.current = savedHost.id; + if (updateTab && currentTabId !== undefined) { + updateTab(currentTabId, { hostConfig: savedHost }); + } + return; + } + + const savedHostId = savedHost?.id || editingHost?.id; setEditingHost(null); setIsAddingHost(false); setTimeout(() => { diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx index 333a9582..caa159eb 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerEditor.tsx @@ -1,29 +1,15 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { Controller, useForm } from "react-hook-form"; +import { useForm, type Resolver } from "react-hook-form"; import { z } from "zod"; -import { cn } from "@/lib/utils.ts"; 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 { Badge } from "@/components/ui/badge.tsx"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table.tsx"; -import { Textarea } from "@/components/ui/textarea.tsx"; + import { ScrollArea } from "@/components/ui/scroll-area.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { @@ -33,10 +19,8 @@ import { TabsTrigger, } from "@/components/ui/tabs.tsx"; import React, { useEffect, useRef, useState } from "react"; -import { Switch } from "@/components/ui/switch.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; import { toast } from "sonner"; -import { useConfirmation } from "@/hooks/use-confirmation.ts"; import { createSSHHost, getCredentials, @@ -45,86 +29,18 @@ import { enableAutoStart, disableAutoStart, getSnippets, - getRoles, - getUserList, - getUserInfo, - shareHost, - getHostAccess, - revokeHostAccess, - getSSHHostById, - notifyHostCreatedOrUpdated, getGuacamoleSettings, - type Role, - type AccessRecord, } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; -import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx"; -import CodeMirror from "@uiw/react-codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; import { githubLight } from "@uiw/codemirror-theme-github"; -import { EditorView } from "@codemirror/view"; import { useTheme } from "@/components/theme-provider.tsx"; import type { StatsConfig } from "@/types/stats-widgets.ts"; import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts"; -import { Checkbox } from "@/components/ui/checkbox.tsx"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select.tsx"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, -} from "@/components/ui/command.tsx"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover.tsx"; -import { Slider } from "@/components/ui/slider.tsx"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion.tsx"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog.tsx"; -import { - TERMINAL_THEMES, - TERMINAL_FONTS, - CURSOR_STYLES, - BELL_STYLES, - FAST_SCROLL_MODIFIERS, - DEFAULT_TERMINAL_CONFIG, -} from "@/constants/terminal-themes.ts"; -import { TerminalPreview } from "@/ui/desktop/apps/features/terminal/TerminalPreview.tsx"; -import type { TerminalConfig, SSHHost, Credential } from "@/types"; -import { - Plus, - X, - Check, - ChevronsUpDown, - Save, - AlertCircle, - Trash2, - Users, - Shield, - Clock, - UserCircle, - ArrowLeft, -} from "lucide-react"; + +import { DEFAULT_TERMINAL_CONFIG } from "@/constants/terminal-themes.ts"; +import type { SSHHost, SSHHostData, Credential } from "@/types"; +import { ArrowLeft } from "lucide-react"; import { HostGeneralTab } from "./tabs/HostGeneralTab"; import { HostTerminalTab } from "./tabs/HostTerminalTab"; import { HostDockerTab } from "./tabs/HostDockerTab"; @@ -136,18 +52,24 @@ import { HostSharingTab } from "./tabs/HostSharingTab"; import { HostRemoteDesktopTab } from "./tabs/HostRemoteDesktopTab"; import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; -interface User { - id: string; - username: string; - is_admin: boolean; -} - interface SSHManagerHostEditorProps { editingHost?: SSHHost | null; onFormSubmit?: (updatedHost?: SSHHost) => void; onBack?: () => void; } +function isValidIPv4(value: string) { + const parts = value.split("."); + return ( + parts.length === 4 && + parts.every((part) => { + if (!/^\d+$/.test(part)) return false; + const parsed = Number(part); + return parsed >= 0 && parsed <= 255; + }) + ); +} + export function HostManagerEditor({ editingHost, onFormSubmit, @@ -162,7 +84,6 @@ export function HostManagerEditor({ window.matchMedia("(prefers-color-scheme: dark)").matches); const editorTheme = isDarkMode ? oneDark : githubLight; const [folders, setFolders] = useState([]); - const [sshConfigurations, setSshConfigurations] = useState([]); const [hosts, setHosts] = useState([]); const [credentials, setCredentials] = useState([]); const [snippets, setSnippets] = useState< @@ -220,16 +141,7 @@ export function HostManagerEditor({ ), ].sort(); - const uniqueConfigurations = [ - ...new Set( - hostsData - .filter((host) => host.name && host.name.trim() !== "") - .map((host) => host.name), - ), - ].sort(); - setFolders(uniqueFolders); - setSshConfigurations(uniqueConfigurations); } catch (error) { console.error("Host manager operation failed:", error); } @@ -273,6 +185,25 @@ export function HostManagerEditor({ }; }, []); + const tunnelConnectionSchema = z.object({ + scope: z.enum(["s2s", "c2s"]).default("s2s").optional(), + mode: z.enum(["local", "remote", "dynamic"]).default("remote").optional(), + tunnelType: z.enum(["local", "remote"]).default("remote").optional(), + bindHost: z.string().optional(), + sourcePort: z.coerce.number().min(1).max(65535), + endpointPort: z.coerce.number().min(1).max(65535), + endpointHost: z.string().optional(), + targetHost: z.string().optional(), + endpointPassword: z.string().optional(), + endpointKey: z.string().optional(), + endpointKeyPassword: z.string().optional(), + endpointAuthType: z.string().optional(), + endpointKeyType: z.string().optional(), + 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), + }); + const formSchema = z .object({ connectionType: z.enum(["ssh", "rdp", "vnc", "telnet"]).default("ssh"), @@ -304,27 +235,8 @@ export function HostManagerEditor({ .optional(), enableTerminal: z.boolean().default(true), enableTunnel: z.boolean().default(true), - tunnelConnections: z - .array( - z.object({ - tunnelType: z - .enum(["local", "remote"]) - .default("remote") - .optional(), - sourcePort: z.coerce.number().min(1).max(65535), - endpointPort: z.coerce.number().min(1).max(65535), - endpointHost: z.string().min(1), - endpointPassword: z.string().optional(), - endpointKey: z.string().optional(), - endpointKeyPassword: z.string().optional(), - endpointAuthType: z.string().optional(), - endpointKeyType: z.string().optional(), - 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([]), + tunnelConnections: z.array(tunnelConnectionSchema).default([]), + serverTunnels: z.array(tunnelConnectionSchema).default([]), enableFileManager: z.boolean().default(true), defaultPath: z.string().optional(), statsConfig: z @@ -485,6 +397,38 @@ export function HostManagerEditor({ } } + const serverTunnels = data.serverTunnels; + + serverTunnels.forEach((tunnel, index) => { + if ( + (tunnel.scope || "s2s") === "s2s" && + (!tunnel.endpointHost || tunnel.endpointHost.trim() === "") + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t( + "tunnels.endpointSshConfigRequired", + "Endpoint SSH configuration is required", + ), + path: ["serverTunnels", index, "endpointHost"], + }); + } + + const currentHostIp = + tunnel.mode === "remote" ? tunnel.targetHost : tunnel.bindHost; + if (currentHostIp && !isValidIPv4(currentHostIp)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t("tunnels.invalidCurrentHostIp"), + path: [ + "serverTunnels", + index, + tunnel.mode === "remote" ? "targetHost" : "bindHost", + ], + }); + } + }); + if (data.authType === "none") { return; } @@ -496,7 +440,9 @@ export function HostManagerEditor({ if (data.authType === "password") { if ( !data.password || - (typeof data.password === "string" && data.password.trim() === "") + (typeof data.password === "string" && + data.password.trim() === "" && + data.password !== "existing_password") ) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -534,7 +480,7 @@ export function HostManagerEditor({ type FormData = z.infer; const form = useForm({ - resolver: zodResolver(formSchema) as any, + resolver: zodResolver(formSchema) as unknown as Resolver, mode: "all", defaultValues: { connectionType: "ssh" as const, @@ -562,6 +508,7 @@ export function HostManagerEditor({ showServerStatsInSidebar: false, defaultPath: "/", tunnelConnections: [], + serverTunnels: [], jumpHosts: [], quickActions: [], statsConfig: DEFAULT_STATS_CONFIG, @@ -635,7 +582,11 @@ export function HostManagerEditor({ return false; if (authTab === "password") { - if (!watchedFields.password || watchedFields.password.trim() === "") + if ( + !watchedFields.password || + (watchedFields.password !== "existing_password" && + watchedFields.password.trim() === "") + ) return false; } else if (authTab === "key") { if (!watchedFields.key || !watchedFields.keyType) return false; @@ -709,7 +660,7 @@ export function HostManagerEditor({ useEffect(() => { if (editingHost) { const cleanedHost = { ...editingHost }; - if ((cleanedHost as any).connectionType === "ssh") { + if (cleanedHost.connectionType === "ssh") { if (cleanedHost.credentialId && cleanedHost.key) { cleanedHost.key = undefined; cleanedHost.keyPassword = undefined; @@ -752,7 +703,7 @@ export function HostManagerEditor({ parsedStatsConfig = { ...DEFAULT_STATS_CONFIG, ...parsedStatsConfig }; const formData: Partial = { - connectionType: (cleanedHost as any).connectionType || "ssh", + connectionType: cleanedHost.connectionType || "ssh", name: cleanedHost.name || "", ip: cleanedHost.ip || "", port: cleanedHost.port || 22, @@ -779,8 +730,10 @@ export function HostManagerEditor({ enableFileManager: Boolean(cleanedHost.enableFileManager), defaultPath: cleanedHost.defaultPath || "/", tunnelConnections: Array.isArray(cleanedHost.tunnelConnections) - ? cleanedHost.tunnelConnections.map((conn: any) => ({ + ? cleanedHost.tunnelConnections.map((conn) => ({ ...conn, + scope: conn.scope || "s2s", + mode: conn.mode || conn.tunnelType || "remote", tunnelType: conn.tunnelType || "remote", })) : [], @@ -822,11 +775,11 @@ export function HostManagerEditor({ ? cleanedHost.portKnockSequence : [], enableDocker: Boolean(cleanedHost.enableDocker), - domain: (cleanedHost as any).domain || "", - security: (cleanedHost as any).security || "any", - ignoreCert: (cleanedHost as any).ignoreCert ?? true, + domain: cleanedHost.domain || "", + security: cleanedHost.security || "any", + ignoreCert: cleanedHost.ignoreCert ?? true, guacamoleConfig: (() => { - const cfg = (cleanedHost as any).guacamoleConfig; + const cfg = cleanedHost.guacamoleConfig; if (!cfg) return {}; if (typeof cfg === "string") { try { @@ -858,7 +811,8 @@ export function HostManagerEditor({ formData.password = cleanedHost.password; } } else if (defaultAuthType === "password") { - formData.password = cleanedHost.password || ""; + formData.password = + cleanedHost.password || (editingHost.id ? "existing_password" : ""); } else if (defaultAuthType === "key") { formData.key = editingHost.id ? "existing_key" : editingHost.key; formData.keyPassword = cleanedHost.keyPassword || ""; @@ -877,6 +831,10 @@ export function HostManagerEditor({ formData.credentialId = cleanedHost.credentialId; } + const serverTunnels = Array.isArray(formData.tunnelConnections) + ? formData.tunnelConnections.filter((tunnel) => tunnel.scope !== "c2s") + : []; + formData.serverTunnels = serverTunnels; form.reset(formData as FormData); } else { setAuthTab("password"); @@ -901,6 +859,7 @@ export function HostManagerEditor({ enableFileManager: true, defaultPath: "/", tunnelConnections: [], + serverTunnels: [], jumpHosts: [], quickActions: [], statsConfig: DEFAULT_STATS_CONFIG, @@ -942,15 +901,27 @@ export function HostManagerEditor({ data.name = `${data.username}@${data.ip}`; } - const submitData: Partial = { + type SubmitHostData = SSHHostData & { serverTunnels?: unknown }; + const submitData: SubmitHostData = { ...data, }; + delete submitData.serverTunnels; - (submitData as any).connectionType = data.connectionType; - (submitData as any).domain = data.domain; - (submitData as any).security = data.security; - (submitData as any).ignoreCert = data.ignoreCert; - (submitData as any).guacamoleConfig = data.guacamoleConfig; + const serverTunnels = Array.isArray(data.serverTunnels) + ? data.serverTunnels + : []; + const serverTunnelConnections = serverTunnels.map((tunnel) => ({ + ...tunnel, + scope: "s2s" as const, + })); + + submitData.tunnelConnections = serverTunnelConnections; + + submitData.connectionType = data.connectionType; + submitData.domain = data.domain; + submitData.security = data.security; + submitData.ignoreCert = data.ignoreCert; + submitData.guacamoleConfig = data.guacamoleConfig; if (data.connectionType !== "ssh") { submitData.authType = "none"; @@ -960,8 +931,8 @@ export function HostManagerEditor({ submitData.credentialId = undefined; submitData.tunnelConnections = []; submitData.jumpHosts = []; - (submitData as any).useSocks5 = false; - (submitData as any).socks5ProxyChain = []; + submitData.useSocks5 = false; + submitData.socks5ProxyChain = []; submitData.forceKeyboardInteractive = false; submitData.enableTunnel = false; submitData.enableFileManager = false; @@ -996,17 +967,24 @@ export function HostManagerEditor({ } } + if ( + data.authType === "password" && + data.password === "existing_password" + ) { + delete submitData.password; + } + let savedHost; if (editingHost && editingHost.id) { - savedHost = await updateSSHHost(editingHost.id, submitData as any); + savedHost = await updateSSHHost(editingHost.id, submitData); toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name })); } else { - savedHost = await createSSHHost(submitData as any); + savedHost = await createSSHHost(submitData); toast.success(t("hosts.hostAddedSuccessfully", { name: data.name })); } - if (savedHost && savedHost.id && data.tunnelConnections) { - const hasAutoStartTunnels = data.tunnelConnections.some( + if (savedHost && savedHost.id && serverTunnelConnections) { + const hasAutoStartTunnels = serverTunnelConnections.some( (tunnel) => tunnel.autoStart, ); @@ -1019,7 +997,7 @@ export function HostManagerEditor({ error, ); toast.warning( - t("hosts.autoStartEnableFailed", { name: data.name }), + t("tunnels.autoStartEnableFailed", { name: data.name }), ); } } else { @@ -1039,10 +1017,6 @@ export function HostManagerEditor({ } window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); - - if (savedHost?.id) { - notifyHostCreatedOrUpdated(savedHost.id); - } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -1098,6 +1072,7 @@ export function HostManagerEditor({ connectionType: "general", enableTunnel: "tunnel", tunnelConnections: "tunnel", + serverTunnels: "tunnel", enableFileManager: "file_manager", defaultPath: "file_manager", statsConfig: "statistics", @@ -1207,91 +1182,6 @@ export function HostManagerEditor({ 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 currentHostId = editingHost?.id; - - let filtered = sshConfigurations; - - if (currentHostId) { - const currentHostName = hosts.find((h) => h.id === currentHostId)?.name; - if (currentHostName) { - filtered = sshConfigurations.filter( - (config) => config !== currentHostName, - ); - } - } else { - const currentHostName = - form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`; - 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, { - shouldValidate: true, - shouldDirty: true, - }); - 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 (
    @@ -1549,18 +1435,20 @@ export function HostManagerEditor({
    {!editingHost?.isShared && !isSubmitting && ( - +
    + + {form.formState.isDirty && ( + + {t("common.unsavedChanges")} + + )} +
    )}
    diff --git a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx index 071a77fd..8bff0c75 100644 --- a/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/HostManagerViewer.tsx @@ -80,7 +80,6 @@ import { Globe, FolderOpen, Share2, - Users, ArrowDownUp, Container, Link, @@ -92,6 +91,7 @@ import { Eye, ChevronsDownUp, ChevronsUpDown, + RefreshCw, } from "lucide-react"; import type { SSHHost, @@ -102,6 +102,7 @@ import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts"; import { Checkbox } from "@/components/ui/checkbox.tsx"; import { FolderEditDialog } from "@/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; +import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx"; const INITIAL_HOSTS_PER_FOLDER = 12; @@ -123,6 +124,7 @@ export function HostManagerViewer({ const [editingFolder, setEditingFolder] = useState(null); const [editingFolderName, setEditingFolderName] = useState(""); const [operationLoading, setOperationLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); const [folderMetadata, setFolderMetadata] = useState>( new Map(), ); @@ -138,7 +140,7 @@ export function HostManagerViewer({ new Set(), ); const [bulkUpdating, setBulkUpdating] = useState(false); - const { getStatus } = useServerStatus(); + const { getStatus, refreshStatuses } = useServerStatus(); const dragCounter = useRef(0); useEffect(() => { @@ -165,9 +167,10 @@ export function HostManagerViewer({ }; }, []); - const fetchHosts = async () => { + const fetchHosts = async (options: { showLoading?: boolean } = {}) => { + const { showLoading = true } = options; try { - setLoading(true); + if (showLoading) setLoading(true); const data = await getSSHHosts(); const cleanedHosts = data.map((host) => { @@ -192,7 +195,7 @@ export function HostManagerViewer({ } catch { setError(t("hosts.failedToLoadHosts")); } finally { - setLoading(false); + if (showLoading) setLoading(false); } }; @@ -209,6 +212,20 @@ export function HostManagerViewer({ } }; + const handleRefreshHosts = async () => { + setRefreshing(true); + try { + await Promise.all([ + fetchHosts({ showLoading: false }), + fetchFolderMetadata(), + refreshServerPolling(), + refreshStatuses(), + ]); + } finally { + setRefreshing(false); + } + }; + const handleSaveFolderAppearance = async ( folderName: string, color: string, @@ -408,9 +425,7 @@ export function HostManagerViewer({ }; const selectAllHosts = () => { - const selectableIds = hosts - .filter((h) => !(h as any).isShared) - .map((h) => h.id); + const selectableIds = hosts.filter((h) => !h.isShared).map((h) => h.id); setSelectedHostIds(new Set(selectableIds)); }; @@ -458,7 +473,7 @@ export function HostManagerViewer({ try { document.execCommand("copy"); toast.success(t("hosts.fullScreenUrlCopied")); - } catch (err) { + } catch { toast.error(t("hosts.failedToCopyUrl")); } document.body.removeChild(textArea); @@ -1031,14 +1046,7 @@ export function HostManagerViewer({ ); if (loading) { - return ( -
    -
    -
    -

    {t("hosts.loadingHosts")}

    -
    -
    - ); + return ; } if (error) { @@ -1112,7 +1120,15 @@ export function HostManagerViewer({
    -
    @@ -1221,7 +1237,15 @@ export function HostManagerViewer({
    -
    @@ -1403,7 +1427,7 @@ export function HostManagerViewer({ {selectionMode && (() => { const selectableIds = folderHosts - .filter((h) => !(h as any).isShared) + .filter((h) => !h.isShared) .map((h) => h.id); const allSelected = selectableIds.length > 0 && @@ -1493,15 +1517,12 @@ export function HostManagerViewer({ ? "ring-2 ring-blue-500 border-blue-500" : "border-input hover:border-blue-400/50" } ${ - selectionMode && (host as any).isShared + selectionMode && host.isShared ? "opacity-50 pointer-events-none" : "" }`} onClick={() => { - if ( - selectionMode && - !(host as any).isShared - ) { + if (selectionMode && !host.isShared) { toggleHostSelection(host.id); } else { handleEdit(host); @@ -1511,19 +1532,16 @@ export function HostManagerViewer({
    - {selectionMode && - !(host as any).isShared && ( - - toggleHostSelection(host.id) - } - onClick={(e) => e.stopPropagation()} - className="bg-background border-2 mr-1 flex-shrink-0" - /> - )} + {selectionMode && !host.isShared && ( + + toggleHostSelection(host.id) + } + onClick={(e) => e.stopPropagation()} + className="bg-background border-2 mr-1 flex-shrink-0" + /> + )} {(() => { const statsConfig = (() => { if (!host.statsConfig) { @@ -1536,7 +1554,7 @@ export function HostManagerViewer({ } try { return JSON.parse(host.statsConfig); - } catch (e) { + } catch { return DEFAULT_STATS_CONFIG; } })(); @@ -1568,7 +1586,7 @@ export function HostManagerViewer({ ? `${host.username}@${host.ip}` : host.ip)} - {(host as any).isShared && ( + {host.isShared && (
    - {!(host as any).isShared && + {!host.isShared && host.folder && host.folder !== "" && ( @@ -1635,7 +1653,7 @@ export function HostManagerViewer({

    {t("hosts.editHostTooltip")}

    - {!(host as any).isShared && ( + {!host.isShared && ( <> @@ -1723,8 +1741,8 @@ export function HostManagerViewer({ {(() => { - const connType = (host as any) - .connectionType; + const connType = + host.connectionType; const isRemoteDesktop = connType === "rdp" || connType === "vnc" || @@ -1869,8 +1887,7 @@ export function HostManagerViewer({
    {(() => { - const connType = (host as any) - .connectionType; + const connType = host.connectionType; if (connType === "rdp") { return ( {(() => { - const connType = (host as any) - .connectionType; + const connType = host.connectionType; const isRemoteDesktop = connType === "rdp" || connType === "vnc" || @@ -2267,14 +2283,12 @@ export function HostManagerViewer({ variant="outline" size="sm" onClick={ - selectedHostIds.size === - hosts.filter((h) => !(h as any).isShared).length + selectedHostIds.size === hosts.filter((h) => !h.isShared).length ? deselectAllHosts : selectAllHosts } > - {selectedHostIds.size === - hosts.filter((h) => !(h as any).isShared).length + {selectedHostIds.size === hosts.filter((h) => !h.isShared).length ? t("hosts.deselectAll") : t("hosts.selectAll")} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx index 1f555792..d8a19982 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx @@ -32,23 +32,19 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion.tsx"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx"; +import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx"; import CodeMirror from "@uiw/react-codemirror"; import { EditorView } from "@codemirror/view"; -import { - Plus, - X, - Upload, - AlertCircle, - ArrowRight, - Loader2, -} from "lucide-react"; +import { Plus, X, ArrowRight, Loader2 } from "lucide-react"; import type { HostGeneralTabProps } from "./shared/tab-types"; import { JumpHostItem } from "./shared/JumpHostItem"; import { testProxyConnection } from "@/ui/main-axios"; import { toast } from "sonner"; +import type { ProxyNode } from "@/types"; + +type JumpHostSelection = { hostId: number }; export function HostGeneralTab({ form, @@ -76,7 +72,6 @@ export function HostGeneralTab({ editorTheme, hosts, editingHost, - folders, credentials, t, }: HostGeneralTabProps) { @@ -138,12 +133,12 @@ export function HostGeneralTab({ t("hosts.connectionPath") === "Connection Path" ? "You" : "You", ]; const useSocks5 = form.watch("useSocks5"); - const jumpHosts = form.watch("jumpHosts") || []; + const jumpHosts = (form.watch("jumpHosts") || []) as JumpHostSelection[]; if (useSocks5) { if (proxyMode === "chain") { - const chain = form.watch("socks5ProxyChain") || []; - chain.forEach((node: any, i: number) => { + const chain = (form.watch("socks5ProxyChain") || []) as ProxyNode[]; + chain.forEach((node) => { if (node.host) { const typeLabel = node.type === "http" ? "HTTP" : `SOCKS${node.type}`; @@ -160,8 +155,8 @@ export function HostGeneralTab({ } if (jumpHosts.length > 0 && hosts) { - jumpHosts.forEach((jh: any) => { - const found = hosts.find((h: any) => h.id === jh.hostId); + jumpHosts.forEach((jh) => { + const found = hosts.find((h) => h.id === jh.hostId); if (found) { parts.push(`Jump: ${found.name || found.ip}`); } @@ -177,6 +172,9 @@ export function HostGeneralTab({ return parts; }; + const socks5ProxyChain = (form.watch("socks5ProxyChain") || + []) as ProxyNode[]; + return (
    @@ -224,52 +222,50 @@ export function HostGeneralTab({ )} /> - {connectionType !== "vnc" && ( - { - const isCredentialAuth = authTab === "credential"; - const credentialId = form.watch("credentialId"); - const overrideEnabled = form.watch("overrideCredentialUsername"); - const selectedCredential = credentials.find( - (c) => c.id === credentialId, - ); - const shouldDisable = - isCredentialAuth && - selectedCredential?.username && - !overrideEnabled; + { + const isCredentialAuth = authTab === "credential"; + const credentialId = form.watch("credentialId"); + const overrideEnabled = form.watch("overrideCredentialUsername"); + const selectedCredential = credentials.find( + (c) => c.id === credentialId, + ); + const shouldDisable = + isCredentialAuth && + selectedCredential?.username && + !overrideEnabled; - return ( - - {t("hosts.username")} - - { - field.onChange(e.target.value); - if ( - isCredentialAuth && - selectedCredential && - !selectedCredential.username && - e.target.value.trim() !== "" - ) { - form.setValue("overrideCredentialUsername", true); - } - }} - onBlur={(e) => { - field.onChange(e.target.value.trim()); - field.onBlur(); - }} - /> - - - ); - }} - /> - )} + return ( + + {t("hosts.username")} + + { + field.onChange(e.target.value); + if ( + isCredentialAuth && + selectedCredential && + !selectedCredential.username && + e.target.value.trim() !== "" + ) { + form.setValue("overrideCredentialUsername", true); + } + }} + onBlur={(e) => { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + + ); + }} + />
    - {(form.watch("socks5ProxyChain") || []).length === - 0 && ( + {socks5ProxyChain.length === 0 && (
    {t("hosts.noProxyNodes")}
    )} - {(form.watch("socks5ProxyChain") || []).map( - (node: any, index: number) => ( -
    -
    - - {t("hosts.proxyNode")} {index + 1} - - -
    - -
    -
    - {t("hosts.socks5Host")} - { - const currentChain = - form.watch("socks5ProxyChain") || []; - const newChain = [...currentChain]; - newChain[index] = { - ...newChain[index], - host: e.target.value, - }; - form.setValue( - "socks5ProxyChain", - newChain, - ); - }} - onBlur={(e) => { - const currentChain = - form.watch("socks5ProxyChain") || []; - const newChain = [...currentChain]; - newChain[index] = { - ...newChain[index], - host: e.target.value.trim(), - }; - form.setValue( - "socks5ProxyChain", - newChain, - ); - }} - /> -
    - -
    - {t("hosts.socks5Port")} - { - const currentChain = - form.watch("socks5ProxyChain") || []; - const newChain = [...currentChain]; - newChain[index] = { - ...newChain[index], - port: parseInt(e.target.value) || 1080, - }; - form.setValue( - "socks5ProxyChain", - newChain, - ); - }} - /> -
    -
    + {socks5ProxyChain.map((node, index) => ( +
    +
    + + {t("hosts.proxyNode")} {index + 1} + + +
    +
    - {t("hosts.proxyType")} - { const currentChain = form.watch("socks5ProxyChain") || []; const newChain = [...currentChain]; newChain[index] = { ...newChain[index], - type: - value === "http" - ? ("http" as const) - : (parseInt(value) as 4 | 5), + host: e.target.value, }; form.setValue("socks5ProxyChain", newChain); }} - > - - - - - - {t("hosts.socks4")} - - - {t("hosts.socks5")} - - - {t("hosts.httpConnect")} - - - + onBlur={(e) => { + const currentChain = + form.watch("socks5ProxyChain") || []; + const newChain = [...currentChain]; + newChain[index] = { + ...newChain[index], + host: e.target.value.trim(), + }; + form.setValue("socks5ProxyChain", newChain); + }} + />
    -
    -
    - - {t("hosts.socks5Username")}{" "} - {t("hosts.optional")} - - { - const currentChain = - form.watch("socks5ProxyChain") || []; - const newChain = [...currentChain]; - newChain[index] = { - ...newChain[index], - username: e.target.value, - }; - form.setValue( - "socks5ProxyChain", - newChain, - ); - }} - onBlur={(e) => { - const currentChain = - form.watch("socks5ProxyChain") || []; - const newChain = [...currentChain]; - newChain[index] = { - ...newChain[index], - username: e.target.value.trim(), - }; - form.setValue( - "socks5ProxyChain", - newChain, - ); - }} - /> -
    - -
    - - {t("hosts.socks5Password")}{" "} - {t("hosts.optional")} - - { - const currentChain = - form.watch("socks5ProxyChain") || []; - const newChain = [...currentChain]; - newChain[index] = { - ...newChain[index], - password: e.target.value, - }; - form.setValue( - "socks5ProxyChain", - newChain, - ); - }} - /> -
    +
    + {t("hosts.socks5Port")} + { + const currentChain = + form.watch("socks5ProxyChain") || []; + const newChain = [...currentChain]; + newChain[index] = { + ...newChain[index], + port: parseInt(e.target.value) || 1080, + }; + form.setValue("socks5ProxyChain", newChain); + }} + />
    - ), - )} + +
    + {t("hosts.proxyType")} + +
    + +
    +
    + + {t("hosts.socks5Username")}{" "} + {t("hosts.optional")} + + { + const currentChain = + form.watch("socks5ProxyChain") || []; + const newChain = [...currentChain]; + newChain[index] = { + ...newChain[index], + username: e.target.value, + }; + form.setValue("socks5ProxyChain", newChain); + }} + onBlur={(e) => { + const currentChain = + form.watch("socks5ProxyChain") || []; + const newChain = [...currentChain]; + newChain[index] = { + ...newChain[index], + username: e.target.value.trim(), + }; + form.setValue("socks5ProxyChain", newChain); + }} + /> +
    + +
    + + {t("hosts.socks5Password")}{" "} + {t("hosts.optional")} + + { + const currentChain = + form.watch("socks5ProxyChain") || []; + const newChain = [...currentChain]; + newChain[index] = { + ...newChain[index], + password: e.target.value, + }; + form.setValue("socks5ProxyChain", newChain); + }} + /> +
    +
    +
    + ))}
    )} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostRemoteDesktopTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostRemoteDesktopTab.tsx index 2428701b..43ac6068 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostRemoteDesktopTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostRemoteDesktopTab.tsx @@ -23,22 +23,22 @@ import { import { PasswordInput } from "@/components/ui/password-input.tsx"; import type { HostRemoteDesktopTabProps } from "./shared/tab-types"; +type RemoteDesktopForm = HostRemoteDesktopTabProps["form"]; + function GuacField({ form, path, label, description, type = "text", - t, }: { - form: any; + form: RemoteDesktopForm; path: string; label: string; description?: string; type?: "text" | "number" | "password" | "switch"; - t: (key: string) => string; }) { - const fieldName = `guacamoleConfig.${path}` as any; + const fieldName = `guacamoleConfig.${path}` as never; if (type === "switch") { return ( @@ -120,13 +120,13 @@ function GuacSelect({ options, placeholder, }: { - form: any; + form: RemoteDesktopForm; path: string; label: string; options: { value: string; label: string }[]; placeholder?: string; }) { - const fieldName = `guacamoleConfig.${path}` as any; + const fieldName = `guacamoleConfig.${path}` as never; return (
    {!isTelnet && ( @@ -271,7 +269,6 @@ export function HostRemoteDesktopTab({ path="dpi" label={t("hosts.dpi")} type="number" - t={t} /> )} @@ -318,14 +314,12 @@ export function HostRemoteDesktopTab({ form={form} path="font-name" label={t("hosts.guacFontName")} - t={t} /> {isRDP && ( )} @@ -388,70 +380,60 @@ export function HostRemoteDesktopTab({ path="enable-wallpaper" label={t("hosts.enableWallpaper")} type="switch" - t={t} /> @@ -467,54 +449,46 @@ export function HostRemoteDesktopTab({ path="enable-printing" label={t("hosts.enablePrinting")} type="switch" - t={t} /> @@ -529,20 +503,17 @@ export function HostRemoteDesktopTab({ form={form} path="client-name" label={t("hosts.clientName")} - t={t} /> @@ -581,33 +551,28 @@ export function HostRemoteDesktopTab({ form={form} path="gateway-hostname" label={t("hosts.gatewayHostname")} - t={t} /> @@ -622,19 +587,16 @@ export function HostRemoteDesktopTab({ form={form} path="remote-app" label={t("hosts.remoteAppProgram")} - t={t} /> @@ -660,14 +622,12 @@ export function HostRemoteDesktopTab({ path="disable-copy" label={t("hosts.disableCopy")} type="switch" - t={t} /> @@ -692,14 +652,12 @@ export function HostRemoteDesktopTab({ path="swap-red-blue" label={t("hosts.swapRedBlue")} type="switch" - t={t} /> @@ -713,41 +671,35 @@ export function HostRemoteDesktopTab({ form={form} path="recording-path" label={t("hosts.recordingPath")} - t={t} /> @@ -761,33 +713,28 @@ export function HostRemoteDesktopTab({ path="wol-send-packet" label={t("hosts.sendWolPacket")} type="switch" - t={t} /> diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostSharingTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostSharingTab.tsx index 0f94d9e5..0dd0fff5 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostSharingTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostSharingTab.tsx @@ -56,7 +56,6 @@ import { UserCircle, } from "lucide-react"; import type { SSHHost } from "@/types"; -import type { HostSharingTabProps } from "./shared/tab-types"; interface User { id: string; @@ -64,15 +63,10 @@ interface User { is_admin: boolean; } -interface HostSharingTabProps { - hostId: number | undefined; - isNewHost: boolean; -} - export function HostSharingTab({ hostId, isNewHost, -}: SharingTabContentProps): React.ReactElement { +}: HostSharingTabProps): React.ReactElement { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); @@ -81,7 +75,7 @@ export function HostSharingTab({ const [selectedRoleId, setSelectedRoleId] = React.useState( null, ); - const [permissionLevel, setPermissionLevel] = React.useState("view"); + const permissionLevel = "view"; const [expiresInHours, setExpiresInHours] = React.useState(""); const [roles, setRoles] = React.useState([]); @@ -204,7 +198,7 @@ export function HostSharingTab({ setSelectedRoleId(null); setExpiresInHours(""); loadAccessList(); - } catch (error) { + } catch { toast.error(t("rbac.failedToShare")); } }; @@ -225,7 +219,7 @@ export function HostSharingTab({ await revokeHostAccess(hostId, accessId); toast.success(t("rbac.accessRevokedSuccessfully")); loadAccessList(); - } catch (error) { + } catch { toast.error(t("rbac.failedToRevokeAccess")); } }; diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostStatisticsTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostStatisticsTab.tsx index 4ea90ada..a10a659e 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostStatisticsTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostStatisticsTab.tsx @@ -17,7 +17,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select.tsx"; -import { Plus, X } from "lucide-react"; +import { Plus } from "lucide-react"; import type { HostStatisticsTabProps } from "./shared/tab-types"; import { QuickActionItem } from "./shared/QuickActionItem"; diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx index 66b55a7a..5abf83b4 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx @@ -40,9 +40,6 @@ import { cn } from "@/lib/utils.ts"; import { TERMINAL_THEMES, TERMINAL_FONTS, - CURSOR_STYLES, - BELL_STYLES, - FAST_SCROLL_MODIFIERS, } from "@/constants/terminal-themes.ts"; import { TerminalPreview } from "@/ui/desktop/apps/features/terminal/TerminalPreview.tsx"; import type { HostTerminalTabProps } from "./shared/tab-types"; @@ -51,7 +48,7 @@ import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; export function HostTerminalTab({ form, snippets, t }: HostTerminalTabProps) { const [snippetPopoverOpen, setSnippetPopoverOpen] = React.useState(false); - const { setPreviewTerminalTheme } = useTabs() as any; + const { setPreviewTerminalTheme } = useTabs(); return (
    + >({}); + const [tunnelActions, setTunnelActions] = useState>( + {}, + ); + const previousTunnelStatusesRef = useRef>({}); + const supportsC2S = + typeof window !== "undefined" && window.electronAPI?.isElectron === true; + + useEffect(() => { + return subscribeTunnelStatuses(setTunnelStatuses, () => { + // The form should stay usable if the tunnel status stream reconnects. + }); + }, []); + + useEffect(() => { + const previousStatuses = previousTunnelStatusesRef.current; + + for (const [tunnelName, status] of Object.entries(tunnelStatuses)) { + const previous = previousStatuses[tunnelName]; + const statusChanged = + previous?.status !== status.status || + previous?.reason !== status.reason || + previous?.retryCount !== status.retryCount || + previous?.nextRetryIn !== status.nextRetryIn; + + if (!statusChanged) continue; + + console.info("[tunnels] Server tunnel status changed", { + tunnelName, + status, + }); + + const statusValue = status.status?.toUpperCase(); + const hasFailureDetail = + statusValue === "ERROR" || + statusValue === "FAILED" || + (Boolean(status.errorType) && + Boolean(status.reason) && + previous?.reason !== status.reason); + + if (hasFailureDetail) { + const message = status.reason || t("tunnels.manualControlError"); + console.error("[tunnels] Server tunnel failed", { + tunnelName, + status, + }); + toast.error(message, { + id: `server-tunnel-error-${tunnelName}`, + }); + } + } + + previousTunnelStatusesRef.current = tunnelStatuses; + }, [t, tunnelStatuses]); + + const openC2SPresets = () => { + const profileTab = tabs.find((tab) => tab.type === "user_profile"); + if (profileTab) { + updateTab(profileTab.id, { + initialTab: "c2s-tunnels", + _updateTimestamp: Date.now(), + }); + setCurrentTab(profileTab.id); + return; + } + + const id = addTab({ + type: "user_profile", + title: t("profile.title"), + initialTab: "c2s-tunnels", + }); + setCurrentTab(id); + }; + + const getCurrentHost = (): SSHHost | null => { + if (!editingHost?.id) return null; + return hosts.find((host) => host.id === editingHost.id) || editingHost; + }; + + const getTunnelName = (host: SSHHost, index: number) => { + const tunnel = form.getValues(`serverTunnels.${index}`); + return `${host.id}::${index}::${host.name || `${host.username}@${host.ip}`}::${tunnel.sourcePort}::${tunnel.endpointHost}::${tunnel.endpointPort}`; + }; + + const handleServerTunnelAction = async ( + action: "connect" | "disconnect", + index: number, + ) => { + const host = getCurrentHost(); + if (!host?.id) { + toast.error(t("tunnels.saveHostBeforeManualControl")); + return; + } + + const tunnel = form.getValues(`serverTunnels.${index}`); + const tunnelName = getTunnelName(host, index); + setTunnelActions((current) => ({ ...current, [tunnelName]: true })); + console.info(`[tunnels] ${action} server tunnel`, { + tunnelName, + tunnel, + }); + + try { + if (action === "connect") { + const endpointHost = hosts.find( + (item) => + item.name === tunnel.endpointHost || + `${item.username}@${item.ip}` === tunnel.endpointHost, + ); + const tunnelConfig: TunnelConfig = { + name: tunnelName, + scope: "s2s", + mode: tunnel.mode || tunnel.tunnelType || "remote", + bindHost: tunnel.bindHost, + targetHost: tunnel.targetHost, + tunnelType: + tunnel.tunnelType || + (tunnel.mode === "local" || tunnel.mode === "remote" + ? tunnel.mode + : "remote"), + sourceHostId: host.id, + tunnelIndex: index, + hostName: host.name || `${host.username}@${host.ip}`, + sourceIP: host.ip, + sourceSSHPort: host.port, + sourceUsername: host.username, + sourcePassword: + host.authType === "password" ? host.password : undefined, + sourceAuthMethod: host.authType, + sourceSSHKey: host.authType === "key" ? host.key : undefined, + sourceKeyPassword: + host.authType === "key" ? host.keyPassword : undefined, + sourceKeyType: host.authType === "key" ? host.keyType : undefined, + sourceCredentialId: host.credentialId, + sourceUserId: host.userId, + endpointHost: tunnel.endpointHost, + endpointIP: endpointHost?.ip, + endpointSSHPort: endpointHost?.port, + endpointUsername: endpointHost?.username, + endpointPassword: + endpointHost?.authType === "password" + ? endpointHost.password + : undefined, + endpointAuthMethod: endpointHost?.authType, + endpointSSHKey: + endpointHost?.authType === "key" ? endpointHost.key : undefined, + endpointKeyPassword: + endpointHost?.authType === "key" + ? endpointHost.keyPassword + : undefined, + endpointKeyType: + endpointHost?.authType === "key" ? endpointHost.keyType : undefined, + endpointCredentialId: endpointHost?.credentialId, + endpointUserId: endpointHost?.userId, + sourcePort: tunnel.sourcePort, + endpointPort: tunnel.endpointPort, + maxRetries: tunnel.maxRetries, + retryInterval: tunnel.retryInterval * 1000, + autoStart: tunnel.autoStart, + isPinned: host.pin, + useSocks5: host.useSocks5, + socks5Host: host.socks5Host, + socks5Port: host.socks5Port, + socks5Username: host.socks5Username, + socks5Password: host.socks5Password, + socks5ProxyChain: host.socks5ProxyChain, + }; + await connectTunnel(tunnelConfig); + } else { + await disconnectTunnel(tunnelName); + } + } catch (error) { + console.error(`[tunnels] Failed to ${action} server tunnel`, { + tunnelName, + error, + }); + toast.error( + error instanceof Error + ? error.message + : t("tunnels.manualControlError"), + ); + } finally { + setTunnelActions((current) => ({ ...current, [tunnelName]: false })); + } + }; + return (
    -
    +
    +
    - ( - - {t("hosts.tunnelConnections")} - -
    - {field.value.map((connection, index) => ( -
    + {t("tunnels.clientTunnelsUnavailable")} +

    + )} + {(() => { + type TunnelFieldName = "serverTunnels"; + + const removeTunnel = ( + fieldName: TunnelFieldName, + tunnels: unknown[], + index: number, + ) => { + form.setValue( + fieldName, + tunnels.filter((_, i) => i !== index), + { shouldDirty: true, shouldValidate: true }, + ); + }; + + const addServerTunnel = (tunnels: unknown[]) => { + form.setValue( + "serverTunnels", + [ + ...tunnels, + { + scope: "s2s", + mode: "remote", + tunnelType: "remote", + bindHost: "", + targetHost: "", + sourcePort: 22, + endpointPort: 224, + endpointHost: "", + maxRetries: 3, + retryInterval: 10, + autoStart: false, + }, + ], + { shouldDirty: true, shouldValidate: true }, + ); + }; + + const renderTunnelCard = ( + fieldName: TunnelFieldName, + index: number, + displayIndex: number, + tunnels: unknown[], + ) => { + const scope = form.watch(`${fieldName}.${index}.scope`) || "s2s"; + const mode = form.watch(`${fieldName}.${index}.mode`) || "remote"; + const isC2S = scope === "c2s"; + const currentHost = getCurrentHost(); + const endpointConfigOptions = hosts + .filter( + (item) => + item.id !== currentHost?.id && + (item.connectionType || "ssh") === "ssh", + ) + .map((item) => item.name || `${item.username}@${item.ip}`) + .sort((a, b) => a.localeCompare(b)); + const tunnelName = currentHost + ? getTunnelName(currentHost, index) + : ""; + const tunnelStatus = tunnelName + ? tunnelStatuses[tunnelName] + : undefined; + const isTunnelActionLoading = tunnelName + ? Boolean(tunnelActions[tunnelName]) + : false; + const formScope = isC2S ? "client" : "server"; + const { sourcePortLabel, endpointPortLabel } = + getTunnelPortLabels(formScope, mode, t); + const modeDescription = getTunnelModeDescription( + formScope, + mode, + { + sourcePort: + form.watch(`${fieldName}.${index}.sourcePort`) || "22", + endpointPort: + form.watch(`${fieldName}.${index}.endpointPort`) || "22", + }, + t, + ); + + return ( +
    +
    +

    + {isC2S + ? t("tunnels.clientTunnel") + : t("tunnels.serverTunnel")}{" "} + {displayIndex + 1} +

    +
    + + handleServerTunnelAction("connect", index) + } + onStop={() => + handleServerTunnelAction("disconnect", index) + } + startDisabled={!currentHost} + startDisabledReason={t( + "tunnels.saveHostBeforeManualControl", + )} + /> + +
    +
    +
    + ( + + {t("tunnels.type")} + + { + field.onChange(nextMode); + form.setValue( + `${fieldName}.${index}.tunnelType`, + getTunnelTypeForMode(nextMode), + { + shouldDirty: true, + shouldValidate: true, + }, + ); + }} + /> + + + )} + /> +
    +
    + {!isC2S && ( + ( + + + {t("tunnels.endpointSshConfig")} + + + + + + )} + /> + )} + {mode !== "dynamic" && ( + ( + + {endpointPortLabel} + + + + + )} + /> + )} + {mode === "dynamic" && ( +
    + )} + {!isC2S && ( + ( + + {t("tunnels.currentHostIp")} + + + + + )} + /> + )} + ( + + {sourcePortLabel} + + + + + )} + /> +
    + +

    + {modeDescription} +

    + +
    + ( + + {t("tunnels.maxRetries")} + + + + + {t("tunnels.maxRetriesDescription")} + + + )} + /> + ( + + {t("tunnels.retryInterval")} + + + + + {t("tunnels.retryIntervalDescription")} + + + )} + /> + {!isC2S && ( + ( + + +
    + + {t("tunnels.autoStartContainer")} + + +
    +
    + + {t("tunnels.autoStartContainerDesc")} + +
    + )} + /> + )} +
    +
    + ); + }; + + return ( +
    + { + const serverTunnels = field.value || []; + + return ( + +
    +
    + + {t("tunnels.serverTunnels")} + +

    + {t("tunnels.serverTunnelsDesc")} +

    +
    -
    - ( - - {t("hosts.tunnelType")} - -
    - - -
    -
    -
    + +
    + {serverTunnels.length > 0 ? ( +
    + {serverTunnels.map((_, index) => + renderTunnelCard( + "serverTunnels", + index, + index, + serverTunnels, + ), + )} +
    + ) : ( +

    + {t("tunnels.noServerTunnels")} +

    )} - /> -
    -
    - ( - - - {t("hosts.sourcePort")} - {t("hosts.sourcePortDesc")} - - - - - - )} - /> - ( - - {t("hosts.endpointPort")} - - - - - )} - /> - ( - - - {t("hosts.endpointSshConfig")} - - - { - sshConfigInputRefs.current[index] = el; - }} - placeholder={t("placeholders.sshConfig")} - className="min-h-[40px]" - autoComplete="off" - value={endpointHostField.value} - onFocus={() => - setSshConfigDropdownOpen((prev) => ({ - ...prev, - [index]: true, - })) - } - onChange={(e) => { - endpointHostField.onChange(e); - setSshConfigDropdownOpen((prev) => ({ - ...prev, - [index]: true, - })); - }} - onBlur={(e) => { - endpointHostField.onChange( - e.target.value.trim(), - ); - endpointHostField.onBlur(); - }} - /> - - {sshConfigDropdownOpen[index] && - getFilteredSshConfigs(index).length > 0 && ( -
    { - sshConfigDropdownRefs.current[index] = - el; - }} - className="absolute top-full left-0 z-50 mt-1 w-full bg-canvas border border-input rounded-md shadow-lg max-h-40 overflow-y-auto thin-scrollbar p-1" - > -
    - {getFilteredSshConfigs(index).map( - (config) => ( - - ), - )} -
    -
    - )} -
    - )} - /> -
    - -

    - {form.watch( - `tunnelConnections.${index}.tunnelType`, - ) === "local" - ? t("hosts.tunnelForwardDescriptionLocal", { - sourcePort: - form.watch( - `tunnelConnections.${index}.sourcePort`, - ) || "22", - endpointPort: - form.watch( - `tunnelConnections.${index}.endpointPort`, - ) || "224", - }) - : t("hosts.tunnelForwardDescriptionRemote", { - sourcePort: - form.watch( - `tunnelConnections.${index}.sourcePort`, - ) || "22", - endpointPort: - form.watch( - `tunnelConnections.${index}.endpointPort`, - ) || "224", - })} -

    - -
    - ( - - {t("hosts.maxRetries")} - - - - - {t("hosts.maxRetriesDescription")} - - - )} - /> - ( - - - {t("hosts.retryInterval")} - - - - - - {t("hosts.retryIntervalDescription")} - - - )} - /> - ( - - - {t("hosts.autoStartContainer")} - - - - - - {t("hosts.autoStartDesc")} - - - )} - /> -
    -
    - ))} - -
    - - - )} - /> +
    + + + ); + }} + /> +
    + ); + })()} )}
    diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/shared/tab-types.ts b/src/ui/desktop/apps/host-manager/hosts/tabs/shared/tab-types.ts index a2886189..eb60cdc3 100644 --- a/src/ui/desktop/apps/host-manager/hosts/tabs/shared/tab-types.ts +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/shared/tab-types.ts @@ -36,7 +36,7 @@ export interface HostGeneralTabProps { export interface HostTerminalTabProps { form: UseFormReturn; snippets: Array<{ id: number; name: string; content: string }>; - t: (key: string) => string; + t: (key: string, options?: unknown) => string; } export interface HostDockerTabProps { @@ -46,18 +46,8 @@ export interface HostDockerTabProps { export interface HostTunnelTabProps { form: UseFormReturn; - sshConfigDropdownOpen: { [key: number]: boolean }; - setSshConfigDropdownOpen: React.Dispatch< - React.SetStateAction<{ [key: number]: boolean }> - >; - sshConfigInputRefs: React.MutableRefObject<{ - [key: number]: HTMLInputElement | null; - }>; - sshConfigDropdownRefs: React.MutableRefObject<{ - [key: number]: HTMLDivElement | null; - }>; - getFilteredSshConfigs: (index: number) => string[]; - handleSshConfigClick: (config: string, index: number) => void; + hosts: SSHHost[]; + editingHost?: SSHHost | null; t: (key: string) => string; } diff --git a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx b/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx index ced1176a..f2c30dc5 100644 --- a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx +++ b/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx @@ -38,19 +38,16 @@ import { Trash2, Copy, X, - RotateCcw, Search, Loader2, Terminal, LayoutGrid, - MonitorCheck, Folder, ChevronDown, ChevronRight, GripVertical, FolderPlus, Settings, - MoreVertical, Server, Cloud, Database, @@ -61,7 +58,6 @@ import { HardDrive, Globe, Share2, - Users, } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -184,7 +180,7 @@ export function SSHToolsSidebar({ const [isRecording, setIsRecording] = useState(false); const [selectedTabIds, setSelectedTabIds] = useState([]); const [rightClickCopyPaste, setRightClickCopyPaste] = useState( - () => getCookie("rightClickCopyPaste") === "true", + () => getCookie("rightClickCopyPaste") !== "false", ); const [snippets, setSnippets] = useState([]); @@ -229,7 +225,6 @@ export function SSHToolsSidebar({ [], ); const [draggedSnippet, setDraggedSnippet] = useState(null); - const [dragOverFolder, setDragOverFolder] = useState(null); const [collapsedFolders, setCollapsedFolders] = useState>(() => { const shouldCollapse = localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false"; @@ -265,7 +260,7 @@ export function SSHToolsSidebar({ const [splitAssignments, setSplitAssignments] = useState>( new Map(), ); - const [previewKey, setPreviewKey] = useState(0); + const [, setPreviewKey] = useState(0); const [draggedTabId, setDraggedTabId] = useState(null); const [dragOverCellIndex, setDragOverCellIndex] = useState( null, @@ -940,25 +935,16 @@ export function SSHToolsSidebar({ e.dataTransfer.effectAllowed = "move"; }; - const handleDragOver = (e: React.DragEvent, targetSnippet: Snippet) => { + const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }; - const handleDragEnterFolder = (folderName: string) => { - setDragOverFolder(folderName); - }; - - const handleDragLeaveFolder = () => { - setDragOverFolder(null); - }; - const handleDrop = async (e: React.DragEvent, targetSnippet: Snippet) => { e.preventDefault(); if (!draggedSnippet || draggedSnippet.id === targetSnippet.id) { setDraggedSnippet(null); - setDragOverFolder(null); return; } @@ -968,7 +954,6 @@ export function SSHToolsSidebar({ if (sourceFolder !== targetFolder) { toast.error(t("snippets.reorderSameFolder")); setDraggedSnippet(null); - setDragOverFolder(null); return; } @@ -985,7 +970,6 @@ export function SSHToolsSidebar({ if (draggedIndex === -1 || targetIndex === -1) { setDraggedSnippet(null); - setDragOverFolder(null); return; } @@ -1008,12 +992,10 @@ export function SSHToolsSidebar({ } setDraggedSnippet(null); - setDragOverFolder(null); }; const handleDragEnd = () => { setDraggedSnippet(null); - setDragOverFolder(null); }; const handleCreateFolder = () => { @@ -1214,10 +1196,6 @@ export function SSHToolsSidebar({ toast.success(t("splitScreen.cleared")); }; - const handleResetToSingle = () => { - handleClearSplit(); - }; - const handleCommandSelect = (command: string) => { if (activeTerminal?.terminalRef?.current?.sendInput) { activeTerminal.terminalRef.current.sendInput(command); @@ -1256,14 +1234,6 @@ export function SSHToolsSidebar({ {t("nav.tools")}
    -