[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:
146
domain/vaultImport/csvExport.ts
Normal file
146
domain/vaultImport/csvExport.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import type { Host } from '../models';
|
||||
import { isPluginHostProtocol } from '../pluginConnection';
|
||||
import { encodeCsvKeyPath, encodeCsvPassphrase } from './csvCredentialFields';
|
||||
|
||||
const UTF8_BOM = "\uFEFF";
|
||||
|
||||
export interface VaultCsvTemplateOptions {
|
||||
includeExampleRows?: boolean;
|
||||
}
|
||||
|
||||
export interface VaultCsvExportOptions {
|
||||
keyPassphrases?: ReadonlyMap<string, string>;
|
||||
keyPassphrasesById?: ReadonlyMap<string, string>;
|
||||
keyPathsById?: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
export const resolveVaultCsvHostKeyPath = (
|
||||
host: Host,
|
||||
options: VaultCsvExportOptions = {},
|
||||
): string => {
|
||||
const referencedPath = host.identityFileId
|
||||
? options.keyPathsById?.get(host.identityFileId)?.trim()
|
||||
: undefined;
|
||||
return referencedPath
|
||||
|| host.identityFilePaths?.find((path) => path.trim())?.trim()
|
||||
|| "";
|
||||
};
|
||||
|
||||
export const getVaultCsvTemplate = (
|
||||
opts: VaultCsvTemplateOptions = {},
|
||||
): string => {
|
||||
const includeExampleRows = opts.includeExampleRows !== false;
|
||||
const header = ["Groups", "Label", "Tags", "Notes", "Hostname/IP", "Protocol", "Port", "Username", "Password", "KeyPath", "Passphrase"];
|
||||
const rows: string[][] = [header];
|
||||
if (includeExampleRows) {
|
||||
rows.push(["Project/Dev", "Web Server (dev)", "dev,web", "Dev web tier", "192.168.1.10", "ssh", "22", "root", "", "~/.ssh/id_ed25519", ""]);
|
||||
rows.push(["Project/Prod", "Web Server (prod)", "prod,web", "Production", "server-a.example.com", "ssh", "22", "ubuntu", "", "", ""]);
|
||||
rows.push(["Database", "DB", "db,mysql", "MySQL primary", "db.example.com", "ssh", "4567", "admin", "", "", ""]);
|
||||
}
|
||||
|
||||
const escapeCsv = (value: string) => {
|
||||
if (value.includes('"')) value = value.replace(/"/g, '""');
|
||||
if (/[",\r\n]/.test(value)) return `"${value}"`;
|
||||
return value;
|
||||
};
|
||||
|
||||
return rows.map((r) => r.map((c) => escapeCsv(c)).join(",")).join("\r\n") + "\r\n";
|
||||
};
|
||||
|
||||
const exportHostsToCsv = (hosts: Host[], options: VaultCsvExportOptions): string => {
|
||||
const header = ["Groups", "Label", "Tags", "Notes", "Hostname/IP", "Protocol", "Port", "Username", "Password", "KeyPath", "Passphrase"];
|
||||
const rows: string[][] = [header];
|
||||
|
||||
const escapeCsv = (value: string, skipFormulaGuard = false) => {
|
||||
// Prevent CSV formula injection by prefixing dangerous characters with a single quote
|
||||
// These characters can be interpreted as formulas by spreadsheet applications
|
||||
// Skip for password fields to preserve credentials verbatim for round-trip
|
||||
if (!skipFormulaGuard && /^[=+\-@\t\r]/.test(value)) {
|
||||
value = "'" + value;
|
||||
}
|
||||
if (value.includes('"')) value = value.replace(/"/g, '""');
|
||||
if (/[",\r\n]/.test(value)) return `"${value}"`;
|
||||
return value;
|
||||
};
|
||||
|
||||
// Filter out transports the legacy CSV format cannot represent without
|
||||
// silently discarding protocol-owned configuration.
|
||||
// Note: mosh-enabled hosts are exported as SSH (losing mosh flag) rather than being skipped,
|
||||
// since exporting partial data is better than losing the entire host entry
|
||||
const isUnsupported = (h: Host) => h.protocol === "serial" || isPluginHostProtocol(h.protocol);
|
||||
const exportableHosts = hosts.filter((h) => !isUnsupported(h));
|
||||
|
||||
// Helper to bracket IPv6 addresses for CSV export
|
||||
// IPv6 addresses contain colons which would be misinterpreted as port separators on import
|
||||
const formatHostname = (hostname: string): string => {
|
||||
// Check if it looks like an IPv6 address (contains colons but not already bracketed)
|
||||
if (hostname.includes(":") && !hostname.startsWith("[")) {
|
||||
return `[${hostname}]`;
|
||||
}
|
||||
return hostname;
|
||||
};
|
||||
|
||||
for (const host of exportableHosts) {
|
||||
// For telnet hosts, use telnet-specific port and username
|
||||
const isTelnet = host.protocol === "telnet";
|
||||
const effectivePort = isTelnet
|
||||
? (host.telnetPort ?? host.port ?? 23)
|
||||
: (host.port ?? 22);
|
||||
const effectiveUsername = isTelnet
|
||||
? (host.telnetUsername ?? host.username ?? "")
|
||||
: (host.username ?? "");
|
||||
const keyPath = resolveVaultCsvHostKeyPath(host, options);
|
||||
const passphrase = keyPath
|
||||
? (
|
||||
host.identityFileId
|
||||
? (options.keyPassphrasesById?.get(host.identityFileId) ?? "")
|
||||
: (options.keyPassphrases?.get(keyPath) ?? "")
|
||||
)
|
||||
: "";
|
||||
|
||||
rows.push([
|
||||
host.group ?? "",
|
||||
host.label ?? "",
|
||||
(host.tags ?? []).join(","),
|
||||
host.notes ?? "",
|
||||
formatHostname(host.hostname),
|
||||
host.protocol ?? "ssh",
|
||||
String(effectivePort),
|
||||
effectiveUsername,
|
||||
host.password ?? "",
|
||||
encodeCsvKeyPath(keyPath),
|
||||
encodeCsvPassphrase(passphrase),
|
||||
]);
|
||||
}
|
||||
|
||||
const passwordColIdx = header.indexOf("Password");
|
||||
const keyPathColIdx = header.indexOf("KeyPath");
|
||||
const passphraseColIdx = header.indexOf("Passphrase");
|
||||
return rows.map((r, rowIdx) => r.map((c, i) => escapeCsv(
|
||||
c,
|
||||
rowIdx > 0 && (i === passwordColIdx || i === keyPathColIdx || i === passphraseColIdx),
|
||||
)).join(",")).join("\r\n") + "\r\n";
|
||||
};
|
||||
|
||||
interface ExportHostsResult {
|
||||
csv: string;
|
||||
exportedCount: number;
|
||||
skippedCount: number;
|
||||
}
|
||||
|
||||
export const exportHostsToCsvWithStats = (
|
||||
hosts: Host[],
|
||||
options: VaultCsvExportOptions = {},
|
||||
): ExportHostsResult => {
|
||||
// Mosh hosts intentionally degrade to SSH, but namespaced plugin protocols
|
||||
// must be skipped because CSV has no field for their opaque configuration.
|
||||
const isUnsupported = (h: Host) => h.protocol === "serial" || isPluginHostProtocol(h.protocol);
|
||||
const skippedHosts = hosts.filter((h) => isUnsupported(h));
|
||||
const exportableHosts = hosts.filter((h) => !isUnsupported(h));
|
||||
|
||||
return {
|
||||
csv: UTF8_BOM + exportHostsToCsv(exportableHosts, options),
|
||||
exportedCount: exportableHosts.length,
|
||||
skippedCount: skippedHosts.length,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user