import { createHash } from "node:crypto"; export function sha256(input: string | Buffer): string { return createHash("sha256").update(input).digest("hex"); } export function shortHash(input: string, len = 16): string { return sha256(input).slice(0, len); } /** * Structural fingerprint of a payload: the sorted set of key paths with primitive types, * ignoring values and array indexes. Used to detect schema drift on public endpoints. */ export function schemaFingerprint(payload: unknown, maxDepth = 6): string { const paths = new Set(); const walk = (v: unknown, path: string, depth: number) => { if (depth > maxDepth) return; if (Array.isArray(v)) { if (v.length === 0) paths.add(`${path}[]`); // Sample first 3 elements — enough to capture the shape. for (const item of v.slice(0, 3)) walk(item, `${path}[]`, depth + 1); return; } if (v && typeof v === "object") { for (const [k, val] of Object.entries(v as Record)) walk(val, path ? `${path}.${k}` : k, depth + 1); return; } paths.add(`${path}:${v === null ? "null" : typeof v}`); }; walk(payload, "", 0); return shortHash([...paths].sort().join("|"), 20); } /** Deterministic fingerprint for deduplicating observations replayed after a reconnect. */ export function observationFingerprint(parts: { sourceId: string; instrumentId: string; field: string; value: number; sourceTimestamp: number | null; sequence?: number | string | null; }): string { return shortHash( `${parts.sourceId}|${parts.instrumentId}|${parts.field}|${parts.value}|${parts.sourceTimestamp ?? ""}|${parts.sequence ?? ""}`, 24, ); }