release-2.4.1 (#921)

* chore(deps-dev): bump the dev-minor-updates group across 1 directory with 12 updates (#910)

Bumps the dev-minor-updates group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.1` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.3.6` | `1.4.1` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.1` |
| [cytoscape](https://github.com/cytoscape/cytoscape.js) | `3.33.4` | `3.34.0` |
| [electron](https://github.com/electron/electron) | `42.2.0` | `42.4.1` |
| [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) | `26.8.1` | `26.15.3` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.16.0` | `1.20.0` |
| [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.4.3` | `1.6.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.79.0` |
| [sharp](https://github.com/lovell/sharp) | `0.34.5` | `0.35.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.61.1` |



Updates `@radix-ui/react-select` from 2.2.6 to 2.3.1
- [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-slider` from 1.3.6 to 1.4.1
- [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-slot` from 1.2.5 to 1.3.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.1
- [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 `cytoscape` from 3.33.4 to 3.34.0
- [Release notes](https://github.com/cytoscape/cytoscape.js/releases)
- [Commits](https://github.com/cytoscape/cytoscape.js/compare/v3.33.4...v3.34.0)

Updates `electron` from 42.2.0 to 42.4.1
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v42.2.0...v42.4.1)

Updates `electron-builder` from 26.8.1 to 26.15.3
- [Release notes](https://github.com/electron-userland/electron-builder/releases)
- [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md)
- [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.15.3/packages/electron-builder)

Updates `lucide-react` from 1.16.0 to 1.20.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react)

Updates `radix-ui` from 1.4.3 to 1.6.0
- [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-hook-form` from 7.76.1 to 7.79.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.79.0)

Updates `sharp` from 0.34.5 to 0.35.1
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.1)

Updates `typescript-eslint` from 8.60.0 to 8.61.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: cytoscape
  dependency-version: 3.34.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: electron
  dependency-version: 42.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: electron-builder
  dependency-version: 26.15.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: lucide-react
  dependency-version: 1.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: radix-ui
  dependency-version: 1.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.79.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: sharp
  dependency-version: 0.35.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: typescript-eslint
  dependency-version: 8.61.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump node in /docker in the docker-major-updates group (#914)

Bumps the docker-major-updates group in /docker with 1 update: node.


Updates `node` from 24-slim to 26-slim

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26-slim
  dependency-type: direct:production
  dependency-group: docker-major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* ci(deps): bump the github-actions group with 2 updates (#915)

Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

Updates `actions/setup-node` from 4 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-version: '6'
  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): bump the prod-minor-updates group across 1 directory with 5 updates (#916)

Bumps the prod-minor-updates group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [axios](https://github.com/axios/axios) | `1.17.0` | `1.18.0` |
| [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `12.10.0` | `12.11.1` |
| [body-parser](https://github.com/expressjs/body-parser) | `2.2.2` | `2.3.0` |
| [multer](https://github.com/expressjs/multer) | `2.1.1` | `2.2.0` |
| [undici](https://github.com/nodejs/undici) | `8.4.0` | `8.5.0` |



Updates `axios` from 1.17.0 to 1.18.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0)

Updates `better-sqlite3` from 12.10.0 to 12.11.1
- [Release notes](https://github.com/WiseLibs/better-sqlite3/releases)
- [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.10.0...v12.11.1)

Updates `body-parser` from 2.2.2 to 2.3.0
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0)

Updates `multer` from 2.1.1 to 2.2.0
- [Release notes](https://github.com/expressjs/multer/releases)
- [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/expressjs/multer/compare/v2.1.1...v2.2.0)

Updates `undici` from 8.4.0 to 8.5.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.4.0...v8.5.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: better-sqlite3
  dependency-version: 12.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: body-parser
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: multer
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: undici
  dependency-version: 8.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat: add unlink button for dual auth

* fix: general bug fixes

* fix: general qol/small feature additions

* fix: 100mb max upload size

* fix: terminal syntax not handling carriage returns or shell prompt lines properly

* feat: continued general improvements

* fix: terminal outputting success right after folder path

* chore: update release notes

* feat: continued fixes and improvements

* chore: lint, format, and bump version to 2.4.1

* chore: sync Crowdin translations for 2.4.1

* fix: rebase dev branch onto main before Crowdin download

* fix: use merge instead of rebase to sync main before Crowdin

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Luke Gustafson
2026-06-21 15:55:41 -05:00
committed by GitHub
parent 8072b4ede9
commit d1a8dcf3c6
165 changed files with 87244 additions and 60766 deletions
+131 -119
View File
@@ -5,7 +5,7 @@ import ssh2Pkg, {
type PseudoTtyOptions,
} from "ssh2";
const { Client, utils: ssh2Utils } = ssh2Pkg;
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
import { buildSSHAlgorithms } from "../utils/ssh-algorithms.js";
import axios from "axios";
import { getDb } from "../database/db/index.js";
import { hosts } from "../database/db/schema.js";
@@ -30,6 +30,7 @@ import {
waitForTmuxSession,
} from "./tmux-helper.js";
import { MemoryAgent, performPortKnocking } from "./terminal-auth-helpers.js";
import { isWindowsSftpPath, sftpPathToLocalPath } from "./transfer-paths.js";
interface ConnectToHostData {
cols: number;
@@ -187,9 +188,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 isDuplicateConnDiscarded = false;
let warpgateAuthPromptSent = false;
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
let isAwaitingAuthCredentials = false;
@@ -237,7 +237,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (currentSessionId) {
const session = sessionManager.getSession(currentSessionId);
if (session?.isConnected) {
sessionManager.detachWs(currentSessionId);
// Only detach if this WS is still the one attached to the session.
// If a refresh reconnected and reattached a new WS before this close
// event fired, we must not clobber that new attachment.
if (session.attachedWs === ws || session.attachedWs === null) {
sessionManager.detachWs(currentSessionId);
}
} else {
sessionManager.destroySession(currentSessionId);
currentSessionId = null;
@@ -446,15 +451,80 @@ wss.on("connection", async (ws: WebSocket, req) => {
break;
case "get_cwd": {
const activeStream =
sessionManager.getSession(currentSessionId)?.sshStream ?? sshStream;
if (!activeStream) {
const activeConn =
sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn;
if (!activeConn) {
ws.send(JSON.stringify({ type: "cwd", path: "/" }));
break;
}
cwdPending = true;
cwdBuffer = "";
activeStream.write('\x15a=TERMIX_CWD; echo "$a:$(pwd)"\r');
activeConn.exec("pwd", (err, execStream) => {
if (err) {
ws.send(JSON.stringify({ type: "cwd", path: "/" }));
return;
}
let stdout = "";
execStream.on("data", (chunk: Buffer) => {
stdout += chunk.toString("utf-8");
});
execStream.stderr.on("data", () => {});
execStream.on("close", () => {
const cwd = stdout.trim() || "/";
const attachedWs =
sessionManager.getSession(currentSessionId)?.attachedWs ?? ws;
if (attachedWs.readyState === WebSocket.OPEN) {
attachedWs.send(JSON.stringify({ type: "cwd", path: cwd }));
}
});
});
break;
}
case "open_file_in_editor": {
const { path: requestedPath } = data as { path: string };
const activeConn =
sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn;
if (!activeConn || !requestedPath) {
ws.send(
JSON.stringify({
type: "open_file_in_editor",
path: requestedPath || "/",
}),
);
break;
}
const escapedPath = requestedPath.replace(/'/g, "'\\''");
activeConn.exec(
`realpath '${escapedPath}' 2>/dev/null || echo '${escapedPath}'`,
(err, execStream) => {
if (err) {
ws.send(
JSON.stringify({
type: "open_file_in_editor",
path: requestedPath,
}),
);
return;
}
let stdout = "";
execStream.on("data", (chunk: Buffer) => {
stdout += chunk.toString("utf-8");
});
execStream.stderr.on("data", () => {});
execStream.on("close", () => {
const resolvedPath = stdout.trim() || requestedPath;
const attachedWs =
sessionManager.getSession(currentSessionId)?.attachedWs ?? ws;
if (attachedWs.readyState === WebSocket.OPEN) {
attachedWs.send(
JSON.stringify({
type: "open_file_in_editor",
path: resolvedPath,
}),
);
}
});
},
);
break;
}
@@ -990,7 +1060,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
);
}
if (!hostConfig.useSocks5 && resolvedHostData.useSocks5) {
if (resolvedHostData.useSocks5) {
hostConfig.useSocks5 = resolvedHostData.useSocks5;
hostConfig.socks5Host = resolvedHostData.socks5Host;
hostConfig.socks5Port = resolvedHostData.socks5Port;
@@ -1134,27 +1204,43 @@ wss.on("connection", async (ws: WebSocket, req) => {
!existingSession.sshStream.destroyed &&
existingSession.sshConn !== sshConn
) {
const reusedSessionId = currentSessionId;
sshLogger.info(
"Reusing existing live session after duplicate connectToHost, closing new SSH conn",
{
operation: "terminal_reuse_existing_session",
sessionId: currentSessionId,
sessionId: reusedSessionId,
tabInstanceId,
userId,
},
);
// Null out currentSessionId before ending the duplicate connection so
// the sshConn "close" handler does not destroy the reused session.
// Set isDuplicateConnDiscarded so the close handler does not send a
// "disconnected" message to the new WS that is now attached to the live session.
// Null out currentSessionId before ending the duplicate connection so
// the sshConn "close" handler does not destroy the reused session.
// Set isDuplicateConnDiscarded so the close handler exits without
// sending a "disconnected" message to the new WS.
currentSessionId = null;
isDuplicateConnDiscarded = true;
clearTimeout(connectionTimeout);
try {
sshConn?.end();
} catch {
/* ignore */
}
sshConn = null;
sshStream = null;
// Point this WS handler's closure at the live session so the input
// handler can forward keystrokes via currentSessionId.
currentSessionId = reusedSessionId;
sshStream = existingSession.sshStream;
sshConn = existingSession.sshConn;
isConnecting = false;
isConnected = true;
sessionManager.attachWs(currentSessionId, userId, ws, tabInstanceId);
sessionManager.attachWs(reusedSessionId, userId, ws, tabInstanceId);
const buffered = sessionManager.getBuffer(existingSession);
if (buffered) {
@@ -1163,20 +1249,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws.send(
JSON.stringify({
type: "sessionCreated",
sessionId: currentSessionId,
sessionId: reusedSessionId,
}),
);
ws.send(
JSON.stringify({
type: "sessionAttached",
sessionId: currentSessionId,
sessionId: reusedSessionId,
}),
);
ws.send(
JSON.stringify({ type: "connected", message: "Session reattached" }),
);
cleanupAuthState(connectionTimeout);
return;
}
@@ -1350,44 +1434,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
const boundSessionId = currentSessionId;
const CWD_SENTINEL = "TERMIX_CWD:";
stream.on("data", (data: Buffer) => {
try {
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;
}
}
const utf8String = data.toString("utf-8");
if (!utf8String) return;
@@ -1475,7 +1524,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
const runPostShellCommands = (delay: number) => {
setTimeout(() => {
if (initialPath && initialPath.trim() !== "") {
const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}"\r`;
let cdCommand: string;
if (isWindowsSftpPath(initialPath)) {
const winPath = sftpPathToLocalPath(initialPath);
const escaped = winPath.replace(/"/g, '""');
cdCommand = `cd "${escaped}"\r`;
} else {
cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}"\r`;
}
stream.write(cdCommand);
}
if (executeCommand && executeCommand.trim() !== "") {
@@ -1543,27 +1599,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
}),
);
runPostShellCommands(0);
} else if (detection.sessions.length === 1) {
attachOrCreateTmuxSession(stream, detection.sessions[0].name);
const sessionName = detection.sessions[0].name;
const session = sessionManager.getSession(boundSessionId);
if (session) {
session.tmuxSessionName = sessionName;
}
sshLogger.info("Auto-attached to existing tmux session", {
operation: "tmux_auto_attach",
sessionName,
hostId: id,
});
ws.send(
JSON.stringify({
type: "tmux_session_attached",
sessionName,
}),
);
// Reattaching to existing session -- don't re-run
// initialPath/executeCommand since the session already
// has its own state
} else {
sshLogger.info(
"Multiple tmux sessions found, sending list to frontend",
@@ -1910,6 +1945,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
hostId: id,
});
if (isDuplicateConnDiscarded) {
cleanupAuthState(connectionTimeout);
return;
}
if (isAwaitingAuthCredentials) {
if (currentSessionId) {
sessionManager.destroySession(currentSessionId);
@@ -1991,6 +2031,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
resolvedCredentials as unknown as Parameters<
typeof sshAuthManager.handleKeyboardInteractive
>[5],
hostConfig,
);
isKeyboardInteractive = sshAuthManager.context.isKeyboardInteractive;
@@ -2008,19 +2049,24 @@ wss.on("connection", async (ws: WebSocket, req) => {
const hostKeepaliveInterval = hostConfig.terminalConfig?.keepaliveInterval;
const hostKeepaliveCountMax = hostConfig.terminalConfig?.keepaliveCountMax;
// Pre-fetch the stored host key before connect so the verifier callback
// runs synchronously during SSH key exchange, avoiding LoginGraceTime
// expiry on slow connections (especially through jump host tunnels).
const preloadedHostData = await SSHHostKeyVerifier.preloadHostData(id);
const connectConfig: Record<string, unknown> = {
host: ip,
port,
username,
tryKeyboard:
resolvedCredentials.authType !== "none" &&
resolvedCredentials.authType !== "tailscale",
tryKeyboard: resolvedCredentials.authType !== "tailscale",
keepaliveInterval:
typeof hostKeepaliveInterval === "number"
? hostKeepaliveInterval * 1000
? Math.max(5000, hostKeepaliveInterval * 1000)
: 30000,
keepaliveCountMax:
typeof hostKeepaliveCountMax === "number" ? hostKeepaliveCountMax : 3,
typeof hostKeepaliveCountMax === "number"
? Math.max(1, hostKeepaliveCountMax)
: 5,
readyTimeout: 120000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
@@ -2032,6 +2078,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws,
userId,
false,
preloadedHostData,
),
env: {
TERM: "xterm-256color",
@@ -2045,51 +2092,16 @@ wss.on("connection", async (ws: WebSocket, req) => {
LC_COLLATE: "en_US.UTF-8",
COLORTERM: "truecolor",
},
algorithms: {
kex: [
"curve25519-sha256",
"curve25519-sha256@libssh.org",
"ecdh-sha2-nistp521",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp256",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group18-sha512",
"diffie-hellman-group17-sha512",
"diffie-hellman-group16-sha512",
"diffie-hellman-group15-sha512",
"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"],
},
algorithms: buildSSHAlgorithms(
hostConfig.terminalConfig?.allowLegacyAlgorithms !== false,
),
};
if (
resolvedCredentials.authType === "none" ||
resolvedCredentials.authType === "tailscale"
) {
// Tailscale SSH and "none" auth: daemon handles authorization, no credentials needed
// Tailscale SSH and "none": no static credentials needed
} else if (resolvedCredentials.authType === "password") {
if (!resolvedCredentials.password) {
sshLogger.error(