46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
|
|
import type { CheckDefinitionSpec, CheckSetPolicySpec } from './types.js';
|
|
|
|
// Deterministic JSON: object keys sorted at every level so two specs with the
|
|
// same content always produce the same bytes (and thus the same digest).
|
|
export function canonicalJson(value: unknown): string {
|
|
if (value === null || typeof value !== 'object') {
|
|
return JSON.stringify(value);
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
|
|
}
|
|
const record = value as Record<string, unknown>;
|
|
const keys = Object.keys(record).sort();
|
|
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
}
|
|
|
|
/** sha256 over the canonical JSON of `value`. */
|
|
export function digestContent(value: unknown): string {
|
|
return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Content digest of a check definition: covers the declarative spec (id,
|
|
* version, canonical check, description, applicability, params) — everything a
|
|
* reviewer reasons about — while excluding the executable function object.
|
|
* Changing any covered field changes the digest, so a recorded digest always
|
|
* identifies exactly which definition content produced a verdict.
|
|
*/
|
|
export function digestOfSpec(spec: CheckDefinitionSpec): string {
|
|
return digestContent({
|
|
id: spec.id,
|
|
version: spec.version,
|
|
canonicalCheck: spec.canonicalCheck,
|
|
description: spec.description,
|
|
appliesTo: spec.appliesTo,
|
|
params: spec.params,
|
|
});
|
|
}
|
|
|
|
/** Content digest of the per-subject check-set policy. */
|
|
export function digestOfPolicy(spec: CheckSetPolicySpec): string {
|
|
return digestContent(spec);
|
|
}
|