mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
release-2.6.0 (#1085)
* fix: general bug fixes * fix: general qol additions * ci(deps): bump actions/setup-node in the github-actions group (#1068) Bumps the github-actions group with 1 update: [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/setup-node` from 6 to 7 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump the dev-patch-updates group with 28 updates (#1069) Bumps the dev-patch-updates group with 28 updates: | Package | From | To | | --- | --- | --- | | [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.2` | `2.5.4` | | [@codemirror/view](https://github.com/codemirror/view) | `6.43.5` | `6.43.6` | | [@radix-ui/react-accordion](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/accordion) | `1.2.15` | `1.2.17` | | [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.18` | `1.1.20` | | [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.6` | `1.3.8` | | [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.18` | `1.1.20` | | [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.19` | `2.1.21` | | [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.11` | `2.1.12` | | [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.18` | `1.1.20` | | [@radix-ui/react-progress](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/progress) | `1.1.11` | `1.1.13` | | [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.13` | `1.2.15` | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.2` | `2.3.4` | | [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.11` | `1.1.12` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.2` | `1.4.4` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.2` | `1.3.4` | | [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.16` | `1.1.18` | | [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.11` | `1.2.13` | | [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.2` | `4.3.3` | | [@uiw/codemirror-extensions-langs](https://github.com/uiwjs/react-codemirror) | `4.25.10` | `4.25.11` | | [@uiw/codemirror-theme-github](https://github.com/uiwjs/react-codemirror) | `4.25.10` | `4.25.11` | | [@uiw/react-codemirror](https://github.com/uiwjs/react-codemirror) | `4.25.10` | `4.25.11` | | [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.1.9` | `4.1.10` | | [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `4.1.9` | `4.1.10` | | [i18next](https://github.com/i18next/i18next) | `26.3.4` | `26.3.6` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.6.1` | `1.6.3` | | [react-i18next](https://github.com/i18next/react-i18next) | `17.0.8` | `17.0.10` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.2` | `4.3.3` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.9` | `4.1.10` | Updates `@biomejs/biome` from 2.5.2 to 2.5.4 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.4/packages/@biomejs/biome) Updates `@codemirror/view` from 6.43.5 to 6.43.6 - [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md) - [Commits](https://github.com/codemirror/view/commits) Updates `@radix-ui/react-accordion` from 1.2.15 to 1.2.17 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/accordion/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/accordion) Updates `@radix-ui/react-alert-dialog` from 1.1.18 to 1.1.20 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog) Updates `@radix-ui/react-checkbox` from 1.3.6 to 1.3.8 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox) Updates `@radix-ui/react-dialog` from 1.1.18 to 1.1.20 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog) Updates `@radix-ui/react-dropdown-menu` from 2.1.19 to 2.1.21 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu) Updates `@radix-ui/react-label` from 2.1.11 to 2.1.12 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label) Updates `@radix-ui/react-popover` from 1.1.18 to 1.1.20 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover) Updates `@radix-ui/react-progress` from 1.1.11 to 1.1.13 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/progress/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/progress) Updates `@radix-ui/react-scroll-area` from 1.2.13 to 1.2.15 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area) Updates `@radix-ui/react-select` from 2.3.2 to 2.3.4 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-separator` from 1.1.11 to 1.1.12 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator) Updates `@radix-ui/react-slider` from 1.4.2 to 1.4.4 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-switch` from 1.3.2 to 1.3.4 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `@radix-ui/react-tabs` from 1.1.16 to 1.1.18 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs) Updates `@radix-ui/react-tooltip` from 1.2.11 to 1.2.13 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip) Updates `@tailwindcss/vite` from 4.3.2 to 4.3.3 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-vite) Updates `@uiw/codemirror-extensions-langs` from 4.25.10 to 4.25.11 - [Release notes](https://github.com/uiwjs/react-codemirror/releases) - [Commits](https://github.com/uiwjs/react-codemirror/compare/v4.25.10...v4.25.11) Updates `@uiw/codemirror-theme-github` from 4.25.10 to 4.25.11 - [Release notes](https://github.com/uiwjs/react-codemirror/releases) - [Commits](https://github.com/uiwjs/react-codemirror/compare/v4.25.10...v4.25.11) Updates `@uiw/react-codemirror` from 4.25.10 to 4.25.11 - [Release notes](https://github.com/uiwjs/react-codemirror/releases) - [Commits](https://github.com/uiwjs/react-codemirror/compare/v4.25.10...v4.25.11) Updates `@vitest/coverage-v8` from 4.1.9 to 4.1.10 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/coverage-v8) Updates `@vitest/ui` from 4.1.9 to 4.1.10 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/ui) Updates `i18next` from 26.3.4 to 26.3.6 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.3.4...v26.3.6) Updates `radix-ui` from 1.6.1 to 1.6.3 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui) Updates `react-i18next` from 17.0.8 to 17.0.10 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v17.0.8...v17.0.10) Updates `tailwindcss` from 4.3.2 to 4.3.3 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss) Updates `vitest` from 4.1.9 to 4.1.10 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.5.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@codemirror/view" dependency-version: 6.43.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-accordion" dependency-version: 1.2.17 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-alert-dialog" dependency-version: 1.1.20 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-checkbox" dependency-version: 1.3.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-dialog" dependency-version: 1.1.20 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-dropdown-menu" dependency-version: 2.1.21 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-label" dependency-version: 2.1.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-popover" dependency-version: 1.1.20 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-progress" dependency-version: 1.1.13 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-scroll-area" dependency-version: 1.2.15 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-separator" dependency-version: 1.1.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-slider" dependency-version: 1.4.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-tabs" dependency-version: 1.1.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@radix-ui/react-tooltip" dependency-version: 1.2.13 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@uiw/codemirror-extensions-langs" dependency-version: 4.25.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@uiw/codemirror-theme-github" dependency-version: 4.25.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@uiw/react-codemirror" dependency-version: 4.25.11 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@vitest/ui" dependency-version: 4.1.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: i18next dependency-version: 26.3.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: radix-ui dependency-version: 1.6.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: react-i18next dependency-version: 17.0.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: tailwindcss dependency-version: 4.3.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: vitest dependency-version: 4.1.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump ws in the prod-patch-updates group (#1071) Bumps the prod-patch-updates group with 1 update: [ws](https://github.com/websockets/ws). Updates `ws` from 8.21.0 to 8.21.1 - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.21.0...8.21.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the major-updates group with 2 updates (#1072) Bumps the major-updates group with 2 updates: [nanoid](https://github.com/ai/nanoid) and [typescript](https://github.com/microsoft/TypeScript). Updates `nanoid` from 5.1.16 to 6.0.0 - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/5.1.16...6.0.0) Updates `typescript` from 6.0.3 to 7.0.2 - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](https://github.com/microsoft/TypeScript/commits) --- updated-dependencies: - dependency-name: nanoid dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: major-updates - dependency-name: typescript dependency-version: 7.0.2 dependency-type: direct:development update-type: version-update:semver-major dependency-group: major-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: general qol additions and new analytics/telemetrics feature * fix: incorrect version sent to posthog * feat: add multiplayer/shared sessions for terminal and guacd * feat: rework Electron desktop app to run standalone-first with optional two-way sync to a remote Termix server * Fix Guacamole tab visibility lifecycle (#1074) Co-authored-by: default-student <default-student@github.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * fix(alerts): send channel config as object payload instead of JSON string (#1075) * fix tmux-monitor tailscale issue (#1076) * Initial plan * fix(tmux-monitor): explicitly handle tailscale auth in PanePreview hostConfig For Tailscale-auth hosts the pane-preview attach path was building the Terminal hostConfig with only the generic spread of host fields. This could omit or mismap auth-critical details and trigger a plain TCP/SSH reachability path that doesn't work with Tailscale-only SSH endpoints. The fix branches on `host.authType === "tailscale"` and: - Carries `authType: "tailscale"` explicitly so the backend always selects the Tailscale-aware PTY path regardless of how the host object evolves. - Derives `port` from `host.sshPort ?? host.port` so Tailscale SSH endpoints on a non-default SSH port are reached correctly. - Leaves all non-tailscale auth types on the unchanged code path. Reattach (bumping instanceId + attachNonce) continues to work because terminalHostConfig is recomputed on every render with the latest instanceIdRef.current value. * refactor(tmux-monitor): simplify tailscale port logic with extracted variable Address code review feedback: extract resolvedPort into a local variable to avoid the duplicated `host.sshPort ?? host.port` expression that was assigned to both `port` and `sshPort`. Restructure as an if/else block instead of an IIFE for readability. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * chore: run prettier * chore: update beta release text * fix: cant update credential of an RDP host * feat: add custom key shortcuts * feat: add support for MFA over SSH * fix: Invalid websocket frame causing code 10006 crash triggering restart loop * fix(net): correct SSRF blocklist false-positive blocking all outbound IPv4 (#1079) * fix: correct IPv4-mapped-IPv6 blocklist entry blocking all outbound IPv4 ::ffff:0:0/96 in the IPv6 blocklist matches every IPv4 address once mapped, since Node's BlockList compares addresses in their mapped form internally regardless of the declared family. This caused safeOutboundFetch to reject all IPv4-resolved destinations as private, breaking outbound requests (e.g. ntfy/webhook notifications) whenever DNS resolved to IPv4. Replaced with individual mapped ranges mirroring the existing IPv4 blocklist. * test: cover isBlockedAddress and link the Node BlockList citation Exports isBlockedAddress so its family-crossing behavior around IPv4-mapped-IPv6 addresses can actually be asserted, instead of relying on manual container debugging to notice a regression. Also swaps the prior "Node's BlockList compares addresses in mapped form" comment for one citing the documented example in the Node docs (https://nodejs.org/api/net.html#class-netblocklist), since that behavior isn't otherwise obvious from the addSubnet/check call sites. Related: Termix-SSH/Support#1024 * refactor: derive IPv6 mirror from IPv4 list, split DNS error messages Two follow-ups from review: - The IPv6 blocklist previously hand-duplicated each IPv4 range as its IPv4-mapped-IPv6 equivalent. Nothing enforced the two stayed in sync, which is exactly how the original bug (a mismatched ::ffff:0:0/96 entry blocking all IPv4) was introduced in the first place. Now derived from a single blockedIpv4Ranges list in one loop. - The connect.lookup hook threw the same "Private destinations are not allowed" for both an empty DNS result and an actually-blocked address. An empty result is a resolution failure, not a privacy decision, and conflating the two is the same kind of opaque-error problem that made this bug slow to diagnose in production. Split into distinct messages. Also extracted the lookup hook itself (createDnsLookupHook) so it can be unit-tested against a fake resolver directly, instead of only through a real fetch()/Agent call — the bug lived entirely in this callback, and undici wraps any error thrown here as a generic "fetch failed" TypeError, which is why isolating it matters for testability. --------- Co-authored-by: brennanneoh <497569+brennanneoh@users.noreply.github.com> * fix(ssh): do not offer chacha20-poly1305 without the native ssh2 binding (#1081) The availability probe treated a working OpenSSL "chacha20" cipher as proof that chacha20-poly1305@openssh.com is usable. It is not: ssh2 pure-JS chacha20-poly1305 corrupts the transport, so the peer aborts the KEX ("incomplete message [preauth]") and the connection times out. Easy to hit on jump-host connections whose target sshd negotiates chacha20-poly1305 first. Only trust the native binding (sshcrypto.node); otherwise leave the cipher disabled so filterCiphers() drops it and AES-GCM is negotiated instead. Co-authored-by: XtraLarge <> * fix: add Swiss German server layout (#1078) * chore: update release notes * feat: continue improving desktop app 2-way sync with logic fixes and a migration dialog * fix: dekstop app showing auth form without syncing * feat: create desktop auto sessions for existing setups * feat: add electron backend killing * fix: electron login and session related bugs and updated readme for v2.6.0 * chore: finalize release notes * fix: click to expand hosts including extra bottom margin * fix: desktop auth modal failing to log users in * fix: desktop app failing to sync * fix: reverse proxy causing sync error * chore: lint, format, and bump version to 2.6.0 * chore: sync Crowdin translations for 2.6.0 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: L.H. <117188168+default-student@users.noreply.github.com> Co-authored-by: default-student <default-student@github.com> Co-authored-by: Brad Baker <xyzulu@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Brennan Neoh <brennanneoh@users.noreply.github.com> Co-authored-by: brennanneoh <497569+brennanneoh@users.noreply.github.com> Co-authored-by: XtraLarge <eMail@WilliWerres.de> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
co-authored by
XtraLarge <>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
L.H.
default-student
Brad Baker
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Copilot Autofix powered by AI
Brennan Neoh
brennanneoh
XtraLarge
ZacharyZcR
parent
cf3e2cb499
commit
1a26628a48
+206
-95
@@ -20,6 +20,7 @@ const net = require("net");
|
||||
const { URL } = require("url");
|
||||
const { fork, spawn } = require("child_process");
|
||||
const WebSocket = require("ws");
|
||||
const remoteSync = require("./remote-sync.cjs");
|
||||
|
||||
// Portable mode: if a `.portable` marker exists next to the executable,
|
||||
// store all data in a `data` folder beside the exe instead of %APPDATA%.
|
||||
@@ -441,7 +442,10 @@ function isInvalidCertificateAllowedForUrl(url) {
|
||||
// fall through
|
||||
}
|
||||
|
||||
const config = getServerConfigSync();
|
||||
// The only remaining "connected remote server" a self-signed/invalid
|
||||
// certificate could legitimately apply to is the Remote Sync server
|
||||
// (also used for C2S tunnel relaying, see getC2SRelayUrl).
|
||||
const config = remoteSync.getRemoteSyncConfig();
|
||||
if (!config?.allowInvalidCertificate || !config?.serverUrl) return false;
|
||||
|
||||
return getOrigin(url) === getOrigin(config.serverUrl);
|
||||
@@ -816,7 +820,62 @@ function getBackendDataDir() {
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
function getBackendPidFilePath() {
|
||||
return path.join(app.getPath("userData"), "backend.pid");
|
||||
}
|
||||
|
||||
// If the app was previously killed abnormally (crash, force-quit, Task
|
||||
// Manager) rather than through the normal quit flow, will-quit never fires
|
||||
// and stopBackendServer() never runs -- the forked backend child is a
|
||||
// genuinely separate OS process on Windows/mac/Linux, so it keeps running
|
||||
// and holding every port the backend binds (30001, 30003-30008, 30010,
|
||||
// 30012...). Every subsequent launch's own backend then fails outright
|
||||
// with EADDRINUSE and the app is stuck until something manually kills the
|
||||
// orphan. Reap any such leftover process, identified by PID file, before
|
||||
// spawning a new one.
|
||||
function reapOrphanedBackendProcess() {
|
||||
const pidFilePath = getBackendPidFilePath();
|
||||
let recordedPid;
|
||||
try {
|
||||
recordedPid = parseInt(fs.readFileSync(pidFilePath, "utf8").trim(), 10);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!Number.isInteger(recordedPid) || recordedPid <= 0) return;
|
||||
|
||||
try {
|
||||
// Signal 0 does not kill the process -- it only checks whether a
|
||||
// process with this PID exists and is signalable, throwing ESRCH if
|
||||
// not. This avoids killing an unrelated process that happens to have
|
||||
// reused the same PID since the last run.
|
||||
process.kill(recordedPid, 0);
|
||||
} catch {
|
||||
// No live process at that PID; nothing to reap.
|
||||
try {
|
||||
fs.unlinkSync(pidFilePath);
|
||||
} catch {
|
||||
// already absent
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logToFile(
|
||||
`Found orphaned backend process from a previous session (pid ${recordedPid}), terminating it before starting a new one`,
|
||||
);
|
||||
try {
|
||||
process.kill(recordedPid, "SIGKILL");
|
||||
} catch {
|
||||
// already gone
|
||||
}
|
||||
try {
|
||||
fs.unlinkSync(pidFilePath);
|
||||
} catch {
|
||||
// already absent
|
||||
}
|
||||
}
|
||||
|
||||
function startBackendServer() {
|
||||
reapOrphanedBackendProcess();
|
||||
return new Promise((resolve) => {
|
||||
const { entryPath, backendCwd } = getBackendPaths();
|
||||
|
||||
@@ -852,11 +911,17 @@ function startBackendServer() {
|
||||
NODE_ENV: "production",
|
||||
ELECTRON_EMBEDDED: "true",
|
||||
PORT: "30001",
|
||||
VERSION: app.getVersion(),
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||
});
|
||||
|
||||
logToFile("Backend process spawned, pid:", backendProcess.pid);
|
||||
try {
|
||||
fs.writeFileSync(getBackendPidFilePath(), String(backendProcess.pid));
|
||||
} catch {
|
||||
// Non-fatal: only means a future crash won't self-heal via reap.
|
||||
}
|
||||
|
||||
let resolved = false;
|
||||
const readyTimeout = setTimeout(() => {
|
||||
@@ -888,6 +953,7 @@ function startBackendServer() {
|
||||
backendStartFailed = true;
|
||||
}
|
||||
backendProcess = null;
|
||||
clearBackendPidFile();
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
clearTimeout(readyTimeout);
|
||||
@@ -907,6 +973,14 @@ function startBackendServer() {
|
||||
});
|
||||
}
|
||||
|
||||
function clearBackendPidFile() {
|
||||
try {
|
||||
fs.unlinkSync(getBackendPidFilePath());
|
||||
} catch {
|
||||
// already absent
|
||||
}
|
||||
}
|
||||
|
||||
function stopBackendServer() {
|
||||
if (!backendProcess) return;
|
||||
|
||||
@@ -929,6 +1003,7 @@ function stopBackendServer() {
|
||||
backendProcess.on("exit", () => {
|
||||
clearTimeout(forceKillTimeout);
|
||||
backendProcess = null;
|
||||
clearBackendPidFile();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1335,7 +1410,6 @@ ipcMain.handle("get-embedded-server-status", () => {
|
||||
return {
|
||||
running:
|
||||
backendProcess !== null && !backendProcess.killed && !backendStartFailed,
|
||||
embedded: !isDev,
|
||||
dataDir: isDev ? null : getBackendDataDir(),
|
||||
};
|
||||
});
|
||||
@@ -1442,6 +1516,80 @@ ipcMain.handle("save-server-config", (event, config) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Remote sync (optional desktop <-> self-hosted server sync) ---
|
||||
|
||||
// Surfaces the pre-standalone-rework server-config.json (if a serverUrl was
|
||||
// ever set in it) so the renderer can prompt upgraded installs to set up
|
||||
// Remote Sync -- their hosts live on that old server and won't appear
|
||||
// locally until sync is enabled. A fresh install never had this file, so
|
||||
// this is naturally false for anyone who never used the old architecture.
|
||||
ipcMain.handle("get-legacy-server-config", () => {
|
||||
const config = getServerConfigSync();
|
||||
return { serverUrl: config?.serverUrl || null };
|
||||
});
|
||||
|
||||
ipcMain.handle("get-desktop-settings", () => {
|
||||
return remoteSync.getDesktopSettings();
|
||||
});
|
||||
|
||||
ipcMain.handle("save-desktop-settings", (_event, settings) => {
|
||||
return remoteSync.saveDesktopSettings(settings);
|
||||
});
|
||||
|
||||
ipcMain.handle("get-remote-sync-config", () => {
|
||||
return remoteSync.getRemoteSyncConfig();
|
||||
});
|
||||
|
||||
ipcMain.handle("save-remote-sync-config", (_event, config) => {
|
||||
return remoteSync.saveRemoteSyncConfig(config);
|
||||
});
|
||||
|
||||
ipcMain.handle("clear-remote-sync-config", async () => {
|
||||
const result = remoteSync.clearRemoteSyncConfig();
|
||||
remoteSync.clearRemoteSyncJwt();
|
||||
remoteSync.getRemoteSyncEngine()?.updateStatus({
|
||||
connected: false,
|
||||
syncing: false,
|
||||
needsReauth: false,
|
||||
lastError: null,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
ipcMain.handle("save-remote-sync-jwt", (_event, token) => {
|
||||
const result = remoteSync.saveRemoteSyncJwt(token);
|
||||
if (result.success) {
|
||||
remoteSync.getRemoteSyncEngine()?.updateStatus({
|
||||
connected: true,
|
||||
needsReauth: false,
|
||||
lastError: null,
|
||||
});
|
||||
remoteSync.getRemoteSyncEngine()?.syncNow();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
ipcMain.handle("get-remote-sync-jwt", () => {
|
||||
return remoteSync.getRemoteSyncJwt();
|
||||
});
|
||||
|
||||
ipcMain.handle("clear-remote-sync-jwt", () => {
|
||||
return remoteSync.clearRemoteSyncJwt();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-remote-sync-status", () => {
|
||||
return remoteSync.getRemoteSyncEngine()?.status || null;
|
||||
});
|
||||
|
||||
ipcMain.handle("remote-sync-now", async () => {
|
||||
return (await remoteSync.getRemoteSyncEngine()?.syncNow()) || null;
|
||||
});
|
||||
|
||||
ipcMain.handle("notify-local-login", (_event, token) => {
|
||||
remoteSync.getRemoteSyncEngine()?.setLocalJwt(token);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
function getC2STunnelConfigPath() {
|
||||
return path.join(app.getPath("userData"), "c2s-tunnels.json");
|
||||
}
|
||||
@@ -1577,36 +1725,33 @@ const C2S_WS_HIGH_WATERMARK = 1024 * 1024;
|
||||
const C2S_WS_LOW_WATERMARK = 256 * 1024;
|
||||
const C2S_STREAM_WRITE_LIMIT = 8 * 1024 * 1024;
|
||||
|
||||
// C2S (client-to-server) tunnels relay through a connected, self-hosted
|
||||
// Termix server -- the same "remote server" concept Remote Sync connects
|
||||
// to, not the always-local embedded backend. There's no separate C2S
|
||||
// server-URL setting in the UI; it has always shared whatever remote
|
||||
// server the rest of the app was pointed at. Before the standalone-first
|
||||
// rework that was server-config.json; now it's remote-sync-config.json,
|
||||
// since that's the only remaining notion of "a connected remote server."
|
||||
function getC2SRelayUrl() {
|
||||
const config = getServerConfigSync();
|
||||
const serverUrl =
|
||||
config?.serverUrl || (!isDev ? "http://127.0.0.1:30003" : null);
|
||||
const config = remoteSync.getRemoteSyncConfig();
|
||||
const serverUrl = config?.serverUrl;
|
||||
if (!serverUrl) {
|
||||
throw new Error("No Termix server configured");
|
||||
throw new Error(
|
||||
"No remote Termix server connected -- enable Remote Sync first",
|
||||
);
|
||||
}
|
||||
|
||||
const base = serverUrl.replace(/\/$/, "");
|
||||
const relayHttpUrl = base.endsWith(":30003")
|
||||
? `${base}/ssh/tunnel/c2s/stream`
|
||||
: `${base}/ssh/tunnel/c2s/stream`;
|
||||
const relayHttpUrl = `${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;
|
||||
async function getC2SRelayHeaders() {
|
||||
const jwt = remoteSync.getRemoteSyncJwt();
|
||||
if (!jwt) return {};
|
||||
|
||||
return {
|
||||
Cookie: `jwt=${encodeURIComponent(jwt)}`,
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1706,7 +1851,7 @@ async function openC2SRelay(
|
||||
) {
|
||||
const tunnelName = tunnel.name || getC2STunnelName(tunnel);
|
||||
const relayUrl = getC2SRelayUrl();
|
||||
const headers = await getC2SRelayHeaders(relayUrl);
|
||||
const headers = await getC2SRelayHeaders();
|
||||
logToFile(`[c2s] opening relay for ${tunnelName}`, {
|
||||
relayUrl,
|
||||
targetHost,
|
||||
@@ -1811,7 +1956,7 @@ async function openC2SRelay(
|
||||
|
||||
async function testC2SRelay(tunnel, targetHost, targetPort) {
|
||||
const relayUrl = getC2SRelayUrl();
|
||||
const headers = await getC2SRelayHeaders(relayUrl);
|
||||
const headers = await getC2SRelayHeaders();
|
||||
const ws = new WebSocket(
|
||||
relayUrl,
|
||||
getWebSocketOptions(relayUrl, { headers }),
|
||||
@@ -2090,7 +2235,7 @@ async function startC2SRemoteTunnel(tunnel, index = 0) {
|
||||
}
|
||||
|
||||
const relayUrl = getC2SRelayUrl();
|
||||
const headers = await getC2SRelayHeaders(relayUrl);
|
||||
const headers = await getC2SRelayHeaders();
|
||||
const ws = new WebSocket(
|
||||
relayUrl,
|
||||
getWebSocketOptions(relayUrl, { headers }),
|
||||
@@ -2772,31 +2917,33 @@ ipcMain.handle("close-external-editor", (_event, editId) => {
|
||||
ipcMain.handle("test-server-connection", async (event, serverUrl) => {
|
||||
try {
|
||||
const normalizedServerUrl = serverUrl.replace(/\/$/, "");
|
||||
|
||||
const healthUrl = `${normalizedServerUrl}/health`;
|
||||
|
||||
// This is a best-effort reachability probe, not a hard gate: a reverse
|
||||
// proxy doing SSO in front of the real server (Pangolin, Authelia,
|
||||
// Cloudflare Access, etc.) intercepts this unauthenticated request
|
||||
// before it ever reaches Termix's own /health route, and returns its
|
||||
// own login page (HTML, or a redirect) instead of {"status":"ok"}.
|
||||
// That's a legitimate, working setup -- the login iframe shown right
|
||||
// after this check is what actually proves the server is real, by
|
||||
// completing an authenticated round-trip. So any response at all here
|
||||
// (any status code, any body) means "something is there, let the user
|
||||
// proceed"; only a network-level failure (nothing answered at all)
|
||||
// blocks continuing.
|
||||
try {
|
||||
const response = await httpFetch(healthUrl, {
|
||||
method: "GET",
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.text();
|
||||
|
||||
if (
|
||||
data.includes("<html") ||
|
||||
data.includes("<!DOCTYPE") ||
|
||||
data.includes("<head>") ||
|
||||
data.includes("<body>")
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Server returned HTML instead of JSON. This does not appear to be a Termix server.",
|
||||
};
|
||||
}
|
||||
const data = await response.text();
|
||||
const looksLikeHtml =
|
||||
data.includes("<html") ||
|
||||
data.includes("<!DOCTYPE") ||
|
||||
data.includes("<head>") ||
|
||||
data.includes("<body>");
|
||||
|
||||
if (response.ok && !looksLikeHtml) {
|
||||
try {
|
||||
const healthData = JSON.parse(data);
|
||||
if (
|
||||
@@ -2816,64 +2963,27 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
|
||||
console.log("Health endpoint did not return valid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
// Reachable, but not a recognized Termix health response -- likely a
|
||||
// proxy/SSO login page in front of the real server. Let the user
|
||||
// proceed; the login step next will fail clearly if this really
|
||||
// isn't a Termix server.
|
||||
return {
|
||||
success: true,
|
||||
status: response.status,
|
||||
testedUrl: healthUrl,
|
||||
warning: looksLikeHtml
|
||||
? "Could not confirm this is a Termix server (the response looked like an HTML page, which can happen behind a login-protected reverse proxy). You can continue, and the next step will fail clearly if this isn't actually a Termix server."
|
||||
: "Server responded, but not with the expected health check format. Continuing anyway.",
|
||||
};
|
||||
} catch (urlError) {
|
||||
console.error("Health check failed:", urlError);
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Server is not responding. Please ensure the server is running and accessible.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const versionUrl = `${normalizedServerUrl}/version`;
|
||||
const response = await httpFetch(versionUrl, {
|
||||
method: "GET",
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.text();
|
||||
|
||||
if (
|
||||
data.includes("<html") ||
|
||||
data.includes("<!DOCTYPE") ||
|
||||
data.includes("<head>") ||
|
||||
data.includes("<body>")
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Server returned HTML instead of JSON. This does not appear to be a Termix server.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const versionData = JSON.parse(data);
|
||||
if (
|
||||
versionData &&
|
||||
(versionData.status === "up_to_date" ||
|
||||
versionData.status === "requires_update" ||
|
||||
(versionData.localVersion &&
|
||||
versionData.version &&
|
||||
versionData.latest_release))
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
status: response.status,
|
||||
testedUrl: versionUrl,
|
||||
warning:
|
||||
"Health endpoint not available, but server appears to be running",
|
||||
};
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.log("Version endpoint did not return valid JSON");
|
||||
}
|
||||
}
|
||||
} catch (versionError) {
|
||||
console.error("Version check failed:", versionError);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"Server is not responding or does not appear to be a valid Termix server. Please ensure the server is running and accessible.",
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
@@ -2967,6 +3077,7 @@ app.whenReady().then(async () => {
|
||||
|
||||
createTray();
|
||||
createWindow();
|
||||
remoteSync.initRemoteSync(() => mainWindow);
|
||||
logToFile("=== Startup complete ===");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user