import type { InspectionResult } from "./types"; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function finiteScore(value: unknown) { return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100; } function collectorStatus(value: unknown) { return value === "ok" || value === "partial" || value === "failed"; } function nonNegativeInteger(value: unknown): value is number { return typeof value === "number" && Number.isInteger(value) && value >= 0; } function isFinding(value: unknown) { if (!isRecord(value)) return false; const category = value.category; const severity = value.severity; return typeof value.id === "string" && typeof value.title === "string" && typeof value.summary === "string" && (category === "dns" || category === "tls" || category === "http") && (severity === "critical" || severity === "warning" || severity === "info" || severity === "good") && typeof value.scoreImpact === "number" && Number.isFinite(value.scoreImpact) && Array.isArray(value.evidence) && value.evidence.every((pointer) => typeof pointer === "string") && (value.remediation === undefined || typeof value.remediation === "string"); } /** * A deliberately small client boundary: it validates the shape ReportView * depends on without duplicating server-side schema logic or adding a runtime * validation dependency to this compact project. */ export function isInspectionResult(value: unknown): value is InspectionResult { if (!isRecord(value) || typeof value.host !== "string" || !value.host || typeof value.inspectedAt !== "string" || !Number.isFinite(Date.parse(value.inspectedAt))) return false; if (!Array.isArray(value.addresses) || !value.addresses.every((address) => typeof address === "string")) return false; const score = value.score; if (!isRecord(score) || !finiteScore(score.overall) || !isRecord(score.categories)) return false; const categories = score.categories; if (!["dns", "tls", "http"].every((category) => finiteScore(categories[category]))) return false; const completeness = value.completeness; if (!isRecord(completeness) || !["complete", "partial", "failed"].includes(String(completeness.status))) return false; const complete = completeness.complete; const total = completeness.total; if (!nonNegativeInteger(complete) || !nonNegativeInteger(total) || complete > total || !isRecord(completeness.collectors) || !Array.isArray(completeness.issues)) return false; const collectors = completeness.collectors; if (!["dns", "tls", "http"].every((category) => collectorStatus(collectors[category])) || !completeness.issues.every((issue) => typeof issue === "string")) return false; if (!Array.isArray(value.findings) || !value.findings.every(isFinding)) return false; return isRecord(value.dns) && isRecord(value.tls) && isRecord(value.http); }