import type { ClientManagerInstallationListResponse, ClientManagerInstallationResponse, ClientManagerLifecycleActionResponse, ClientManagerLifecycleOperation, ClientManagerLifecycleStatus, JobState } from "../api/types"; const lifecycleStatuses = new Set([ "requested", "building", "available", "deploying", "installed", "registering", "online", "degraded", "offline", "updating", "rolling_back", "stopping", "uninstalled", "failed" ]); const lifecycleOperations = new Set(["deploy", "start", "stop", "restart", "status", "update", "rollback", "uninstall"]); const jobStates = new Set(["queued", "accepted", "running", "retrying", "succeeded", "failed", "cancelled"]); const forbiddenKeys = new Set(["key", "token", "secretref", "secretvalue", "hostpath", "pid", "socket", "credential", "dsn", "password", "runendpointid"]); const forbiddenFragments = ["secret://", "/users/", "/var/run/", "bearer ", "password=", "unix://", "tcp://", "mysql://", "sqlite://", "rcon://"]; export function parseSafeClientManagerLifecycleList(value: unknown): ClientManagerInstallationListResponse { const record = object(value, "Client Manager lifecycle list"); rejectSensitiveProjection(record); const items = array(record.items, "items").map(parseSafeClientManagerLifecycle); const count = number(record.count, "count"); return { items, count }; } export function parseSafeClientManagerLifecycle(value: unknown): ClientManagerInstallationResponse { const record = object(value, "Client Manager lifecycle"); rejectSensitiveProjection(record); const status = string(record.status, "status") as ClientManagerLifecycleStatus; if (!lifecycleStatuses.has(status)) throw new Error("Client Manager lifecycle status is invalid"); const actions = array(record.actions, "actions").map(parseAction); const result: ClientManagerInstallationResponse = { id: string(record.id, "id"), serverInstanceId: string(record.serverInstanceId, "serverInstanceId"), pluginId: string(record.pluginId, "pluginId"), profileKey: string(record.profileKey, "profileKey"), targetOs: string(record.targetOs, "targetOs"), targetArch: string(record.targetArch, "targetArch"), status, phase: string(record.phase, "phase"), keyGeneration: number(record.keyGeneration, "keyGeneration"), deploymentGeneration: number(record.deploymentGeneration, "deploymentGeneration"), health: health(record.health), retryable: boolean(record.retryable, "retryable"), requiresRedeploy: boolean(record.requiresRedeploy, "requiresRedeploy"), updatedAt: string(record.updatedAt, "updatedAt"), actions }; copyOptionalStrings(record, result, ["desiredVersion", "activeVersion", "previousVersion", "desiredRevision", "activeRevision", "previousRevision", "desiredArtifactId", "activeArtifactId", "previousArtifactId", "currentJobId", "lastSuccessfulJobId", "healthReason", "lastSeenAt", "installedAt", "uninstalledAt"]); if (record.lastOperation !== undefined) { const operation = string(record.lastOperation, "lastOperation") as ClientManagerLifecycleOperation; if (!lifecycleOperations.has(operation)) throw new Error("Client Manager lifecycle operation is invalid"); result.lastOperation = operation; } if (record.distribution !== undefined) { const distribution = object(record.distribution, "distribution"); result.distribution = { id: string(distribution.id, "distribution.id"), artifactId: string(distribution.artifactId, "distribution.artifactId"), sourceRevision: string(distribution.sourceRevision, "distribution.sourceRevision"), targetOs: string(distribution.targetOs, "distribution.targetOs"), targetArch: string(distribution.targetArch, "distribution.targetArch"), checksum: string(distribution.checksum, "distribution.checksum"), keyGeneration: number(distribution.keyGeneration, "distribution.keyGeneration"), status: string(distribution.status, "distribution.status") }; } if (record.job !== undefined) { const job = object(record.job, "job"); const state = string(job.state, "job.state") as JobState; if (!jobStates.has(state)) throw new Error("Client Manager job state is invalid"); const progress = object(job.progress, "job.progress"); result.job = { id: string(job.id, "job.id"), state, progress: { percent: number(progress.percent, "job.progress.percent"), message: optionalString(progress.message) }, attempt: number(job.attempt, "job.attempt"), createdAt: string(job.createdAt, "job.createdAt"), updatedAt: string(job.updatedAt, "job.updatedAt") }; } return result; } function parseAction(value: unknown): ClientManagerLifecycleActionResponse { const action = object(value, "action"); const operation = string(action.operation, "action.operation") as ClientManagerLifecycleOperation; if (!lifecycleOperations.has(operation)) throw new Error("Client Manager action is invalid"); return { operation, available: boolean(action.available, "action.available"), reason: optionalString(action.reason) }; } function rejectSensitiveProjection(value: unknown, key = ""): void { if (typeof value === "string") { const normalized = value.toLowerCase(); if (forbiddenFragments.some((fragment) => normalized.includes(fragment))) throw new Error("Client Manager response contains sensitive machine data"); return; } if (Array.isArray(value)) { value.forEach((item) => rejectSensitiveProjection(item, key)); return; } if (value && typeof value === "object") { for (const [childKey, child] of Object.entries(value)) { if (forbiddenKeys.has(childKey.toLowerCase())) throw new Error("Client Manager response contains a forbidden field"); rejectSensitiveProjection(child, childKey); } } } function copyOptionalStrings(source: Record, target: ClientManagerInstallationResponse, keys: Array) { for (const key of keys) { const value = source[key]; if (typeof value === "string" && value !== "") (target as unknown as Record)[key] = value; } } function object(value: unknown, label: string): Record { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`); return value as Record; } function array(value: unknown, label: string): unknown[] { if (!Array.isArray(value)) throw new Error(`${label} must be an array`); return value; } function string(value: unknown, label: string): string { if (typeof value !== "string" || value === "") throw new Error(`${label} must be a string`); return value; } function optionalString(value: unknown): string | undefined { return typeof value === "string" && value !== "" ? value : undefined; } function number(value: unknown, label: string): number { if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be a number`); return value; } function boolean(value: unknown, label: string): boolean { if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`); return value; } function health(value: unknown): ClientManagerInstallationResponse["health"] { if (value === "unknown" || value === "healthy" || value === "degraded" || value === "unhealthy" || value === "offline") return value; throw new Error("Client Manager health is invalid"); }