Files

53 lines
2.9 KiB
TypeScript

import type { InspectionResult } from "./types";
function isRecord(value: unknown): value is Record<string, unknown> {
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);
}