Files
Termix/src/ui/sidebar/host-export-payload.ts
T

212 lines
5.6 KiB
TypeScript

export type FieldGroup =
| "connection"
| "notes"
| "tags"
| "proxy"
| "jumpHosts"
| "quickActions"
| "featureFlags"
| "advanced";
export interface ExportPayload {
version?: string;
exportedAt?: string;
credentials?: Record<string, unknown>[];
hosts: Record<string, unknown>[];
}
const FIELD_GROUP_KEYS: Record<FieldGroup, string[]> = {
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"],
};
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<string, unknown>,
container: string,
field: string,
): Record<string, unknown> | null {
const blob = host[container];
if (!blob || typeof blob !== "object") return null;
const record = blob as Record<string, unknown>;
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<string, unknown>) => void,
): unknown {
if (!pluginSettings || typeof pluginSettings !== "object") {
return pluginSettings;
}
const perPlugin: Record<string, unknown> = {};
for (const [pluginId, values] of Object.entries(
pluginSettings as Record<string, unknown>,
)) {
const copy = { ...(values as Record<string, unknown>) };
apply(copy);
perPlugin[pluginId] = copy;
}
return perPlugin;
}
export function hostKey(host: Record<string, unknown>): string {
return JSON.stringify(TUPLE_KEYS.map((k) => String(host[k] ?? "")));
}
export function buildExportPayload(
raw: ExportPayload,
selected: Set<string> | null,
groups: Set<FieldGroup>,
withCredentials: boolean,
): ExportPayload {
const allowed = new Set<string>([
"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<string, unknown> = {};
for (const [key, value] of Object.entries(host)) {
if (allowed.has(key)) shaped[key] = value;
}
if (!withCredentials) {
const clearNested = (target: Record<string, unknown>) => {
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<string, unknown>), [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] = "<included>";
}
}
const maskNested = (target: Record<string, unknown>) => {
for (const { container, field } of NESTED_SECRETS) {
const record = nestedSecret(target, container, field);
if (record && record[field]) {
target[container] = { ...record, [field]: "<included>" };
}
}
};
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<string, unknown>;
if (record[field]) return { ...record, [field]: "<included>" };
}
return entry;
});
}
}
return masked;
}),
};
}