[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
"use strict";
/**
* Password-only auth must not probe default ~/.ssh keys (issues #266 / #2079).
* Jump hosts and SFTP share buildAuthHandler; a wrong host password used to
* look fine on direct terminal (startSession still fell back to id_rsa) while
* ProxyJump / SFTP failed after password rejection alone.
*/
const test = require("node:test");
const assert = require("node:assert/strict");
const { buildAuthHandler } = require("./sshAuthHelper.cjs");
const DEFAULT_KEYS = [
{
keyName: "id_ed25519",
keyPath: "/tmp/id_ed25519",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\ned\n-----END OPENSSH PRIVATE KEY-----\n",
},
{
keyName: "id_rsa",
keyPath: "/tmp/id_rsa",
privateKey: "-----BEGIN RSA PRIVATE KEY-----\nrsa\n-----END RSA PRIVATE KEY-----\n",
},
];
/**
* Walk the ssh2-style authHandler and collect method labels.
* Object methods expose type (and sometimes username/key); string methods
* come from the simple ordered-list path (e.g. password-only).
*/
function collectAuthMethods(authHandler, maxSteps = 16) {
const labels = [];
let methodsLeft = null;
for (let i = 0; i < maxSteps; i += 1) {
let offered = null;
authHandler(methodsLeft, false, (method) => {
offered = method;
});
if (offered == null || offered === false) break;
if (typeof offered === "string") {
labels.push(offered);
} else if (offered && typeof offered === "object") {
labels.push(offered.type || "unknown");
} else {
break;
}
// Keep all common methods available so the handler can walk its full list.
methodsLeft = ["publickey", "password", "keyboard-interactive", "agent"];
}
return labels;
}
test("buildAuthHandler password-only does not offer default SSH keys (#2079)", () => {
const auth = buildAuthHandler({
password: "wrong-or-stale-secret",
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: false,
});
const labels = collectAuthMethods(auth.authHandler);
assert.ok(labels.includes("password"), `expected password; got ${labels.join(",")}`);
assert.ok(labels.includes("keyboard-interactive"), `expected KI; got ${labels.join(",")}`);
assert.equal(
labels.includes("publickey"),
false,
`password-only must not probe default keys; offered=${labels.join(",")}`,
);
assert.equal(labels.includes("agent"), false);
});
test("buildAuthHandler password-only still fires onAuthAttempt for jump/SFTP progress", () => {
const attempts = [];
const auth = buildAuthHandler({
password: "secret",
username: "root",
// Default keys on disk used to force the dynamic path for progress only;
// after #2079 the simple ordered path must still report attempts.
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: false,
onAuthAttempt: (label) => attempts.push(label),
});
collectAuthMethods(auth.authHandler);
assert.ok(
attempts.some((label) => label === "password" || label.includes("password")),
`expected password attempt callback; got ${attempts.join(" | ")}`,
);
assert.ok(
attempts.some((label) => label.includes("keyboard-interactive") || label.includes("exhausted")),
`expected KI or exhaustion callback; got ${attempts.join(" | ")}`,
);
assert.equal(
attempts.some((label) => /id_rsa|id_ed25519|default/.test(label)),
false,
`must not report default-key probes; got ${attempts.join(" | ")}`,
);
});
test("buildAuthHandler automatic mode tries agent and default keys before password", () => {
const auth = buildAuthHandler({
authMethod: "auto",
password: "saved-secret",
username: "root",
defaultKeys: DEFAULT_KEYS,
sshAgentSocketOverride: "/tmp/ssh-agent.sock",
});
const labels = collectAuthMethods(auth.authHandler);
const agentIndex = labels.indexOf("agent");
const keyIndex = labels.indexOf("publickey");
const passwordIndex = labels.indexOf("password");
assert.ok(agentIndex >= 0, `expected agent; got ${labels.join(",")}`);
assert.ok(keyIndex >= 0, `expected publickey; got ${labels.join(",")}`);
assert.ok(passwordIndex >= 0, `expected password; got ${labels.join(",")}`);
assert.ok(agentIndex < passwordIndex, `agent should precede password; got ${labels.join(",")}`);
assert.ok(keyIndex < passwordIndex, `default key should precede password; got ${labels.join(",")}`);
assert.ok(
labels.every((label, index) => label !== "publickey" || index < passwordIndex),
`all default keys should precede password; got ${labels.join(",")}`,
);
});
test("buildAuthHandler automatic mode skips an unavailable validated agent", () => {
const auth = buildAuthHandler({
authMethod: "auto",
password: "saved-secret",
username: "root",
defaultKeys: DEFAULT_KEYS,
sshAgentSocketOverride: null,
});
const labels = collectAuthMethods(auth.authHandler);
assert.equal(labels.includes("agent"), false, labels.join(","));
assert.equal(labels.includes("publickey"), true, labels.join(","));
assert.equal(labels.includes("password"), true, labels.join(","));
});
test("buildAuthHandler with no credentials still offers default keys", () => {
const auth = buildAuthHandler({
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: false,
});
const labels = collectAuthMethods(auth.authHandler);
assert.ok(
labels.includes("publickey"),
`expected default-key fallback when no explicit auth; offered=${labels.join(",")}`,
);
});
test("buildAuthHandler key+password still allows default key fallback after user key", () => {
const auth = buildAuthHandler({
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nuser\n-----END OPENSSH PRIVATE KEY-----\n",
password: "also-have-password",
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: false,
});
const labels = collectAuthMethods(auth.authHandler);
assert.ok(labels.includes("publickey"), `expected publickey; offered=${labels.join(",")}`);
assert.ok(labels.includes("password"), `expected password; offered=${labels.join(",")}`);
// user key + at least one default key => multiple publickey offers
const publickeyCount = labels.filter((l) => l === "publickey").length;
assert.ok(
publickeyCount >= 2,
`key auth may still fall back to default keys; offered=${labels.join(",")}`,
);
});
test("buildAuthHandler explicit key mode uses only the selected key and stated password fallback", () => {
const auth = buildAuthHandler({
authMethod: "key",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nuser\n-----END OPENSSH PRIVATE KEY-----\n",
password: "fallback-password",
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: true,
});
const labels = collectAuthMethods(auth.authHandler);
assert.equal(labels.filter((label) => label === "publickey").length, 1, labels.join(","));
assert.equal(labels.includes("agent"), false, labels.join(","));
assert.equal(labels.includes("password"), true, labels.join(","));
});
test("buildAuthHandler explicit key mode never substitutes an unrelated default key", () => {
const auth = buildAuthHandler({
authMethod: "key",
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: true,
});
const labels = collectAuthMethods(auth.authHandler);
assert.equal(labels.includes("publickey"), false, labels.join(","));
assert.equal(labels.includes("agent"), false, labels.join(","));
assert.equal(auth.privateKey, null);
});
test("buildAuthHandler explicit password mode ignores an agent and default keys", () => {
const auth = buildAuthHandler({
authMethod: "password",
password: "host-password",
agent: "/tmp/agent.sock",
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: true,
});
const labels = collectAuthMethods(auth.authHandler);
assert.equal(labels.includes("agent"), false, labels.join(","));
assert.equal(labels.includes("publickey"), false, labels.join(","));
assert.equal(labels.includes("password"), true, labels.join(","));
});
test("buildAuthHandler explicit password mode never submits an empty saved password", () => {
const auth = buildAuthHandler({
authMethod: "password",
password: undefined,
agent: "/tmp/agent.sock",
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: true,
});
const labels = collectAuthMethods(auth.authHandler);
assert.equal(labels.includes("password"), false, labels.join(","));
assert.equal(labels.includes("agent"), false, labels.join(","));
assert.equal(labels.includes("publickey"), false, labels.join(","));
assert.equal(labels.includes("keyboard-interactive"), true, labels.join(","));
});
for (const authMethod of ["password", "key", "certificate"]) {
test(`buildAuthHandler explicit ${authMethod} mode ignores unlocked default keys`, () => {
const auth = buildAuthHandler({
authMethod,
password: "fallback-password",
privateKey: authMethod === "password" ? undefined : "-----BEGIN OPENSSH PRIVATE KEY-----\nselected\n-----END OPENSSH PRIVATE KEY-----\n",
username: "root",
allowAgentFallback: false,
unlockedEncryptedKeys: [{
keyName: "id_unrelated",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nunrelated\n-----END OPENSSH PRIVATE KEY-----\n",
passphrase: "key-pass",
}],
});
const labels = collectAuthMethods(auth.authHandler);
assert.equal(
labels.filter((label) => label === "publickey").length,
authMethod === "password" ? 0 : 1,
labels.join(","),
);
});
}
test("buildAuthHandler password-only may still attach unlocked encrypted keys for jump retry", () => {
const auth = buildAuthHandler({
password: "host-password",
username: "root",
defaultKeys: DEFAULT_KEYS,
allowAgentFallback: false,
unlockedEncryptedKeys: [{
keyName: "id_encrypted",
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nenc\n-----END OPENSSH PRIVATE KEY-----\n",
passphrase: "key-pass",
}],
});
const labels = collectAuthMethods(auth.authHandler);
assert.ok(labels.includes("password"), `offered=${labels.join(",")}`);
assert.ok(
labels.includes("publickey"),
`jump-chain retry may still offer unlocked keys; offered=${labels.join(",")}`,
);
});