Files
browser/platform_web/schemas/clientManagerLifecycle.ts
T
2026-07-20 16:42:33 +08:00

118 lines
7.3 KiB
TypeScript

import type {
ClientManagerInstallationListResponse,
ClientManagerInstallationResponse,
ClientManagerLifecycleActionResponse,
ClientManagerLifecycleOperation,
ClientManagerLifecycleStatus,
JobState
} from "../api/types";
const lifecycleStatuses = new Set<ClientManagerLifecycleStatus>([
"requested", "building", "available", "deploying", "installed", "registering", "online", "degraded", "offline", "updating", "rolling_back", "stopping", "uninstalled", "failed"
]);
const lifecycleOperations = new Set<ClientManagerLifecycleOperation>(["deploy", "start", "stop", "restart", "status", "update", "rollback", "uninstall"]);
const jobStates = new Set<JobState>(["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<string, unknown>, target: ClientManagerInstallationResponse, keys: Array<keyof ClientManagerInstallationResponse>) {
for (const key of keys) {
const value = source[key];
if (typeof value === "string" && value !== "") (target as unknown as Record<string, unknown>)[key] = value;
}
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
return value as Record<string, unknown>;
}
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");
}