import type { JobResponse, JobState } from "../api/types"; const jobStates = new Set(["queued", "accepted", "running", "retrying", "succeeded", "failed", "cancelled"]); const forbiddenProjectionKeys = new Set(["leaseToken", "leaseTokenHash", "leaseSessionGeneration", "sessionToken", "secretRef", "hostPath", "socket", "credential"]); export function parseSafeJobResponse(value: unknown): JobResponse { if (!isRecord(value)) throw new Error("job projection must be an object"); rejectForbiddenKeys(value); const state = requiredString(value, "state") as JobState; if (!jobStates.has(state)) throw new Error("job state is invalid"); const progress = requiredRecord(value, "progress"); const retryPolicy = requiredRecord(value, "retryPolicy"); const parsed: JobResponse = { id: requiredString(value, "id"), serverInstanceId: optionalString(value, "serverInstanceId"), runEndpointId: requiredString(value, "runEndpointId"), capability: requiredString(value, "capability"), targetKey: optionalString(value, "targetKey"), inputRef: optionalString(value, "inputRef"), idempotencyKey: requiredString(value, "idempotencyKey"), state, progress: { percent: requiredNumber(progress, "percent"), message: optionalString(progress, "message") }, resultRef: optionalString(value, "resultRef"), executionResult: optionalExecutionResult(value, "executionResult"), retryPolicy: { maxAttempts: requiredNumber(retryPolicy, "maxAttempts"), initialBackoffSeconds: requiredNumber(retryPolicy, "initialBackoffSeconds"), maxBackoffSeconds: requiredNumber(retryPolicy, "maxBackoffSeconds") }, attempt: requiredNumber(value, "attempt"), nextAttemptAt: optionalString(value, "nextAttemptAt"), ackDeadlineAt: optionalString(value, "ackDeadlineAt"), leaseExpiresAt: optionalString(value, "leaseExpiresAt"), cancelReason: optionalString(value, "cancelReason"), cancelRequestedAt: optionalString(value, "cancelRequestedAt"), cancelCompletedAt: optionalString(value, "cancelCompletedAt"), terminalAt: optionalString(value, "terminalAt"), lastReconciledAt: optionalString(value, "lastReconciledAt"), reconcileCount: requiredNumber(value, "reconcileCount"), reconcileOutcome: optionalString(value, "reconcileOutcome"), createdAt: requiredString(value, "createdAt"), updatedAt: requiredString(value, "updatedAt") }; return parsed; } function optionalExecutionResult(value: Record, key: string): JobResponse["executionResult"] { const field = value[key]; if (field === undefined) return undefined; if (!isRecord(field)) throw new Error(`${key} must be an object`); rejectForbiddenKeys(field); const result: NonNullable = { kind: optionalString(field, "kind"), processState: optionalString(field, "processState"), exitClassification: optionalString(field, "exitClassification"), exitCode: optionalSignedNumber(field, "exitCode"), version: optionalNumber(field, "version"), checksum: optionalString(field, "checksum"), sizeBytes: optionalNumber(field, "sizeBytes"), summary: optionalString(field, "summary"), content: optionalString(field, "content") }; return result; } function rejectForbiddenKeys(value: Record): void { for (const key of Object.keys(value)) { if (forbiddenProjectionKeys.has(key)) throw new Error(`job projection contains forbidden field ${key}`); } } function requiredRecord(value: Record, key: string): Record { const field = value[key]; if (!isRecord(field)) throw new Error(`${key} must be an object`); rejectForbiddenKeys(field); return field; } function requiredString(value: Record, 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, 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 optionalSignedNumber(value: Record, key: string): number | undefined { const field = value[key]; if (field === undefined) return undefined; if (typeof field !== "number" || !Number.isFinite(field)) throw new Error(`${key} must be a number`); return field; } function optionalNumber(value: Record, key: string): number | undefined { const field = optionalSignedNumber(value, key); if (field !== undefined && field < 0) throw new Error(`${key} must be non-negative`); return field; } function requiredNumber(value: Record, 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 isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }