[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
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:
283
electron/bridges/systemSshAgent.test.cjs
Normal file
283
electron/bridges/systemSshAgent.test.cjs
Normal file
@@ -0,0 +1,283 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { utils } = require("ssh2");
|
||||
const {
|
||||
prepareSystemSshAgent,
|
||||
resolveIdentityPath,
|
||||
} = require("./systemSshAgent.cjs");
|
||||
|
||||
const TEST_PUBLIC_KEYS = [
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILKxGkTKkraRFbFuzZ2hIJiiRCVecNR1V0Az2YPSHyB1",
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKX948eIUbsCfpX6+wkEedowUaDcg9jZP0rAlCLfiPPY",
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFDTqolh2DHp5OhKylW13C0cZSwJ0pxzAWYoLsZR9VKE",
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGwNYEIS5fjDhPYDAeKd+osXu05BTlVfvlBGTpctoaWj",
|
||||
];
|
||||
let nextTestPublicKey = 0;
|
||||
|
||||
function makePublicKey() {
|
||||
const key = TEST_PUBLIC_KEYS[nextTestPublicKey % TEST_PUBLIC_KEYS.length];
|
||||
nextTestPublicKey += 1;
|
||||
return key;
|
||||
}
|
||||
|
||||
function fakeAgent(publicKeys) {
|
||||
const identities = publicKeys.map((key) => utils.parseKey(key));
|
||||
return {
|
||||
getIdentities(callback) {
|
||||
callback(null, identities);
|
||||
},
|
||||
sign(_key, _data, _options, callback) {
|
||||
callback(null, Buffer.from("signature"));
|
||||
},
|
||||
getStream(callback) {
|
||||
callback(null, "forwarded-stream");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getIdentities(agent) {
|
||||
return new Promise((resolve, reject) => {
|
||||
agent.getIdentities((error, identities) => {
|
||||
if (error) reject(error);
|
||||
else resolve(identities);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("IdentityFile paths expand standard OpenSSH connection tokens", () => {
|
||||
const resolved = resolveIdentityPath(
|
||||
"%d/.ssh/key-%h-%p-%r-%u-%l-%L-%i-%%-%C",
|
||||
{
|
||||
hostname: "server.example.com",
|
||||
port: 2222,
|
||||
username: "deploy",
|
||||
localHostname: "mac.example.net",
|
||||
localUsername: "alice",
|
||||
uid: 501,
|
||||
},
|
||||
);
|
||||
|
||||
assert.match(resolved, /key-server\.example\.com-2222-deploy-alice-mac\.example\.net-mac-501-%-[a-f0-9]{40}$/);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent prioritizes the identity selected by IdentityFile", async () => {
|
||||
const unrelated = makePublicKey();
|
||||
const selected = makePublicKey();
|
||||
const agent = await prepareSystemSshAgent({
|
||||
socketPath: "/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
|
||||
identitiesOnly: false,
|
||||
}, {
|
||||
createAgent: () => fakeAgent([unrelated, selected]),
|
||||
readFile: async () => `${selected} alice@mac\n`,
|
||||
platform: "linux",
|
||||
});
|
||||
|
||||
const identities = await getIdentities(agent);
|
||||
assert.deepEqual(
|
||||
identities.map((key) => key.getPublicSSH().toString("base64")),
|
||||
[selected, unrelated].map((key) => utils.parseKey(key).getPublicSSH().toString("base64")),
|
||||
);
|
||||
await new Promise((resolve, reject) => {
|
||||
agent.getStream((error, stream) => {
|
||||
if (error) reject(error);
|
||||
else {
|
||||
assert.equal(stream, "forwarded-stream");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent excludes unrelated identities for IdentitiesOnly", async () => {
|
||||
const unrelated = makePublicKey();
|
||||
const selected = makePublicKey();
|
||||
const agent = await prepareSystemSshAgent({
|
||||
socketPath: "/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
|
||||
identitiesOnly: true,
|
||||
}, {
|
||||
createAgent: () => fakeAgent([unrelated, selected]),
|
||||
readFile: async () => selected,
|
||||
platform: "linux",
|
||||
});
|
||||
|
||||
const identities = await getIdentities(agent);
|
||||
assert.deepEqual(
|
||||
identities.map((key) => key.getPublicSSH().toString("base64")),
|
||||
[utils.parseKey(selected).getPublicSSH().toString("base64")],
|
||||
);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent filters by a selected vault public key", async () => {
|
||||
const unrelated = makePublicKey();
|
||||
const selected = makePublicKey();
|
||||
const agent = await prepareSystemSshAgent({
|
||||
socketPath: "/tmp/agent.sock",
|
||||
agentPublicKeys: [selected],
|
||||
identitiesOnly: true,
|
||||
}, {
|
||||
createAgent: () => fakeAgent([unrelated, selected]),
|
||||
platform: "linux",
|
||||
});
|
||||
|
||||
const identities = await getIdentities(agent);
|
||||
assert.deepEqual(
|
||||
identities.map((key) => key.getPublicSSH().toString("base64")),
|
||||
[utils.parseKey(selected).getPublicSSH().toString("base64")],
|
||||
);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent asks macOS to load a missing configured identity from Keychain", async () => {
|
||||
const selected = makePublicKey();
|
||||
const sshAddCalls = [];
|
||||
|
||||
await prepareSystemSshAgent({
|
||||
socketPath: "/private/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
|
||||
identitiesOnly: true,
|
||||
useKeychain: true,
|
||||
addKeysToAgent: "yes",
|
||||
}, {
|
||||
createAgent: () => fakeAgent([]),
|
||||
readFile: async () => selected,
|
||||
runSshAdd: async (args) => sshAddCalls.push(args),
|
||||
platform: "darwin",
|
||||
});
|
||||
|
||||
assert.deepEqual(sshAddCalls, [[
|
||||
"--apple-load-keychain",
|
||||
"/Users/alice/.ssh/aws_root",
|
||||
]]);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent does not invoke macOS Keychain loading when the identity is already present", async () => {
|
||||
const selected = makePublicKey();
|
||||
const sshAddCalls = [];
|
||||
|
||||
await prepareSystemSshAgent({
|
||||
socketPath: "/private/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
|
||||
identitiesOnly: true,
|
||||
useKeychain: true,
|
||||
addKeysToAgent: "yes",
|
||||
}, {
|
||||
createAgent: () => fakeAgent([selected]),
|
||||
readFile: async () => selected,
|
||||
runSshAdd: async (args) => sshAddCalls.push(args),
|
||||
platform: "darwin",
|
||||
});
|
||||
|
||||
assert.deepEqual(sshAddCalls, []);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent loads macOS Keychain when only some configured identities are present", async () => {
|
||||
const first = makePublicKey();
|
||||
const second = makePublicKey();
|
||||
const sshAddCalls = [];
|
||||
|
||||
await prepareSystemSshAgent({
|
||||
socketPath: "/private/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/first", "/Users/alice/.ssh/second"],
|
||||
identitiesOnly: true,
|
||||
useKeychain: true,
|
||||
addKeysToAgent: "yes",
|
||||
}, {
|
||||
createAgent: () => fakeAgent([first]),
|
||||
readFile: async (publicKeyPath) => publicKeyPath.endsWith("first.pub") ? first : second,
|
||||
runSshAdd: async (args) => sshAddCalls.push(args),
|
||||
platform: "darwin",
|
||||
});
|
||||
|
||||
assert.deepEqual(sshAddCalls, [[
|
||||
"--apple-load-keychain",
|
||||
"/Users/alice/.ssh/first",
|
||||
"/Users/alice/.ssh/second",
|
||||
]]);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent does not bypass AddKeysToAgent confirmation policies", async () => {
|
||||
const selected = makePublicKey();
|
||||
const sshAddCalls = [];
|
||||
|
||||
await prepareSystemSshAgent({
|
||||
socketPath: "/private/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
|
||||
identitiesOnly: true,
|
||||
useKeychain: true,
|
||||
addKeysToAgent: "confirm",
|
||||
}, {
|
||||
createAgent: () => fakeAgent([]),
|
||||
readFile: async () => selected,
|
||||
runSshAdd: async (args) => sshAddCalls.push(args),
|
||||
platform: "darwin",
|
||||
});
|
||||
|
||||
assert.deepEqual(sshAddCalls, []);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent reports a clear error when strict selection has no readable public key", async () => {
|
||||
await assert.rejects(
|
||||
prepareSystemSshAgent({
|
||||
socketPath: "/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/missing"],
|
||||
identitiesOnly: true,
|
||||
}, {
|
||||
createAgent: () => fakeAgent([makePublicKey()]),
|
||||
readFile: async () => { throw new Error("ENOENT"); },
|
||||
platform: "linux",
|
||||
}),
|
||||
(error) => {
|
||||
assert.equal(error.code, "ERR_SSH_AGENT_IDENTITY_SELECTOR_UNAVAILABLE");
|
||||
assert.match(error.message, /missing\.pub/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent reports every missing selector in strict multi-key mode", async () => {
|
||||
const selected = makePublicKey();
|
||||
await assert.rejects(
|
||||
prepareSystemSshAgent({
|
||||
socketPath: "/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/available", "/Users/alice/.ssh/missing"],
|
||||
identitiesOnly: true,
|
||||
}, {
|
||||
createAgent: () => fakeAgent([selected]),
|
||||
readFile: async (publicKeyPath) => {
|
||||
if (publicKeyPath.endsWith("available.pub")) return selected;
|
||||
throw new Error("ENOENT");
|
||||
},
|
||||
platform: "linux",
|
||||
}),
|
||||
(error) => {
|
||||
assert.equal(error.code, "ERR_SSH_AGENT_IDENTITY_SELECTOR_UNAVAILABLE");
|
||||
assert.match(error.message, /missing\.pub/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("prepareSystemSshAgent falls back to all identities and still invokes macOS ssh-add without a .pub file", async () => {
|
||||
const loaded = makePublicKey();
|
||||
const sshAddCalls = [];
|
||||
const agent = await prepareSystemSshAgent({
|
||||
socketPath: "/private/tmp/agent.sock",
|
||||
identityFilePaths: ["/Users/alice/.ssh/aws_root"],
|
||||
identitiesOnly: false,
|
||||
useKeychain: true,
|
||||
addKeysToAgent: "yes",
|
||||
}, {
|
||||
createAgent: () => fakeAgent([loaded]),
|
||||
readFile: async () => { throw new Error("ENOENT"); },
|
||||
runSshAdd: async (args) => sshAddCalls.push(args),
|
||||
platform: "darwin",
|
||||
});
|
||||
|
||||
assert.deepEqual(sshAddCalls, [[
|
||||
"--apple-load-keychain",
|
||||
"/Users/alice/.ssh/aws_root",
|
||||
]]);
|
||||
assert.equal((await getIdentities(agent)).length, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user