203 lines
7.9 KiB
TypeScript
203 lines
7.9 KiB
TypeScript
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);
|
|
}
|