Remove legacy client-manager workflows
This commit is contained in:
@@ -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");
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user