export type FieldGroup = | "connection" | "notes" | "tags" | "proxy" | "jumpHosts" | "quickActions" | "featureFlags" | "advanced"; export interface ExportPayload { version?: string; exportedAt?: string; credentials?: Record[]; hosts: Record[]; } 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"], // Every plugin's host settings, enable switches included. featureFlags: ["pluginSettings", "forceKeyboardInteractive"], advanced: [ "statusCheckEnabled", "statusCheckInterval", "terminalConfig", "sshOptions", ], }; 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; } /** Plugin host settings can carry the same nested secrets as the host. */ function mapPluginSettings( pluginSettings: unknown, apply: (values: Record) => void, ): unknown { if (!pluginSettings || typeof pluginSettings !== "object") { return pluginSettings; } const perPlugin: Record = {}; for (const [pluginId, values] of Object.entries( pluginSettings as Record, )) { const copy = { ...(values as Record) }; apply(copy); perPlugin[pluginId] = copy; } return perPlugin; } 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) { const clearNested = (target: Record) => { for (const { container, field } of NESTED_SECRETS) { const record = nestedSecret(target, container, field); if (record) target[container] = { ...record, [field]: null }; } }; clearNested(shaped); shaped.pluginSettings = mapPluginSettings( shaped.pluginSettings, clearNested, ); 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] = ""; } } const maskNested = (target: Record) => { for (const { container, field } of NESTED_SECRETS) { const record = nestedSecret(target, container, field); if (record && record[field]) { target[container] = { ...record, [field]: "" }; } } }; maskNested(masked); masked.pluginSettings = mapPluginSettings( masked.pluginSettings, maskNested, ); 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; }), }; }