SPB Git

Repository Public

audit.ts 1.5 KB 36 lines typescript

1import { sha256 } from '@noble/hashes/sha2.js';23export interface AuditRecord { seq: number; ts: string; type: string; actor: string; data: Record<string, unknown>; prev: string; hash: string }45const enc = new TextEncoder();6function stable(v: unknown): string {7  if (v === null || typeof v !== 'object') return JSON.stringify(v ?? null);8  if (Array.isArray(v)) return `[${v.map(stable).join(',')}]`;9  const o = v as Record<string, unknown>;10  return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stable(o[k])}`).join(',')}}`;11}1213export const GENESIS = '0'.repeat(64);1415export function auditHash(r: Omit<AuditRecord, 'hash'>): string {16  const body = ['SPW-AUDIT-v1', r.seq, r.ts, r.type, r.actor, stable(r.data), r.prev].join('\n');17  return Buffer.from(sha256(enc.encode(body))).toString('hex');18}1920export function appendAudit(prev: AuditRecord | null, type: string, actor: string, data: Record<string, unknown>, ts = new Date().toISOString()): AuditRecord {21  const base = { seq: (prev?.seq ?? 0) + 1, ts, type, actor, data, prev: prev?.hash ?? GENESIS };22  return { ...base, hash: auditHash(base) };23}2425/** Returns the first broken sequence number, or null when the chain is intact. */26export function verifyAuditChain(records: AuditRecord[]): number | null {27  let prev = GENESIS, seq = 0;28  for (const r of records) {29    seq += 1;30    const { hash, ...rest } = r;31    if (r.seq !== seq || r.prev !== prev || auditHash(rest) !== hash) return r.seq;32    prev = hash;33  }34  return null;35}36
Files main
Menu

Theme