Remove legacy client-manager workflows

This commit is contained in:
npc0-hue
2026-09-03 13:08:08 +08:00
parent bf3c382d15
commit fe09d21a56
56 changed files with 304 additions and 4121 deletions
@@ -1,59 +0,0 @@
import { describe, expect, it } from "vitest";
import { parseSafeClientManagerLifecycle, parseSafeClientManagerLifecycleList } from "./clientManagerLifecycle";
export const safeClientManagerLifecycleFixture = {
id: "client-manager-installation-1",
serverInstanceId: "server-1",
pluginId: "game.scum",
profileKey: "scum-client-manager",
targetOs: "windows",
targetArch: "amd64",
status: "online",
phase: "component heartbeat healthy",
desiredVersion: "2.0.0",
activeVersion: "2.0.0",
previousVersion: "1.0.0",
desiredRevision: "rev-2",
activeRevision: "rev-2",
previousRevision: "rev-1",
desiredArtifactId: "artifact-2",
activeArtifactId: "artifact-2",
previousArtifactId: "artifact-1",
keyGeneration: 3,
deploymentGeneration: 4,
currentJobId: "job-update-1",
lastSuccessfulJobId: "job-deploy-1",
lastOperation: "update",
health: "healthy",
healthReason: "component heartbeat healthy",
lastSeenAt: "2026-07-18T08:00:00Z",
retryable: false,
requiresRedeploy: false,
updatedAt: "2026-07-18T08:00:00Z",
distribution: { id: "distribution-2", artifactId: "artifact-2", sourceRevision: "rev-2", targetOs: "windows", targetArch: "amd64", checksum: `sha256:${"a".repeat(64)}`, keyGeneration: 3, status: "available" },
job: { id: "job-update-1", state: "running", progress: { percent: 65, message: "health confirmation" }, attempt: 1, createdAt: "2026-07-18T07:59:00Z", updatedAt: "2026-07-18T08:00:00Z" },
actions: [
{ operation: "start", available: false, reason: "already online" },
{ operation: "stop", available: true },
{ operation: "rollback", available: true }
]
} as const;
describe("Client Manager lifecycle schema", () => {
it("preserves safe lifecycle, job progress, versions and action availability", () => {
const parsed = parseSafeClientManagerLifecycle(safeClientManagerLifecycleFixture);
expect(parsed).toMatchObject({ status: "online", health: "healthy", activeVersion: "2.0.0", previousVersion: "1.0.0", job: { state: "running", progress: { percent: 65 } } });
expect(parseSafeClientManagerLifecycleList({ items: [safeClientManagerLifecycleFixture], count: 1 })).toMatchObject({ count: 1, items: [{ profileKey: "scum-client-manager" }] });
});
it.each([
{ runEndpointId: "run-private" },
{ pid: 4124 },
{ secretRef: "redacted" },
{ healthReason: "/Users/operator/client-manager" },
{ healthReason: "unix://private.sock" }
])("rejects machine and credential projection %#", (unsafe) => {
expect(() => parseSafeClientManagerLifecycle({ ...safeClientManagerLifecycleFixture, ...unsafe })).toThrow(/forbidden|sensitive/i);
});
});
@@ -1,117 +0,0 @@
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");
}
-20
View File
@@ -1,5 +1,4 @@
import type {
ClientManagerBuildRequest,
DependencyJobRequest,
LogBackfillRequest,
RunDistributionGenerateRequest,
@@ -92,25 +91,6 @@ export function runUpdateRequest(serverInstanceId: string, artifactId: string, c
};
}
export function clientManagerBuildRequest(input: {
serverInstanceId: string;
profileKey: string;
targetOs: string;
targetArch: string;
repositoryUrl: string;
sourceRevision?: string;
sequence?: number;
}): ClientManagerBuildRequest {
return {
profileKey: input.profileKey.trim(),
targetOs: input.targetOs,
targetArch: input.targetArch,
repositoryUrl: input.repositoryUrl.trim(),
sourceRevision: input.sourceRevision?.trim() || undefined,
idempotencyKey: runtimeIdempotencyKey("client-manager.generate", input.serverInstanceId, input.sequence ?? Date.now())
};
}
export function dependencyJobRequest(serverInstanceId: string, probeKey: string, installPlanKey = "", planDigest = "", sequence = Date.now()): DependencyJobRequest {
return {
probeKey: probeKey.trim(),