export type FieldGroup = | "connection" | "notes" | "tags" | "proxy" | "jumpHosts" | "quickActions" | "featureFlags" | "advanced"; export interface ExportPayload { version?: string; exportedAt?: string; credentials?: Record[]; hosts: Record[]; } export const FIELD_GROUP_KEYS: Record = { connection: [ "connectionType", "name", "ip", "port", "username", "folder", "domain", "security", "ignoreCert", ], notes: ["notes"], tags: ["tags", "pin"], proxy: [ "useSocks5", "socks5Host", "socks5Port", "socks5Username", "socks5ProxyChain", ], jumpHosts: ["jumpHosts"], quickActions: ["quickActions"], featureFlags: [ "enableTerminal", "enableCommandHistory", "enableTerminalToolbar", "enableAiAssistant", "enableFileManager", "enableDocker", "enableWebUi", "enableProxmox", "enableTmuxMonitor", "showTerminalInSidebar", "showFileManagerInSidebar", "showTunnelInSidebar", "showDockerInSidebar", "showServerStatsInSidebar", "defaultPath", "forceKeyboardInteractive", ], advanced: [ "statsConfig", "dockerConfig", "webUiConfig", "proxmoxConfig", "terminalConfig", "guacamoleConfig", ], }; export const SECRET_KEYS = [ "password", "key", "keyPassword", "sudoPassword", "socks5Password", ]; const CREDENTIAL_KEYS = [ ...SECRET_KEYS, "authType", "keyType", "credentialAlias", "credentialId", "overrideCredentialUsername", ]; const TUPLE_KEYS = ["name", "ip", "port", "username", "connectionType"]; const NESTED_SECRETS: { container: string; field: string }[] = [ { container: "guacamoleConfig", field: "gateway-password" }, ]; const NESTED_SECRET_ARRAYS: { container: string; field: string }[] = [ { container: "socks5ProxyChain", field: "password" }, ]; function nestedSecret( host: Record, container: string, field: string, ): Record | null { const blob = host[container]; if (!blob || typeof blob !== "object") return null; const record = blob as Record; return field in record ? record : null; } export function hostKey(host: Record): string { return JSON.stringify(TUPLE_KEYS.map((k) => String(host[k] ?? ""))); } export function buildExportPayload( raw: ExportPayload, selected: Set | null, groups: Set, withCredentials: boolean, ): ExportPayload { const allowed = new Set([ "exportId", ...CREDENTIAL_KEYS, ...FIELD_GROUP_KEYS.connection, ]); for (const group of groups) { for (const key of FIELD_GROUP_KEYS[group]) allowed.add(key); } const hosts = (raw.hosts ?? []) .filter((host) => selected === null || selected.has(hostKey(host))) .map((host) => { const shaped: Record = {}; for (const [key, value] of Object.entries(host)) { if (allowed.has(key)) shaped[key] = value; } if (!withCredentials) { for (const { container, field } of NESTED_SECRETS) { const record = nestedSecret(shaped, container, field); if (record) shaped[container] = { ...record, [field]: null }; } for (const { container, field } of NESTED_SECRET_ARRAYS) { const arr = shaped[container]; if (Array.isArray(arr)) { shaped[container] = arr.map((entry) => entry && typeof entry === "object" ? { ...(entry as Record), [field]: null } : entry, ); } } } return shaped; }); const result: ExportPayload = { ...raw, hosts }; if (raw.credentials) { const used = new Set( hosts.map((host) => host.credentialAlias).filter(Boolean), ); result.credentials = raw.credentials.filter((entry) => used.has(entry.alias), ); } return result; } export function maskSecrets(payload: ExportPayload): ExportPayload { return { ...payload, hosts: payload.hosts.map((host) => { const masked = { ...host }; for (const key of SECRET_KEYS) { if ( masked[key] !== undefined && masked[key] !== null && masked[key] !== "" ) { masked[key] = ""; } } for (const { container, field } of NESTED_SECRETS) { const record = nestedSecret(masked, container, field); if (record && record[field]) { masked[container] = { ...record, [field]: "" }; } } for (const { container, field } of NESTED_SECRET_ARRAYS) { const arr = masked[container]; if (Array.isArray(arr)) { masked[container] = arr.map((entry) => { if (entry && typeof entry === "object") { const record = entry as Record; if (record[field]) return { ...record, [field]: "" }; } return entry; }); } } return masked; }), }; }