feat: 完整游戏运维功能
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
import type {
|
||||
DependencyCatalogResponse,
|
||||
DependencyPlanStepViewResponse,
|
||||
DependencyPlanViewResponse,
|
||||
DependencyProbeViewResponse,
|
||||
DependencyState,
|
||||
RunUpdateJobListResponse,
|
||||
RunUpdateJobResponse,
|
||||
RunUpdatePhase
|
||||
} from "../api/types";
|
||||
|
||||
const dependencyStates = new Set<DependencyState>(["unknown", "present", "missing", "installing", "failed"]);
|
||||
const updatePhases = new Set<RunUpdatePhase>(["queued", "downloading", "staged", "restart-requested", "activating", "succeeded", "rolled-back", "failed"]);
|
||||
const updateStatuses = new Set(["queued", "running", "succeeded", "failed", "denied"]);
|
||||
const forbiddenProjectionKeys = new Set([
|
||||
"leasetoken",
|
||||
"leasetokenhash",
|
||||
"leasesessiongeneration",
|
||||
"sessiontoken",
|
||||
"runtoken",
|
||||
"secretref",
|
||||
"hostpath",
|
||||
"executablepath",
|
||||
"stagingpath",
|
||||
"backuppath",
|
||||
"socket",
|
||||
"credential",
|
||||
"pid",
|
||||
"content",
|
||||
"payload",
|
||||
"bindings",
|
||||
"downloadref"
|
||||
]);
|
||||
const unsafeProjectionText = /(?:\/Users\/|\/home\/|\/var\/run\/|[A-Za-z]:\\|unix:\/\/|tcp:\/\/|Bearer\s+|password=|token=|sk-[A-Za-z0-9_-]+)/i;
|
||||
|
||||
export function parseSafeDependencyCatalog(value: unknown): DependencyCatalogResponse {
|
||||
const record = requiredRecordValue(value, "dependency catalog");
|
||||
rejectForbiddenProjection(record);
|
||||
return {
|
||||
serverInstanceId: requiredString(record, "serverInstanceId"),
|
||||
pluginId: requiredString(record, "pluginId"),
|
||||
pluginVersion: requiredString(record, "pluginVersion"),
|
||||
profileKey: requiredString(record, "profileKey"),
|
||||
targetOs: requiredString(record, "targetOs"),
|
||||
targetArch: requiredString(record, "targetArch"),
|
||||
probes: requiredArray(record, "probes").map(parseDependencyProbe),
|
||||
plans: requiredArray(record, "plans").map(parseDependencyPlan),
|
||||
updatedAt: requiredString(record, "updatedAt")
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSafeRunUpdateList(value: unknown): RunUpdateJobListResponse {
|
||||
const record = requiredRecordValue(value, "Run update list");
|
||||
rejectForbiddenProjection(record);
|
||||
const items = requiredArray(record, "items").map((item) => parseRunUpdate(requiredRecordValue(item, "Run update")));
|
||||
const count = requiredNumber(record, "count");
|
||||
if (count !== items.length) throw new Error("Run update count does not match items");
|
||||
return { items, count };
|
||||
}
|
||||
|
||||
export function parseSafeRunUpdate(value: unknown): RunUpdateJobResponse {
|
||||
const record = requiredRecordValue(value, "Run update");
|
||||
rejectForbiddenProjection(record);
|
||||
return parseRunUpdate(record);
|
||||
}
|
||||
|
||||
function parseDependencyProbe(value: unknown): DependencyProbeViewResponse {
|
||||
const record = requiredRecordValue(value, "dependency probe");
|
||||
const state = requiredString(record, "state") as DependencyState;
|
||||
if (!dependencyStates.has(state)) throw new Error("dependency state is invalid");
|
||||
return {
|
||||
key: requiredString(record, "key"),
|
||||
kind: requiredString(record, "kind"),
|
||||
required: requiredBoolean(record, "required"),
|
||||
minimumVersion: optionalSafeString(record, "minimumVersion"),
|
||||
state,
|
||||
evidence: optionalSafeString(record, "evidence"),
|
||||
installPlanKey: optionalString(record, "installPlanKey")
|
||||
};
|
||||
}
|
||||
|
||||
function parseDependencyPlan(value: unknown): DependencyPlanViewResponse {
|
||||
const record = requiredRecordValue(value, "dependency plan");
|
||||
return {
|
||||
key: requiredString(record, "key"),
|
||||
title: requiredString(record, "title"),
|
||||
targetOs: requiredString(record, "targetOs"),
|
||||
targetArch: requiredString(record, "targetArch"),
|
||||
digest: requiredChecksum(record, "digest"),
|
||||
steps: requiredArray(record, "steps").map(parseDependencyStep)
|
||||
};
|
||||
}
|
||||
|
||||
function parseDependencyStep(value: unknown): DependencyPlanStepViewResponse {
|
||||
const record = requiredRecordValue(value, "dependency plan step");
|
||||
const downloadHost = optionalString(record, "downloadHost");
|
||||
if (downloadHost && (downloadHost.includes("/") || downloadHost.includes("@") || downloadHost.includes(":"))) throw new Error("dependency download host is invalid");
|
||||
return {
|
||||
type: requiredString(record, "type"),
|
||||
targetKey: requiredString(record, "targetKey"),
|
||||
packageManager: optionalString(record, "packageManager"),
|
||||
packageName: optionalString(record, "packageName"),
|
||||
version: optionalString(record, "version"),
|
||||
downloadHost,
|
||||
sizeBytes: optionalNumber(record, "sizeBytes")
|
||||
};
|
||||
}
|
||||
|
||||
function parseRunUpdate(record: Record<string, unknown>): RunUpdateJobResponse {
|
||||
const phase = requiredString(record, "phase") as RunUpdatePhase;
|
||||
if (!updatePhases.has(phase)) throw new Error("Run update phase is invalid");
|
||||
const status = requiredString(record, "status");
|
||||
if (!updateStatuses.has(status)) throw new Error("Run update status is invalid");
|
||||
return {
|
||||
id: requiredString(record, "id"),
|
||||
serverInstanceId: requiredString(record, "serverInstanceId"),
|
||||
runEndpointId: requiredString(record, "runEndpointId"),
|
||||
artifactId: requiredString(record, "artifactId"),
|
||||
checksum: requiredChecksum(record, "checksum"),
|
||||
targetOs: requiredString(record, "targetOs"),
|
||||
targetArch: requiredString(record, "targetArch"),
|
||||
targetRelease: optionalString(record, "targetRelease"),
|
||||
previousVersion: optionalString(record, "previousVersion"),
|
||||
jobId: optionalString(record, "jobId"),
|
||||
idempotencyKey: optionalString(record, "idempotencyKey"),
|
||||
status,
|
||||
phase,
|
||||
message: optionalSafeString(record, "message"),
|
||||
rollback: requiredBoolean(record, "rollback"),
|
||||
createdAt: requiredString(record, "createdAt"),
|
||||
updatedAt: requiredString(record, "updatedAt")
|
||||
};
|
||||
}
|
||||
|
||||
function rejectForbiddenProjection(value: unknown): void {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(rejectForbiddenProjection);
|
||||
return;
|
||||
}
|
||||
if (!isRecord(value)) return;
|
||||
for (const [key, field] of Object.entries(value)) {
|
||||
if (forbiddenProjectionKeys.has(key.toLowerCase())) throw new Error(`runtime projection contains forbidden field ${key}`);
|
||||
rejectForbiddenProjection(field);
|
||||
}
|
||||
}
|
||||
|
||||
function requiredRecordValue(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function requiredArray(value: Record<string, unknown>, key: string): unknown[] {
|
||||
const field = value[key];
|
||||
if (!Array.isArray(field)) throw new Error(`${key} must be an array`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function requiredString(value: Record<string, unknown>, key: string): string {
|
||||
const field = value[key];
|
||||
if (typeof field !== "string" || field.trim() === "") throw new Error(`${key} must be a non-empty string`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function optionalString(value: Record<string, unknown>, key: string): string | undefined {
|
||||
const field = value[key];
|
||||
if (field === undefined) return undefined;
|
||||
if (typeof field !== "string") throw new Error(`${key} must be a string`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function optionalSafeString(value: Record<string, unknown>, key: string): string | undefined {
|
||||
const field = optionalString(value, key);
|
||||
if (field && unsafeProjectionText.test(field)) throw new Error(`${key} contains unsafe runtime details`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function requiredChecksum(value: Record<string, unknown>, key: string): string {
|
||||
const field = requiredString(value, key);
|
||||
if (!/^sha256:[a-f0-9]{64}$/.test(field)) throw new Error(`${key} must be a SHA-256 checksum`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function requiredNumber(value: Record<string, unknown>, key: string): number {
|
||||
const field = value[key];
|
||||
if (typeof field !== "number" || !Number.isFinite(field) || field < 0) throw new Error(`${key} must be a non-negative number`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function optionalNumber(value: Record<string, unknown>, key: string): number | undefined {
|
||||
if (value[key] === undefined) return undefined;
|
||||
return requiredNumber(value, key);
|
||||
}
|
||||
|
||||
function requiredBoolean(value: Record<string, unknown>, key: string): boolean {
|
||||
const field = value[key];
|
||||
if (typeof field !== "boolean") throw new Error(`${key} must be a boolean`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user