import { isUuidV7 } from './uuid-v7'; export const EVENT_ENVELOPE_SCHEMA_VERSION = 1; export interface EventEnvelope { schemaVersion: number; eventId: string; eventType: string; occurredAt: string; producer: string; traceId: string; payload: TPayload; } const TRACE_ID_RE = /^[0-9a-f]{32}$/i; const ISO_8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/; export interface EnvelopeValidationIssue { path: string; message: string; } export function validateEnvelope(envelope: unknown): EnvelopeValidationIssue[] { const issues: EnvelopeValidationIssue[] = []; if (envelope === null || typeof envelope !== 'object') { return [{ path: '$', message: 'envelope must be an object' }]; } const e = envelope as Record; if (e['schemaVersion'] !== EVENT_ENVELOPE_SCHEMA_VERSION) { issues.push({ path: 'schemaVersion', message: `expected ${EVENT_ENVELOPE_SCHEMA_VERSION}, got ${String(e['schemaVersion'])}`, }); } if (typeof e['eventId'] !== 'string' || !isUuidV7(e['eventId'])) { issues.push({ path: 'eventId', message: 'must be a UUIDv7 string' }); } if ( typeof e['eventType'] !== 'string' || !/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(e['eventType']) ) { issues.push({ path: 'eventType', message: 'must match /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$/', }); } if (typeof e['occurredAt'] !== 'string' || !ISO_8601_RE.test(e['occurredAt'])) { issues.push({ path: 'occurredAt', message: 'must be an ISO-8601 timestamp' }); } if (typeof e['producer'] !== 'string' || e['producer'].length === 0) { issues.push({ path: 'producer', message: 'must be a non-empty string' }); } if (typeof e['traceId'] !== 'string' || !TRACE_ID_RE.test(e['traceId'])) { issues.push({ path: 'traceId', message: 'must be 32 hex characters' }); } if (!('payload' in e)) { issues.push({ path: 'payload', message: 'is required (use {} for empty)' }); } return issues; } export function assertValidEnvelope(envelope: unknown): asserts envelope is EventEnvelope { const issues = validateEnvelope(envelope); if (issues.length > 0) { const flat = issues.map((i) => `${i.path}: ${i.message}`).join('; '); throw new Error(`Invalid EventEnvelope — ${flat}`); } }