[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:
176
domain/vaultImport/csvExport.test.ts
Normal file
176
domain/vaultImport/csvExport.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { importVaultHostsFromText } from "../vaultImport.ts";
|
||||
import { exportHostsToCsvWithStats } from "./csvExport.ts";
|
||||
import type { Host } from "../models.ts";
|
||||
|
||||
test("CSV exports include a UTF-8 BOM and preserve Chinese text when imported again", () => {
|
||||
const host: Host = {
|
||||
id: "host-1",
|
||||
label: "中文服务器",
|
||||
hostname: "10.0.0.1",
|
||||
username: "root",
|
||||
port: 22,
|
||||
};
|
||||
|
||||
const { csv } = exportHostsToCsvWithStats([host]);
|
||||
|
||||
assert.equal(csv.charCodeAt(0), 0xfeff);
|
||||
assert.deepEqual([...new TextEncoder().encode(csv).slice(0, 3)], [0xef, 0xbb, 0xbf]);
|
||||
|
||||
const imported = importVaultHostsFromText("csv", csv);
|
||||
assert.equal(imported.hosts[0]?.label, host.label);
|
||||
assert.equal(imported.hosts[0]?.hostname, host.hostname);
|
||||
});
|
||||
|
||||
test("CSV round-trips local key authentication and its saved passphrase", () => {
|
||||
const host: Host = {
|
||||
id: "host-key",
|
||||
label: "Key host",
|
||||
hostname: "key.example.com",
|
||||
username: "ubuntu",
|
||||
port: 22,
|
||||
identityFilePaths: ["~/.ssh/id_ed25519"],
|
||||
authMethod: "key",
|
||||
};
|
||||
|
||||
const { csv } = exportHostsToCsvWithStats([host], {
|
||||
keyPassphrases: new Map([["~/.ssh/id_ed25519", "+secret"]]),
|
||||
});
|
||||
assert.equal(csv.includes(",+secret"), false);
|
||||
const imported = importVaultHostsFromText("csv", csv);
|
||||
|
||||
assert.deepEqual(imported.hosts[0]?.identityFilePaths, ["~/.ssh/id_ed25519"]);
|
||||
assert.equal(imported.hosts[0]?.authMethod, "key");
|
||||
assert.equal(imported.hosts[0]?.password, undefined);
|
||||
assert.deepEqual(imported.keyPassphrases, [{
|
||||
hostId: imported.hosts[0]?.id,
|
||||
keyPath: "~/.ssh/id_ed25519",
|
||||
passphrase: "+secret",
|
||||
}]);
|
||||
});
|
||||
|
||||
test("CSV round-trips a referenced Keychain file path and saved passphrase", () => {
|
||||
const host: Host = {
|
||||
id: "host-reference-key",
|
||||
label: "Reference key host",
|
||||
hostname: "reference.example.com",
|
||||
username: "ubuntu",
|
||||
port: 22,
|
||||
identityFileId: "key-reference",
|
||||
identityFilePaths: ["/Users/alice/.ssh/stale"],
|
||||
authMethod: "key",
|
||||
};
|
||||
const keyPath = "/Users/alice/.ssh/id_ed25519";
|
||||
|
||||
const { csv } = exportHostsToCsvWithStats([host], {
|
||||
keyPathsById: new Map([["key-reference", keyPath]]),
|
||||
keyPassphrasesById: new Map([["key-reference", "reference-secret"]]),
|
||||
keyPassphrases: new Map([[keyPath, "stale-side-store-secret"]]),
|
||||
});
|
||||
const imported = importVaultHostsFromText("csv", csv);
|
||||
|
||||
assert.deepEqual(imported.hosts[0]?.identityFilePaths, [keyPath]);
|
||||
assert.deepEqual(imported.keyPassphrases, [{
|
||||
hostId: imported.hosts[0]?.id,
|
||||
keyPath,
|
||||
passphrase: "reference-secret",
|
||||
}]);
|
||||
});
|
||||
|
||||
test("CSV never falls back to path storage for a referenced key", () => {
|
||||
const host: Host = {
|
||||
id: "host-reference-key",
|
||||
label: "Reference key host",
|
||||
hostname: "reference.example.com",
|
||||
username: "ubuntu",
|
||||
port: 22,
|
||||
identityFileId: "key-reference",
|
||||
authMethod: "key",
|
||||
};
|
||||
const keyPath = "/Users/alice/.ssh/id_ed25519";
|
||||
|
||||
const { csv } = exportHostsToCsvWithStats([host], {
|
||||
keyPathsById: new Map([["key-reference", keyPath]]),
|
||||
keyPassphrases: new Map([[keyPath, "stale-side-store-secret"]]),
|
||||
});
|
||||
const imported = importVaultHostsFromText("csv", csv);
|
||||
|
||||
assert.deepEqual(imported.hosts[0]?.identityFilePaths, [keyPath]);
|
||||
assert.deepEqual(imported.keyPassphrases, []);
|
||||
assert.equal(csv.includes("stale-side-store-secret"), false);
|
||||
});
|
||||
|
||||
test("CSV reversibly guards key paths that spreadsheets treat as formulas", () => {
|
||||
const hosts: Host[] = [
|
||||
"-relative-key",
|
||||
"'-literal-key",
|
||||
"__netcatty_csv_keypath_v1__:literal",
|
||||
].map((keyPath, index) => ({
|
||||
id: `host-${index}`,
|
||||
label: `Host ${index}`,
|
||||
hostname: `host-${index}.example.com`,
|
||||
username: "root",
|
||||
port: 22,
|
||||
identityFilePaths: [keyPath],
|
||||
authMethod: "key",
|
||||
}));
|
||||
|
||||
const { csv } = exportHostsToCsvWithStats(hosts);
|
||||
const imported = importVaultHostsFromText("csv", csv);
|
||||
|
||||
assert.deepEqual(imported.hosts.map((host) => host.identityFilePaths?.[0]), [
|
||||
"-relative-key",
|
||||
"'-literal-key",
|
||||
"__netcatty_csv_keypath_v1__:literal",
|
||||
]);
|
||||
});
|
||||
|
||||
test("CSV export never writes credentials from skipped serial hosts", () => {
|
||||
const serialHost: Host = {
|
||||
id: "serial-with-stale-key",
|
||||
label: "Serial with stale key",
|
||||
hostname: "ttyUSB0",
|
||||
protocol: "serial",
|
||||
port: 22,
|
||||
identityFilePaths: ["~/.ssh/id_stale"],
|
||||
authMethod: "key",
|
||||
};
|
||||
const sshHost: Host = {
|
||||
id: "ssh-host",
|
||||
label: "SSH host",
|
||||
hostname: "ssh.example.com",
|
||||
protocol: "ssh",
|
||||
port: 22,
|
||||
};
|
||||
|
||||
const result = exportHostsToCsvWithStats([serialHost, sshHost], {
|
||||
keyPassphrases: new Map([["~/.ssh/id_stale", "must-not-leak"]]),
|
||||
});
|
||||
|
||||
assert.equal(result.exportedCount, 1);
|
||||
assert.equal(result.skippedCount, 1);
|
||||
assert.equal(result.csv.includes(serialHost.label), false);
|
||||
assert.equal(result.csv.includes("id_stale"), false);
|
||||
assert.equal(result.csv.includes("must-not-leak"), false);
|
||||
assert.equal(result.csv.includes(sshHost.hostname), true);
|
||||
});
|
||||
|
||||
test("CSV export skips plugin hosts instead of discarding opaque provider configuration", () => {
|
||||
const pluginHost: Host = {
|
||||
id: "plugin-host",
|
||||
label: "Plugin host",
|
||||
hostname: "com.example.transport.connection",
|
||||
username: "alice",
|
||||
protocol: "plugin:com.example.transport.connection",
|
||||
pluginConnection: {
|
||||
providerId: "com.example.transport.connection",
|
||||
configuration: { endpoint: "opaque://target" },
|
||||
},
|
||||
};
|
||||
const result = exportHostsToCsvWithStats([pluginHost]);
|
||||
assert.equal(result.exportedCount, 0);
|
||||
assert.equal(result.skippedCount, 1);
|
||||
assert.equal(result.csv.includes("opaque://target"), false);
|
||||
});
|
||||
Reference in New Issue
Block a user