@fonderie/events 5.0.1 → 5.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -88,6 +88,7 @@ function matchesPattern(pattern, eventType) {
88
88
 
89
89
  // src/integrity.ts
90
90
  var import_node_crypto2 = require("crypto");
91
+ var import_core = require("@fonderie/core");
91
92
  function canonicalize(value) {
92
93
  return JSON.stringify(sortKeys(value));
93
94
  }
@@ -105,11 +106,6 @@ function sortKeys(value) {
105
106
  function computeEventHmac(key, event) {
106
107
  return (0, import_node_crypto2.createHmac)("sha256", key).update(event.id).update("\n").update(event.type).update("\n").update(canonicalize(event.payload)).update("\n").update(canonicalize(event.meta)).digest("hex");
107
108
  }
108
- function hmacEquals(a, b) {
109
- const bufA = Buffer.from(a);
110
- const bufB = Buffer.from(b);
111
- return bufA.length === bufB.length && (0, import_node_crypto2.timingSafeEqual)(bufA, bufB);
112
- }
113
109
  async function verifyEventChain(store, key) {
114
110
  const rows = await store.query(
115
111
  `SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`
@@ -122,7 +118,7 @@ async function verifyEventChain(store, key) {
122
118
  }
123
119
  report.checked += 1;
124
120
  const expected = computeEventHmac(key, row);
125
- if (!hmacEquals(expected, row.hmac)) {
121
+ if (!(0, import_core.constantTimeEqual)(expected, row.hmac)) {
126
122
  report.ok = false;
127
123
  report.tampered.push(row.id);
128
124
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/bus.ts","../src/transports/pg.ts","../src/transports/pattern.ts","../src/integrity.ts","../src/module.ts","../src/transports/memory.ts","../src/integrity-job.ts","../src/retention.ts"],"sourcesContent":["export { EventBus } from './bus';\nexport { EventsModule } from './module';\nexport type { IEventsConfig, EventTransportConfig } from './module';\n\nexport { MemoryTransport, PGTransport } from './transports';\nexport type { IEventTransport, IPGTransportConfig } from './transports';\n\nexport { matchesPattern } from './transports/pattern';\n\n// Audit-log tamper-evidence\nexport { computeEventHmac, verifyEventChain, canonicalize } from './integrity';\nexport { startIntegrityCheck } from './integrity-job';\nexport type { IIntegrityCheckOptions, IIntegrityCheckHandle } from './integrity-job';\nexport type { IHashableEvent, IIntegrityReport } from './integrity';\n\n// Retention / disposal\nexport { purgeEvents, startEventRetention } from './retention';\nexport type { IPurgeEventsOptions, IRetentionScheduleOptions } from './retention';\n\nexport type { IEventMeta, IEventHandler, IEventRecord, IConsumerRecord } from './types';\n\n// ── Typed event keys ─────────────────────────────────────────────\n// Each domain package re-exports its own EVENT_KEYS.\n// Consumers alias on import:\n// import { EVENT_KEYS as AUTH_EVENT_KEYS } from '@fonderie/auth'\n\nexport const NOTIFICATION_EVENT = 'fonderie.notification.send' as const;\nexport type NotificationEvent = typeof NOTIFICATION_EVENT;\n","import { randomUUID } from 'node:crypto';\n\nimport type { IEventTransport } from './transports/types';\nimport type { IEventMeta, IEventHandler } from './types';\n\nexport class EventBus {\n\tconstructor(private transport: IEventTransport) {}\n\n\tasync emit<T = unknown>(type: string, payload: T, opts?: { requestId?: string }): Promise<void> {\n\t\tconst meta: IEventMeta = {\n\t\t\tid: randomUUID(),\n\t\t\ttype,\n\t\t\temittedAt: new Date().toISOString(),\n\t\t\tattempts: 0,\n\t\t\t...(opts?.requestId !== undefined ? { requestId: opts.requestId } : {}),\n\t\t};\n\t\tawait this.transport.publish(type, payload, meta);\n\t}\n\n\t// consumer identifies the logical subscriber for per-consumer delivery tracking.\n\t// Defaults to the pattern string — stable and predictable for single-subscriber patterns.\n\ton<T = unknown>(type: string, handler: IEventHandler<T>, consumer: string = type): void {\n\t\tthis.transport.subscribe(type, handler as IEventHandler, consumer);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tawait this.transport.start();\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.transport.stop();\n\t}\n}\n","import pg from 'pg';\n\nimport { PGAdapter } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler, IEventRecord } from '../types';\nimport { matchesPattern } from './pattern';\nimport { computeEventHmac } from '../integrity';\n\nexport interface IPGTransportConfig {\n\tconnectionUrl: string;\n\tmaxRetries?: number; // default 3\n\tbatchSize?: number; // default 10 rows claimed per consumer per poll cycle\n\tpollInterval?: number; // default 1000ms fallback poll when no NOTIFY arrives\n\t// When set, every event is stored with a keyed HMAC over its immutable\n\t// content, making the audit log tamper-evident. Unset → no HMAC (unchanged\n\t// behaviour). Verify later with `verifyEventChain(store, integrityKey)`.\n\tintegrityKey?: string;\n}\n\ninterface Subscription {\n\tpattern: string;\n\thandler: IEventHandler;\n\tconsumer: string;\n}\n\nexport class PGTransport implements IEventTransport {\n\tprivate subscriptions: Subscription[] = [];\n\tprivate listenClient: pg.Client | null = null;\n\tprivate store!: IStoreAdapter;\n\tprivate running = false;\n\tprivate wakeResolvers: Array<() => void> = [];\n\n\tprivate readonly maxRetries: number;\n\tprivate readonly batchSize: number;\n\tprivate readonly pollInterval: number;\n\tprivate readonly integrityKey: string | undefined;\n\n\tconstructor(private config: IPGTransportConfig) {\n\t\tthis.maxRetries = config.maxRetries ?? 3;\n\t\tthis.batchSize = config.batchSize ?? 10;\n\t\tthis.pollInterval = config.pollInterval ?? 1_000;\n\t\tthis.integrityKey = config.integrityKey;\n\t}\n\n\t// ── Public API ──────────────────────────────────────────────────\n\n\tsubscribe(pattern: string, handler: IEventHandler, consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler, consumer });\n\t}\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst hmac = this.integrityKey\n\t\t\t? computeEventHmac(this.integrityKey, { id: meta.id, type, payload, meta })\n\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_events (id, type, payload, meta, hmac)\n\t\t\t VALUES ($1, $2, $3, $4, $5)`,\n\t\t\t[meta.id, type, JSON.stringify(payload), JSON.stringify(meta), hmac],\n\t\t);\n\n\t\tconst consumers = this.matchingConsumers(type);\n\t\tif (consumers.length > 0) {\n\t\t\tawait this.store.query(\n\t\t\t\t`INSERT INTO fonderie_event_consumers (event_id, consumer, status, attempts)\n\t\t\t\t SELECT $1, unnest($2::text[]), 'pending', 0\n\t\t\t\t ON CONFLICT (event_id, consumer) DO NOTHING`,\n\t\t\t\t[meta.id, consumers],\n\t\t\t);\n\t\t}\n\n\t\t// NOTIFY carries no payload — it is a wake signal only\n\t\tawait this.store.query(`SELECT pg_notify('fonderie_events', '')`);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.running = true;\n\t\tthis.store = new PGAdapter(this.config.connectionUrl);\n\n\t\t// Reset any rows left in 'processing' by a crashed instance\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_event_consumers SET status = 'failed' WHERE status = 'processing'`,\n\t\t);\n\n\t\tthis.listenClient = new pg.Client(this.config.connectionUrl);\n\t\tawait this.listenClient.connect();\n\t\tawait this.listenClient.query('LISTEN fonderie_events');\n\n\t\tthis.listenClient.on('notification', () => this.wake());\n\t\tthis.listenClient.on('error', (err) =>\n\t\t\tconsole.error('[events:pg] listen client error:', err.message),\n\t\t);\n\n\t\tthis.runPollLoop().catch((err) => console.error('[events:pg] poll loop crashed:', err));\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tthis.running = false;\n\t\tthis.wake();\n\t\tawait this.listenClient?.end();\n\t\tthis.listenClient = null;\n\t}\n\n\t// ── Poll loop ───────────────────────────────────────────────────\n\n\tprivate async runPollLoop(): Promise<void> {\n\t\twhile (this.running) {\n\t\t\ttry {\n\t\t\t\tconst hadWork = await this.pollAllConsumers();\n\t\t\t\tif (!hadWork) await this.sleep();\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error('[events:pg] poll error:', err);\n\t\t\t\tawait this.sleep();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async pollAllConsumers(): Promise<boolean> {\n\t\tconst consumers = [...new Set(this.subscriptions.map((s) => s.consumer))];\n\t\tconst results = await Promise.all(consumers.map((c) => this.pollConsumer(c)));\n\t\treturn results.some((n) => n > 0);\n\t}\n\n\tprivate async pollConsumer(consumer: string): Promise<number> {\n\t\tconst claimed = await this.store.query<{ event_id: string }>(\n\t\t\t`UPDATE fonderie_event_consumers c\n\t\t\t SET status = 'processing', attempts = c.attempts + 1\n\t\t\t FROM (\n\t\t\t SELECT event_id\n\t\t\t FROM fonderie_event_consumers\n\t\t\t WHERE consumer = $1\n\t\t\t AND status IN ('pending', 'failed')\n\t\t\t AND attempts < $2\n\t\t\t ORDER BY event_id\n\t\t\t LIMIT $3\n\t\t\t FOR UPDATE SKIP LOCKED\n\t\t\t ) AS locked\n\t\t\t WHERE c.event_id = locked.event_id\n\t\t\t AND c.consumer = $1\n\t\t\t RETURNING c.event_id`,\n\t\t\t[consumer, this.maxRetries, this.batchSize],\n\t\t);\n\n\t\tawait Promise.all(claimed.map((row) => this.processConsumerEvent(consumer, row.event_id)));\n\t\treturn claimed.length;\n\t}\n\n\t// ── Event processing ────────────────────────────────────────────\n\n\tprivate async processConsumerEvent(consumer: string, eventId: string): Promise<void> {\n\t\tconst [event] = await this.store.query<IEventRecord>(\n\t\t\t`SELECT type, payload, meta FROM fonderie_events WHERE id = $1`,\n\t\t\t[eventId],\n\t\t);\n\t\tif (!event) return;\n\n\t\tconst handlers = this.subscriptions\n\t\t\t.filter((s) => s.consumer === consumer && matchesPattern(s.pattern, event.type))\n\t\t\t.map((s) => s.handler);\n\n\t\ttry {\n\t\t\tawait Promise.all(handlers.map((h) => h(event.payload, event.meta)));\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = 'processed', processed_at = now()\n\t\t\t\t WHERE event_id = $1 AND consumer = $2`,\n\t\t\t\t[eventId, consumer],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = CASE WHEN attempts >= $1 THEN 'dead' ELSE 'failed' END,\n\t\t\t\t error = $2\n\t\t\t\t WHERE event_id = $3 AND consumer = $4`,\n\t\t\t\t[this.maxRetries, err instanceof Error ? err.message : String(err), eventId, consumer],\n\t\t\t);\n\t\t}\n\t}\n\n\t// ── Helpers ─────────────────────────────────────────────────────\n\n\tprivate matchingConsumers(eventType: string): string[] {\n\t\tconst seen = new Set<string>();\n\t\tfor (const sub of this.subscriptions) {\n\t\t\tif (matchesPattern(sub.pattern, eventType)) seen.add(sub.consumer);\n\t\t}\n\t\treturn [...seen];\n\t}\n\n\tprivate sleep(): Promise<void> {\n\t\treturn new Promise<void>((resolve) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\t\tconst wake = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tconst idx = this.wakeResolvers.indexOf(wake);\n\t\t\t\tif (idx !== -1) this.wakeResolvers.splice(idx, 1);\n\t\t\t\tresolve();\n\t\t\t}, this.pollInterval);\n\t\t\tthis.wakeResolvers.push(wake);\n\t\t});\n\t}\n\n\tprivate wake(): void {\n\t\tthis.wakeResolvers.shift()?.();\n\t}\n}\n","// Glob matching for event topic patterns.\n// '*' alone matches everything. Otherwise '*' is a wildcard for any\n// characters including dots, so 'sport.*' matches 'sport.event.created'.\nexport function matchesPattern(pattern: string, eventType: string): boolean {\n\tif (pattern === '*') return true;\n\tconst regex = new RegExp('^' + pattern.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*') + '$');\n\treturn regex.test(eventType);\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport type { IStoreAdapter } from '@fonderie/store';\n\n// Tamper-evidence for the append-only event log. Each row carries an HMAC-SHA256\n// over its immutable content, keyed by a server-held secret. An auditor (or a\n// scheduled job) re-derives every HMAC and compares: any modified or forged row\n// fails, because rewriting it without the key can't produce a matching HMAC.\n//\n// Scope: this detects *content* tampering and forged rows. It does not by itself\n// prove no whole row was deleted — that is the job of append-only grants,\n// restricted DB permissions, and backups. Kept deliberately keyed-per-row (not a\n// prev-hash chain) so publishing stays lock-free on the hot event-bus path.\n\n// Deterministic JSON: recursively sort object keys so the same logical value\n// always serialises identically, regardless of insertion order or a JSONB\n// round-trip through Postgres.\nexport function canonicalize(value: unknown): string {\n\treturn JSON.stringify(sortKeys(value));\n}\n\nfunction sortKeys(value: unknown): unknown {\n\tif (Array.isArray(value)) return value.map(sortKeys);\n\tif (value && typeof value === 'object') {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const k of Object.keys(value as Record<string, unknown>).sort()) {\n\t\t\tout[k] = sortKeys((value as Record<string, unknown>)[k]);\n\t\t}\n\t\treturn out;\n\t}\n\treturn value;\n}\n\nexport interface IHashableEvent {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n}\n\n// The HMAC over an event's immutable fields. Field separators (\\n) are safe\n// because they can't appear unescaped inside a JSON string or a UUID/type.\nexport function computeEventHmac(key: string, event: IHashableEvent): string {\n\treturn createHmac('sha256', key)\n\t\t.update(event.id)\n\t\t.update('\\n')\n\t\t.update(event.type)\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.payload))\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.meta))\n\t\t.digest('hex');\n}\n\nfunction hmacEquals(a: string, b: string): boolean {\n\tconst bufA = Buffer.from(a);\n\tconst bufB = Buffer.from(b);\n\treturn bufA.length === bufB.length && timingSafeEqual(bufA, bufB);\n}\n\nexport interface IIntegrityReport {\n\t// True when every HMAC-carrying row verified.\n\tok: boolean;\n\t// Rows that carried an HMAC and were checked.\n\tchecked: number;\n\t// Rows with no HMAC (published before integrity was enabled) — skipped.\n\tunprotected: number;\n\t// Ids of rows whose stored HMAC did not match a fresh computation.\n\ttampered: string[];\n}\n\ninterface IRawEventRow {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n\thmac: string | null;\n}\n\n// Walk the whole event log and re-verify every HMAC-carrying row. Intended for a\n// scheduled integrity job or an on-demand audit endpoint.\nexport async function verifyEventChain(store: IStoreAdapter, key: string): Promise<IIntegrityReport> {\n\tconst rows = await store.query<IRawEventRow>(\n\t\t`SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`,\n\t);\n\n\tconst report: IIntegrityReport = { ok: true, checked: 0, unprotected: 0, tampered: [] };\n\n\tfor (const row of rows) {\n\t\tif (row.hmac === null) {\n\t\t\treport.unprotected += 1;\n\t\t\tcontinue;\n\t\t}\n\t\treport.checked += 1;\n\t\tconst expected = computeEventHmac(key, row);\n\t\tif (!hmacEquals(expected, row.hmac)) {\n\t\t\treport.ok = false;\n\t\t\treport.tampered.push(row.id);\n\t\t}\n\t}\n\n\treturn report;\n}\n","import type { IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport { EventBus } from './bus';\nimport { PGTransport } from './transports/pg';\nimport type { IEventTransport } from './transports/types';\n\nexport type EventTransportConfig =\n\t| {\n\t\t\ttype: 'pg';\n\t\t\tconnectionUrl: string;\n\t\t\tmaxRetries?: number;\n\t\t\tbatchSize?: number;\n\t\t\tpollInterval?: number;\n\t\t\t// Enables tamper-evident audit logging (keyed HMAC per event).\n\t\t\tintegrityKey?: string;\n\t }\n\t| IEventTransport;\n\nexport interface IEventsConfig {\n\ttransport: EventTransportConfig;\n}\n\nfunction resolveTransport(config: EventTransportConfig): IEventTransport {\n\tif ('type' in config && config.type === 'pg') {\n\t\treturn new PGTransport({\n\t\t\tconnectionUrl: config.connectionUrl,\n\t\t\t...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),\n\t\t\t...(config.batchSize !== undefined ? { batchSize: config.batchSize } : {}),\n\t\t\t...(config.pollInterval !== undefined ? { pollInterval: config.pollInterval } : {}),\n\t\t\t...(config.integrityKey !== undefined ? { integrityKey: config.integrityKey } : {}),\n\t\t});\n\t}\n\n\treturn config as IEventTransport;\n}\n\nexport class EventsModule implements IFonderieModule {\n\treadonly name = '@fonderie/events';\n\treadonly bus: EventBus;\n\tprivate readonly config: IEventsConfig;\n\n\tconstructor(config: IEventsConfig) {\n\t\tthis.config = config;\n\t\tthis.bus = new EventBus(resolveTransport(config.transport));\n\t}\n\n\tinstall(_app: IFonderieApp): void {\n\t\tthis.bus.start().catch((err) => console.error('[events] failed to start transport', err));\n\t}\n\n\t// The event log doubles as the audit trail. Without an integrityKey it is\n\t// append-only but not tamper-evident, so a compromised DB write could alter\n\t// history undetectably — a finding worth surfacing (not fatal).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\tconst t = this.config.transport;\n\t\tif ('type' in t && t.type === 'pg' && !t.integrityKey) {\n\t\t\treturn [{\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: 'warning',\n\t\t\t\tmessage: 'no integrityKey — the event/audit log is not tamper-evident; set one to enable per-event HMACs',\n\t\t\t}];\n\t\t}\n\t\treturn [];\n\t}\n}\n","import type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler } from '../types';\nimport { matchesPattern } from './pattern';\n\nexport class MemoryTransport implements IEventTransport {\n\tprivate subscriptions: Array<{ pattern: string; handler: IEventHandler }> = [];\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst matching = this.subscriptions.filter((s) => matchesPattern(s.pattern, type));\n\t\tawait Promise.all(matching.map((s) => s.handler(payload, meta)));\n\t}\n\n\tsubscribe(pattern: string, handler: IEventHandler, _consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler });\n\t}\n\n\tasync start(): Promise<void> {}\n\tasync stop(): Promise<void> {}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport { verifyEventChain, type IIntegrityReport } from './integrity';\n\n// Scheduled tamper-detection for the audit/event log (SOC 2 CC7.2). Runs\n// `verifyEventChain` on an interval; if any HMAC-carrying row fails, it fires\n// `onTamper` — wire that to your alerting. Runs once immediately, then every\n// `intervalMs`. Non-blocking (the timer is unref'd). Call `.stop()` to cancel.\n\nexport interface IIntegrityCheckOptions {\n\t// Default 24h.\n\tintervalMs?: number;\n\t// Called after every run (ok or not) — e.g. to record a heartbeat.\n\tonResult?: (report: IIntegrityReport) => void;\n\t// Called only when the log failed verification. Defaults to a loud\n\t// console.error naming the tampered rows — override to page/alert.\n\tonTamper?: (report: IIntegrityReport) => void;\n}\n\nexport interface IIntegrityCheckHandle {\n\tstop: () => void;\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport function startIntegrityCheck(\n\tstore: IStoreAdapter,\n\tkey: string,\n\toptions: IIntegrityCheckOptions = {},\n): IIntegrityCheckHandle {\n\tconst intervalMs = options.intervalMs ?? DAY_MS;\n\tconst onTamper = options.onTamper ?? defaultTamperHandler;\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst report = await verifyEventChain(store, key);\n\t\t\toptions.onResult?.(report);\n\t\t\tif (!report.ok) onTamper(report);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] integrity check failed to run:', err);\n\t\t}\n\t};\n\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') {\n\t\t(timer as { unref: () => void }).unref();\n\t}\n\tvoid run(); // fire once immediately\n\n\treturn {\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tclearInterval(timer);\n\t\t},\n\t};\n}\n\nfunction defaultTamperHandler(report: IIntegrityReport): void {\n\tconsole.error(\n\t\t`[events] AUDIT LOG INTEGRITY FAILURE — ${report.tampered.length} tampered row(s) ` +\n\t\t\t`out of ${report.checked} checked: ${report.tampered.join(', ')}`,\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Retention for the append-only event/audit log. Events accumulate forever by\n// default; a retention policy disposes of them once they age past the window.\n// Call from a scheduled job. Per-consumer delivery rows are removed by\n// ON DELETE CASCADE. Note: purging is disposal, not tampering — it removes whole\n// aged rows wholesale and does not touch the HMAC of anything it keeps.\n\nexport interface IPurgeEventsOptions {\n\t// Delete events whose created_at is older than this many days.\n\tolderThanDays: number;\n}\n\nexport async function purgeEvents(\n\tstore: IStoreAdapter,\n\t{ olderThanDays }: IPurgeEventsOptions,\n): Promise<number> {\n\tif (!Number.isFinite(olderThanDays) || olderThanDays < 0) {\n\t\tthrow new Error('[events] purgeEvents: olderThanDays must be a non-negative number');\n\t}\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_events\n\t\t WHERE created_at < now() - make_interval(days => $1)\n\t\t RETURNING id`,\n\t\t[olderThanDays],\n\t);\n\treturn rows.length;\n}\n\n// Scheduled disposal (SOC 2 C1/P4). Runs purgeEvents on an interval so aged\n// audit/event rows don't accumulate past the policy window. Runs once\n// immediately, then every intervalMs. Non-blocking (timer unref'd). .stop() cancels.\nexport interface IRetentionScheduleOptions extends IPurgeEventsOptions {\n\tintervalMs?: number; // default 24h\n\tonPurge?: (deleted: number) => void;\n}\n\nexport function startEventRetention(\n\tstore: IStoreAdapter,\n\toptions: IRetentionScheduleOptions,\n): { stop: () => void } {\n\tconst intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1000;\n\tlet stopped = false;\n\tconst run = async () => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst deleted = await purgeEvents(store, { olderThanDays: options.olderThanDays });\n\t\t\toptions.onPurge?.(deleted);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] scheduled retention purge failed:', err);\n\t\t}\n\t};\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') (timer as { unref: () => void }).unref();\n\tvoid run();\n\treturn { stop: () => { stopped = true; clearInterval(timer); } };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAA2B;AAKpB,IAAM,WAAN,MAAe;AAAA,EACrB,YAAoB,WAA4B;AAA5B;AAAA,EAA6B;AAAA,EAA7B;AAAA,EAEpB,MAAM,KAAkB,MAAc,SAAY,MAA8C;AAC/F,UAAM,OAAmB;AAAA,MACxB,QAAI,+BAAW;AAAA,MACf;AAAA,MACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,UAAU;AAAA,MACV,GAAI,MAAM,cAAc,SAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,IACtE;AACA,UAAM,KAAK,UAAU,QAAQ,MAAM,SAAS,IAAI;AAAA,EACjD;AAAA;AAAA;AAAA,EAIA,GAAgB,MAAc,SAA2B,WAAmB,MAAY;AACvF,SAAK,UAAU,UAAU,MAAM,SAA0B,QAAQ;AAAA,EAClE;AAAA,EAEA,MAAM,QAAuB;AAC5B,UAAM,KAAK,UAAU,MAAM;AAAA,EAC5B;AAAA,EAEA,MAAM,OAAsB;AAC3B,UAAM,KAAK,UAAU,KAAK;AAAA,EAC3B;AACD;;;AChCA,gBAAe;AAEf,mBAA0B;;;ACCnB,SAAS,eAAe,SAAiB,WAA4B;AAC3E,MAAI,YAAY,IAAK,QAAO;AAC5B,QAAM,QAAQ,IAAI,OAAO,MAAM,QAAQ,QAAQ,OAAO,KAAK,EAAE,QAAQ,OAAO,IAAI,IAAI,GAAG;AACvF,SAAO,MAAM,KAAK,SAAS;AAC5B;;;ACPA,IAAAA,sBAA4C;AAiBrC,SAAS,aAAa,OAAwB;AACpD,SAAO,KAAK,UAAU,SAAS,KAAK,CAAC;AACtC;AAEA,SAAS,SAAS,OAAyB;AAC1C,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,QAAQ;AACnD,MAAI,SAAS,OAAO,UAAU,UAAU;AACvC,UAAM,MAA+B,CAAC;AACtC,eAAW,KAAK,OAAO,KAAK,KAAgC,EAAE,KAAK,GAAG;AACrE,UAAI,CAAC,IAAI,SAAU,MAAkC,CAAC,CAAC;AAAA,IACxD;AACA,WAAO;AAAA,EACR;AACA,SAAO;AACR;AAWO,SAAS,iBAAiB,KAAa,OAA+B;AAC5E,aAAO,gCAAW,UAAU,GAAG,EAC7B,OAAO,MAAM,EAAE,EACf,OAAO,IAAI,EACX,OAAO,MAAM,IAAI,EACjB,OAAO,IAAI,EACX,OAAO,aAAa,MAAM,OAAO,CAAC,EAClC,OAAO,IAAI,EACX,OAAO,aAAa,MAAM,IAAI,CAAC,EAC/B,OAAO,KAAK;AACf;AAEA,SAAS,WAAW,GAAW,GAAoB;AAClD,QAAM,OAAO,OAAO,KAAK,CAAC;AAC1B,QAAM,OAAO,OAAO,KAAK,CAAC;AAC1B,SAAO,KAAK,WAAW,KAAK,cAAU,qCAAgB,MAAM,IAAI;AACjE;AAuBA,eAAsB,iBAAiB,OAAsB,KAAwC;AACpG,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA,EACD;AAEA,QAAM,SAA2B,EAAE,IAAI,MAAM,SAAS,GAAG,aAAa,GAAG,UAAU,CAAC,EAAE;AAEtF,aAAW,OAAO,MAAM;AACvB,QAAI,IAAI,SAAS,MAAM;AACtB,aAAO,eAAe;AACtB;AAAA,IACD;AACA,WAAO,WAAW;AAClB,UAAM,WAAW,iBAAiB,KAAK,GAAG;AAC1C,QAAI,CAAC,WAAW,UAAU,IAAI,IAAI,GAAG;AACpC,aAAO,KAAK;AACZ,aAAO,SAAS,KAAK,IAAI,EAAE;AAAA,IAC5B;AAAA,EACD;AAEA,SAAO;AACR;;;AF5EO,IAAM,cAAN,MAA6C;AAAA,EAYnD,YAAoB,QAA4B;AAA5B;AACnB,SAAK,aAAa,OAAO,cAAc;AACvC,SAAK,YAAY,OAAO,aAAa;AACrC,SAAK,eAAe,OAAO,gBAAgB;AAC3C,SAAK,eAAe,OAAO;AAAA,EAC5B;AAAA,EALoB;AAAA,EAXZ,gBAAgC,CAAC;AAAA,EACjC,eAAiC;AAAA,EACjC;AAAA,EACA,UAAU;AAAA,EACV,gBAAmC,CAAC;AAAA,EAE3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAWjB,UAAU,SAAiB,SAAwB,UAAwB;AAC1E,SAAK,cAAc,KAAK,EAAE,SAAS,SAAS,SAAS,CAAC;AAAA,EACvD;AAAA,EAEA,MAAM,QAAQ,MAAc,SAAkB,MAAiC;AAC9E,UAAM,OAAO,KAAK,eACf,iBAAiB,KAAK,cAAc,EAAE,IAAI,KAAK,IAAI,MAAM,SAAS,KAAK,CAAC,IACxE;AACH,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA;AAAA,MAEA,CAAC,KAAK,IAAI,MAAM,KAAK,UAAU,OAAO,GAAG,KAAK,UAAU,IAAI,GAAG,IAAI;AAAA,IACpE;AAEA,UAAM,YAAY,KAAK,kBAAkB,IAAI;AAC7C,QAAI,UAAU,SAAS,GAAG;AACzB,YAAM,KAAK,MAAM;AAAA,QAChB;AAAA;AAAA;AAAA,QAGA,CAAC,KAAK,IAAI,SAAS;AAAA,MACpB;AAAA,IACD;AAGA,UAAM,KAAK,MAAM,MAAM,yCAAyC;AAAA,EACjE;AAAA,EAEA,MAAM,QAAuB;AAC5B,SAAK,UAAU;AACf,SAAK,QAAQ,IAAI,uBAAU,KAAK,OAAO,aAAa;AAGpD,UAAM,KAAK,MAAM;AAAA,MAChB;AAAA,IACD;AAEA,SAAK,eAAe,IAAI,UAAAC,QAAG,OAAO,KAAK,OAAO,aAAa;AAC3D,UAAM,KAAK,aAAa,QAAQ;AAChC,UAAM,KAAK,aAAa,MAAM,wBAAwB;AAEtD,SAAK,aAAa,GAAG,gBAAgB,MAAM,KAAK,KAAK,CAAC;AACtD,SAAK,aAAa;AAAA,MAAG;AAAA,MAAS,CAAC,QAC9B,QAAQ,MAAM,oCAAoC,IAAI,OAAO;AAAA,IAC9D;AAEA,SAAK,YAAY,EAAE,MAAM,CAAC,QAAQ,QAAQ,MAAM,kCAAkC,GAAG,CAAC;AAAA,EACvF;AAAA,EAEA,MAAM,OAAsB;AAC3B,SAAK,UAAU;AACf,SAAK,KAAK;AACV,UAAM,KAAK,cAAc,IAAI;AAC7B,SAAK,eAAe;AAAA,EACrB;AAAA;AAAA,EAIA,MAAc,cAA6B;AAC1C,WAAO,KAAK,SAAS;AACpB,UAAI;AACH,cAAM,UAAU,MAAM,KAAK,iBAAiB;AAC5C,YAAI,CAAC,QAAS,OAAM,KAAK,MAAM;AAAA,MAChC,SAAS,KAAK;AACb,gBAAQ,MAAM,2BAA2B,GAAG;AAC5C,cAAM,KAAK,MAAM;AAAA,MAClB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAc,mBAAqC;AAClD,UAAM,YAAY,CAAC,GAAG,IAAI,IAAI,KAAK,cAAc,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACxE,UAAM,UAAU,MAAM,QAAQ,IAAI,UAAU,IAAI,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC,CAAC;AAC5E,WAAO,QAAQ,KAAK,CAAC,MAAM,IAAI,CAAC;AAAA,EACjC;AAAA,EAEA,MAAc,aAAa,UAAmC;AAC7D,UAAM,UAAU,MAAM,KAAK,MAAM;AAAA,MAChC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAeA,CAAC,UAAU,KAAK,YAAY,KAAK,SAAS;AAAA,IAC3C;AAEA,UAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,QAAQ,KAAK,qBAAqB,UAAU,IAAI,QAAQ,CAAC,CAAC;AACzF,WAAO,QAAQ;AAAA,EAChB;AAAA;AAAA,EAIA,MAAc,qBAAqB,UAAkB,SAAgC;AACpF,UAAM,CAAC,KAAK,IAAI,MAAM,KAAK,MAAM;AAAA,MAChC;AAAA,MACA,CAAC,OAAO;AAAA,IACT;AACA,QAAI,CAAC,MAAO;AAEZ,UAAM,WAAW,KAAK,cACpB,OAAO,CAAC,MAAM,EAAE,aAAa,YAAY,eAAe,EAAE,SAAS,MAAM,IAAI,CAAC,EAC9E,IAAI,CAAC,MAAM,EAAE,OAAO;AAEtB,QAAI;AACH,YAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,MAAM,SAAS,MAAM,IAAI,CAAC,CAAC;AACnE,YAAM,KAAK,MAAM;AAAA,QAChB;AAAA;AAAA;AAAA,QAGA,CAAC,SAAS,QAAQ;AAAA,MACnB;AAAA,IACD,SAAS,KAAK;AACb,YAAM,KAAK,MAAM;AAAA,QAChB;AAAA;AAAA;AAAA;AAAA,QAIA,CAAC,KAAK,YAAY,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG,SAAS,QAAQ;AAAA,MACtF;AAAA,IACD;AAAA,EACD;AAAA;AAAA,EAIQ,kBAAkB,WAA6B;AACtD,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,OAAO,KAAK,eAAe;AACrC,UAAI,eAAe,IAAI,SAAS,SAAS,EAAG,MAAK,IAAI,IAAI,QAAQ;AAAA,IAClE;AACA,WAAO,CAAC,GAAG,IAAI;AAAA,EAChB;AAAA,EAEQ,QAAuB;AAC9B,WAAO,IAAI,QAAc,CAAC,YAAY;AACrC,UAAI;AACJ,YAAM,OAAO,MAAM;AAClB,qBAAa,KAAK;AAClB,gBAAQ;AAAA,MACT;AACA,cAAQ,WAAW,MAAM;AACxB,cAAM,MAAM,KAAK,cAAc,QAAQ,IAAI;AAC3C,YAAI,QAAQ,GAAI,MAAK,cAAc,OAAO,KAAK,CAAC;AAChD,gBAAQ;AAAA,MACT,GAAG,KAAK,YAAY;AACpB,WAAK,cAAc,KAAK,IAAI;AAAA,IAC7B,CAAC;AAAA,EACF;AAAA,EAEQ,OAAa;AACpB,SAAK,cAAc,MAAM,IAAI;AAAA,EAC9B;AACD;;;AG1LA,SAAS,iBAAiB,QAA+C;AACxE,MAAI,UAAU,UAAU,OAAO,SAAS,MAAM;AAC7C,WAAO,IAAI,YAAY;AAAA,MACtB,eAAe,OAAO;AAAA,MACtB,GAAI,OAAO,eAAe,SAAY,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;AAAA,MAC3E,GAAI,OAAO,cAAc,SAAY,EAAE,WAAW,OAAO,UAAU,IAAI,CAAC;AAAA,MACxE,GAAI,OAAO,iBAAiB,SAAY,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,MACjF,GAAI,OAAO,iBAAiB,SAAY,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,IAClF,CAAC;AAAA,EACF;AAEA,SAAO;AACR;AAEO,IAAM,eAAN,MAA8C;AAAA,EAC3C,OAAO;AAAA,EACP;AAAA,EACQ;AAAA,EAEjB,YAAY,QAAuB;AAClC,SAAK,SAAS;AACd,SAAK,MAAM,IAAI,SAAS,iBAAiB,OAAO,SAAS,CAAC;AAAA,EAC3D;AAAA,EAEA,QAAQ,MAA0B;AACjC,SAAK,IAAI,MAAM,EAAE,MAAM,CAAC,QAAQ,QAAQ,MAAM,sCAAsC,GAAG,CAAC;AAAA,EACzF;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAsC;AACrC,UAAM,IAAI,KAAK,OAAO;AACtB,QAAI,UAAU,KAAK,EAAE,SAAS,QAAQ,CAAC,EAAE,cAAc;AACtD,aAAO,CAAC;AAAA,QACP,QAAQ,KAAK;AAAA,QACb,UAAU;AAAA,QACV,SAAS;AAAA,MACV,CAAC;AAAA,IACF;AACA,WAAO,CAAC;AAAA,EACT;AACD;;;AC5DO,IAAM,kBAAN,MAAiD;AAAA,EAC/C,gBAAoE,CAAC;AAAA,EAE7E,MAAM,QAAQ,MAAc,SAAkB,MAAiC;AAC9E,UAAM,WAAW,KAAK,cAAc,OAAO,CAAC,MAAM,eAAe,EAAE,SAAS,IAAI,CAAC;AACjF,UAAM,QAAQ,IAAI,SAAS,IAAI,CAAC,MAAM,EAAE,QAAQ,SAAS,IAAI,CAAC,CAAC;AAAA,EAChE;AAAA,EAEA,UAAU,SAAiB,SAAwB,WAAyB;AAC3E,SAAK,cAAc,KAAK,EAAE,SAAS,QAAQ,CAAC;AAAA,EAC7C;AAAA,EAEA,MAAM,QAAuB;AAAA,EAAC;AAAA,EAC9B,MAAM,OAAsB;AAAA,EAAC;AAC9B;;;ACKA,IAAM,SAAS,KAAK,KAAK,KAAK;AAEvB,SAAS,oBACf,OACA,KACA,UAAkC,CAAC,GACX;AACxB,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,WAAW,QAAQ,YAAY;AACrC,MAAI,UAAU;AAEd,QAAM,MAAM,YAA2B;AACtC,QAAI,QAAS;AACb,QAAI;AACH,YAAM,SAAS,MAAM,iBAAiB,OAAO,GAAG;AAChD,cAAQ,WAAW,MAAM;AACzB,UAAI,CAAC,OAAO,GAAI,UAAS,MAAM;AAAA,IAChC,SAAS,KAAK;AACb,cAAQ,MAAM,2CAA2C,GAAG;AAAA,IAC7D;AAAA,EACD;AAEA,QAAM,QAAQ,YAAY,KAAK,UAAU;AACzC,MAAI,OAAQ,MAAiC,UAAU,YAAY;AAClE,IAAC,MAAgC,MAAM;AAAA,EACxC;AACA,OAAK,IAAI;AAET,SAAO;AAAA,IACN,MAAM,MAAM;AACX,gBAAU;AACV,oBAAc,KAAK;AAAA,IACpB;AAAA,EACD;AACD;AAEA,SAAS,qBAAqB,QAAgC;AAC7D,UAAQ;AAAA,IACP,+CAA0C,OAAO,SAAS,MAAM,2BACrD,OAAO,OAAO,aAAa,OAAO,SAAS,KAAK,IAAI,CAAC;AAAA,EACjE;AACD;;;ACnDA,eAAsB,YACrB,OACA,EAAE,cAAc,GACE;AAClB,MAAI,CAAC,OAAO,SAAS,aAAa,KAAK,gBAAgB,GAAG;AACzD,UAAM,IAAI,MAAM,mEAAmE;AAAA,EACpF;AACA,QAAM,OAAO,MAAM,MAAM;AAAA,IACxB;AAAA;AAAA;AAAA,IAGA,CAAC,aAAa;AAAA,EACf;AACA,SAAO,KAAK;AACb;AAUO,SAAS,oBACf,OACA,SACuB;AACvB,QAAM,aAAa,QAAQ,cAAc,KAAK,KAAK,KAAK;AACxD,MAAI,UAAU;AACd,QAAM,MAAM,YAAY;AACvB,QAAI,QAAS;AACb,QAAI;AACH,YAAM,UAAU,MAAM,YAAY,OAAO,EAAE,eAAe,QAAQ,cAAc,CAAC;AACjF,cAAQ,UAAU,OAAO;AAAA,IAC1B,SAAS,KAAK;AACb,cAAQ,MAAM,8CAA8C,GAAG;AAAA,IAChE;AAAA,EACD;AACA,QAAM,QAAQ,YAAY,KAAK,UAAU;AACzC,MAAI,OAAQ,MAAiC,UAAU,WAAY,CAAC,MAAgC,MAAM;AAC1G,OAAK,IAAI;AACT,SAAO,EAAE,MAAM,MAAM;AAAE,cAAU;AAAM,kBAAc,KAAK;AAAA,EAAG,EAAE;AAChE;;;AR9BO,IAAM,qBAAqB;","names":["import_node_crypto","pg"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/bus.ts","../src/transports/pg.ts","../src/transports/pattern.ts","../src/integrity.ts","../src/module.ts","../src/transports/memory.ts","../src/integrity-job.ts","../src/retention.ts"],"sourcesContent":["export { EventBus } from './bus';\nexport { EventsModule } from './module';\nexport type { IEventsConfig, EventTransportConfig } from './module';\n\nexport { MemoryTransport, PGTransport } from './transports';\nexport type { IEventTransport, IPGTransportConfig } from './transports';\n\nexport { matchesPattern } from './transports/pattern';\n\n// Audit-log tamper-evidence\nexport { computeEventHmac, verifyEventChain, canonicalize } from './integrity';\nexport { startIntegrityCheck } from './integrity-job';\nexport type { IIntegrityCheckOptions, IIntegrityCheckHandle } from './integrity-job';\nexport type { IHashableEvent, IIntegrityReport } from './integrity';\n\n// Retention / disposal\nexport { purgeEvents, startEventRetention } from './retention';\nexport type { IPurgeEventsOptions, IRetentionScheduleOptions } from './retention';\n\nexport type { IEventMeta, IEventHandler, IEventRecord, IConsumerRecord } from './types';\n\n// ── Typed event keys ─────────────────────────────────────────────\n// Each domain package re-exports its own EVENT_KEYS.\n// Consumers alias on import:\n// import { EVENT_KEYS as AUTH_EVENT_KEYS } from '@fonderie/auth'\n\nexport const NOTIFICATION_EVENT = 'fonderie.notification.send' as const;\nexport type NotificationEvent = typeof NOTIFICATION_EVENT;\n","import { randomUUID } from 'node:crypto';\n\nimport type { IEventTransport } from './transports/types';\nimport type { IEventMeta, IEventHandler } from './types';\n\nexport class EventBus {\n\tconstructor(private transport: IEventTransport) {}\n\n\tasync emit<T = unknown>(type: string, payload: T, opts?: { requestId?: string }): Promise<void> {\n\t\tconst meta: IEventMeta = {\n\t\t\tid: randomUUID(),\n\t\t\ttype,\n\t\t\temittedAt: new Date().toISOString(),\n\t\t\tattempts: 0,\n\t\t\t...(opts?.requestId !== undefined ? { requestId: opts.requestId } : {}),\n\t\t};\n\t\tawait this.transport.publish(type, payload, meta);\n\t}\n\n\t// consumer identifies the logical subscriber for per-consumer delivery tracking.\n\t// Defaults to the pattern string — stable and predictable for single-subscriber patterns.\n\ton<T = unknown>(type: string, handler: IEventHandler<T>, consumer: string = type): void {\n\t\tthis.transport.subscribe(type, handler as IEventHandler, consumer);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tawait this.transport.start();\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.transport.stop();\n\t}\n}\n","import pg from 'pg';\n\nimport { PGAdapter } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler, IEventRecord } from '../types';\nimport { matchesPattern } from './pattern';\nimport { computeEventHmac } from '../integrity';\n\nexport interface IPGTransportConfig {\n\tconnectionUrl: string;\n\tmaxRetries?: number; // default 3\n\tbatchSize?: number; // default 10 rows claimed per consumer per poll cycle\n\tpollInterval?: number; // default 1000ms fallback poll when no NOTIFY arrives\n\t// When set, every event is stored with a keyed HMAC over its immutable\n\t// content, making the audit log tamper-evident. Unset → no HMAC (unchanged\n\t// behaviour). Verify later with `verifyEventChain(store, integrityKey)`.\n\tintegrityKey?: string;\n}\n\ninterface Subscription {\n\tpattern: string;\n\thandler: IEventHandler;\n\tconsumer: string;\n}\n\nexport class PGTransport implements IEventTransport {\n\tprivate subscriptions: Subscription[] = [];\n\tprivate listenClient: pg.Client | null = null;\n\tprivate store!: IStoreAdapter;\n\tprivate running = false;\n\tprivate wakeResolvers: Array<() => void> = [];\n\n\tprivate readonly maxRetries: number;\n\tprivate readonly batchSize: number;\n\tprivate readonly pollInterval: number;\n\tprivate readonly integrityKey: string | undefined;\n\n\tconstructor(private config: IPGTransportConfig) {\n\t\tthis.maxRetries = config.maxRetries ?? 3;\n\t\tthis.batchSize = config.batchSize ?? 10;\n\t\tthis.pollInterval = config.pollInterval ?? 1_000;\n\t\tthis.integrityKey = config.integrityKey;\n\t}\n\n\t// ── Public API ──────────────────────────────────────────────────\n\n\tsubscribe(pattern: string, handler: IEventHandler, consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler, consumer });\n\t}\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst hmac = this.integrityKey\n\t\t\t? computeEventHmac(this.integrityKey, { id: meta.id, type, payload, meta })\n\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_events (id, type, payload, meta, hmac)\n\t\t\t VALUES ($1, $2, $3, $4, $5)`,\n\t\t\t[meta.id, type, JSON.stringify(payload), JSON.stringify(meta), hmac],\n\t\t);\n\n\t\tconst consumers = this.matchingConsumers(type);\n\t\tif (consumers.length > 0) {\n\t\t\tawait this.store.query(\n\t\t\t\t`INSERT INTO fonderie_event_consumers (event_id, consumer, status, attempts)\n\t\t\t\t SELECT $1, unnest($2::text[]), 'pending', 0\n\t\t\t\t ON CONFLICT (event_id, consumer) DO NOTHING`,\n\t\t\t\t[meta.id, consumers],\n\t\t\t);\n\t\t}\n\n\t\t// NOTIFY carries no payload — it is a wake signal only\n\t\tawait this.store.query(`SELECT pg_notify('fonderie_events', '')`);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.running = true;\n\t\tthis.store = new PGAdapter(this.config.connectionUrl);\n\n\t\t// Reset any rows left in 'processing' by a crashed instance\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_event_consumers SET status = 'failed' WHERE status = 'processing'`,\n\t\t);\n\n\t\tthis.listenClient = new pg.Client(this.config.connectionUrl);\n\t\tawait this.listenClient.connect();\n\t\tawait this.listenClient.query('LISTEN fonderie_events');\n\n\t\tthis.listenClient.on('notification', () => this.wake());\n\t\tthis.listenClient.on('error', (err) =>\n\t\t\tconsole.error('[events:pg] listen client error:', err.message),\n\t\t);\n\n\t\tthis.runPollLoop().catch((err) => console.error('[events:pg] poll loop crashed:', err));\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tthis.running = false;\n\t\tthis.wake();\n\t\tawait this.listenClient?.end();\n\t\tthis.listenClient = null;\n\t}\n\n\t// ── Poll loop ───────────────────────────────────────────────────\n\n\tprivate async runPollLoop(): Promise<void> {\n\t\twhile (this.running) {\n\t\t\ttry {\n\t\t\t\tconst hadWork = await this.pollAllConsumers();\n\t\t\t\tif (!hadWork) await this.sleep();\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error('[events:pg] poll error:', err);\n\t\t\t\tawait this.sleep();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async pollAllConsumers(): Promise<boolean> {\n\t\tconst consumers = [...new Set(this.subscriptions.map((s) => s.consumer))];\n\t\tconst results = await Promise.all(consumers.map((c) => this.pollConsumer(c)));\n\t\treturn results.some((n) => n > 0);\n\t}\n\n\tprivate async pollConsumer(consumer: string): Promise<number> {\n\t\tconst claimed = await this.store.query<{ event_id: string }>(\n\t\t\t`UPDATE fonderie_event_consumers c\n\t\t\t SET status = 'processing', attempts = c.attempts + 1\n\t\t\t FROM (\n\t\t\t SELECT event_id\n\t\t\t FROM fonderie_event_consumers\n\t\t\t WHERE consumer = $1\n\t\t\t AND status IN ('pending', 'failed')\n\t\t\t AND attempts < $2\n\t\t\t ORDER BY event_id\n\t\t\t LIMIT $3\n\t\t\t FOR UPDATE SKIP LOCKED\n\t\t\t ) AS locked\n\t\t\t WHERE c.event_id = locked.event_id\n\t\t\t AND c.consumer = $1\n\t\t\t RETURNING c.event_id`,\n\t\t\t[consumer, this.maxRetries, this.batchSize],\n\t\t);\n\n\t\tawait Promise.all(claimed.map((row) => this.processConsumerEvent(consumer, row.event_id)));\n\t\treturn claimed.length;\n\t}\n\n\t// ── Event processing ────────────────────────────────────────────\n\n\tprivate async processConsumerEvent(consumer: string, eventId: string): Promise<void> {\n\t\tconst [event] = await this.store.query<IEventRecord>(\n\t\t\t`SELECT type, payload, meta FROM fonderie_events WHERE id = $1`,\n\t\t\t[eventId],\n\t\t);\n\t\tif (!event) return;\n\n\t\tconst handlers = this.subscriptions\n\t\t\t.filter((s) => s.consumer === consumer && matchesPattern(s.pattern, event.type))\n\t\t\t.map((s) => s.handler);\n\n\t\ttry {\n\t\t\tawait Promise.all(handlers.map((h) => h(event.payload, event.meta)));\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = 'processed', processed_at = now()\n\t\t\t\t WHERE event_id = $1 AND consumer = $2`,\n\t\t\t\t[eventId, consumer],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = CASE WHEN attempts >= $1 THEN 'dead' ELSE 'failed' END,\n\t\t\t\t error = $2\n\t\t\t\t WHERE event_id = $3 AND consumer = $4`,\n\t\t\t\t[this.maxRetries, err instanceof Error ? err.message : String(err), eventId, consumer],\n\t\t\t);\n\t\t}\n\t}\n\n\t// ── Helpers ─────────────────────────────────────────────────────\n\n\tprivate matchingConsumers(eventType: string): string[] {\n\t\tconst seen = new Set<string>();\n\t\tfor (const sub of this.subscriptions) {\n\t\t\tif (matchesPattern(sub.pattern, eventType)) seen.add(sub.consumer);\n\t\t}\n\t\treturn [...seen];\n\t}\n\n\tprivate sleep(): Promise<void> {\n\t\treturn new Promise<void>((resolve) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\t\tconst wake = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tconst idx = this.wakeResolvers.indexOf(wake);\n\t\t\t\tif (idx !== -1) this.wakeResolvers.splice(idx, 1);\n\t\t\t\tresolve();\n\t\t\t}, this.pollInterval);\n\t\t\tthis.wakeResolvers.push(wake);\n\t\t});\n\t}\n\n\tprivate wake(): void {\n\t\tthis.wakeResolvers.shift()?.();\n\t}\n}\n","// Glob matching for event topic patterns.\n// '*' alone matches everything. Otherwise '*' is a wildcard for any\n// characters including dots, so 'sport.*' matches 'sport.event.created'.\nexport function matchesPattern(pattern: string, eventType: string): boolean {\n\tif (pattern === '*') return true;\n\tconst regex = new RegExp('^' + pattern.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*') + '$');\n\treturn regex.test(eventType);\n}\n","import { createHmac } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\n\n// Tamper-evidence for the append-only event log. Each row carries an HMAC-SHA256\n// over its immutable content, keyed by a server-held secret. An auditor (or a\n// scheduled job) re-derives every HMAC and compares: any modified or forged row\n// fails, because rewriting it without the key can't produce a matching HMAC.\n//\n// Scope: this detects *content* tampering and forged rows. It does not by itself\n// prove no whole row was deleted — that is the job of append-only grants,\n// restricted DB permissions, and backups. Kept deliberately keyed-per-row (not a\n// prev-hash chain) so publishing stays lock-free on the hot event-bus path.\n\n// Deterministic JSON: recursively sort object keys so the same logical value\n// always serialises identically, regardless of insertion order or a JSONB\n// round-trip through Postgres.\nexport function canonicalize(value: unknown): string {\n\treturn JSON.stringify(sortKeys(value));\n}\n\nfunction sortKeys(value: unknown): unknown {\n\tif (Array.isArray(value)) return value.map(sortKeys);\n\tif (value && typeof value === 'object') {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const k of Object.keys(value as Record<string, unknown>).sort()) {\n\t\t\tout[k] = sortKeys((value as Record<string, unknown>)[k]);\n\t\t}\n\t\treturn out;\n\t}\n\treturn value;\n}\n\nexport interface IHashableEvent {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n}\n\n// The HMAC over an event's immutable fields. Field separators (\\n) are safe\n// because they can't appear unescaped inside a JSON string or a UUID/type.\nexport function computeEventHmac(key: string, event: IHashableEvent): string {\n\treturn createHmac('sha256', key)\n\t\t.update(event.id)\n\t\t.update('\\n')\n\t\t.update(event.type)\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.payload))\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.meta))\n\t\t.digest('hex');\n}\n\nexport interface IIntegrityReport {\n\t// True when every HMAC-carrying row verified.\n\tok: boolean;\n\t// Rows that carried an HMAC and were checked.\n\tchecked: number;\n\t// Rows with no HMAC (published before integrity was enabled) — skipped.\n\tunprotected: number;\n\t// Ids of rows whose stored HMAC did not match a fresh computation.\n\ttampered: string[];\n}\n\ninterface IRawEventRow {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n\thmac: string | null;\n}\n\n// Walk the whole event log and re-verify every HMAC-carrying row. Intended for a\n// scheduled integrity job or an on-demand audit endpoint.\nexport async function verifyEventChain(store: IStoreAdapter, key: string): Promise<IIntegrityReport> {\n\tconst rows = await store.query<IRawEventRow>(\n\t\t`SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`,\n\t);\n\n\tconst report: IIntegrityReport = { ok: true, checked: 0, unprotected: 0, tampered: [] };\n\n\tfor (const row of rows) {\n\t\tif (row.hmac === null) {\n\t\t\treport.unprotected += 1;\n\t\t\tcontinue;\n\t\t}\n\t\treport.checked += 1;\n\t\tconst expected = computeEventHmac(key, row);\n\t\tif (!constantTimeEqual(expected, row.hmac)) {\n\t\t\treport.ok = false;\n\t\t\treport.tampered.push(row.id);\n\t\t}\n\t}\n\n\treturn report;\n}\n","import type { IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport { EventBus } from './bus';\nimport { PGTransport } from './transports/pg';\nimport type { IEventTransport } from './transports/types';\n\nexport type EventTransportConfig =\n\t| {\n\t\t\ttype: 'pg';\n\t\t\tconnectionUrl: string;\n\t\t\tmaxRetries?: number;\n\t\t\tbatchSize?: number;\n\t\t\tpollInterval?: number;\n\t\t\t// Enables tamper-evident audit logging (keyed HMAC per event).\n\t\t\tintegrityKey?: string;\n\t }\n\t| IEventTransport;\n\nexport interface IEventsConfig {\n\ttransport: EventTransportConfig;\n}\n\nfunction resolveTransport(config: EventTransportConfig): IEventTransport {\n\tif ('type' in config && config.type === 'pg') {\n\t\treturn new PGTransport({\n\t\t\tconnectionUrl: config.connectionUrl,\n\t\t\t...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),\n\t\t\t...(config.batchSize !== undefined ? { batchSize: config.batchSize } : {}),\n\t\t\t...(config.pollInterval !== undefined ? { pollInterval: config.pollInterval } : {}),\n\t\t\t...(config.integrityKey !== undefined ? { integrityKey: config.integrityKey } : {}),\n\t\t});\n\t}\n\n\treturn config as IEventTransport;\n}\n\nexport class EventsModule implements IFonderieModule {\n\treadonly name = '@fonderie/events';\n\treadonly bus: EventBus;\n\tprivate readonly config: IEventsConfig;\n\n\tconstructor(config: IEventsConfig) {\n\t\tthis.config = config;\n\t\tthis.bus = new EventBus(resolveTransport(config.transport));\n\t}\n\n\tinstall(_app: IFonderieApp): void {\n\t\tthis.bus.start().catch((err) => console.error('[events] failed to start transport', err));\n\t}\n\n\t// The event log doubles as the audit trail. Without an integrityKey it is\n\t// append-only but not tamper-evident, so a compromised DB write could alter\n\t// history undetectably — a finding worth surfacing (not fatal).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\tconst t = this.config.transport;\n\t\tif ('type' in t && t.type === 'pg' && !t.integrityKey) {\n\t\t\treturn [{\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: 'warning',\n\t\t\t\tmessage: 'no integrityKey — the event/audit log is not tamper-evident; set one to enable per-event HMACs',\n\t\t\t}];\n\t\t}\n\t\treturn [];\n\t}\n}\n","import type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler } from '../types';\nimport { matchesPattern } from './pattern';\n\nexport class MemoryTransport implements IEventTransport {\n\tprivate subscriptions: Array<{ pattern: string; handler: IEventHandler }> = [];\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst matching = this.subscriptions.filter((s) => matchesPattern(s.pattern, type));\n\t\tawait Promise.all(matching.map((s) => s.handler(payload, meta)));\n\t}\n\n\tsubscribe(pattern: string, handler: IEventHandler, _consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler });\n\t}\n\n\tasync start(): Promise<void> {}\n\tasync stop(): Promise<void> {}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport { verifyEventChain, type IIntegrityReport } from './integrity';\n\n// Scheduled tamper-detection for the audit/event log (SOC 2 CC7.2). Runs\n// `verifyEventChain` on an interval; if any HMAC-carrying row fails, it fires\n// `onTamper` — wire that to your alerting. Runs once immediately, then every\n// `intervalMs`. Non-blocking (the timer is unref'd). Call `.stop()` to cancel.\n\nexport interface IIntegrityCheckOptions {\n\t// Default 24h.\n\tintervalMs?: number;\n\t// Called after every run (ok or not) — e.g. to record a heartbeat.\n\tonResult?: (report: IIntegrityReport) => void;\n\t// Called only when the log failed verification. Defaults to a loud\n\t// console.error naming the tampered rows — override to page/alert.\n\tonTamper?: (report: IIntegrityReport) => void;\n}\n\nexport interface IIntegrityCheckHandle {\n\tstop: () => void;\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport function startIntegrityCheck(\n\tstore: IStoreAdapter,\n\tkey: string,\n\toptions: IIntegrityCheckOptions = {},\n): IIntegrityCheckHandle {\n\tconst intervalMs = options.intervalMs ?? DAY_MS;\n\tconst onTamper = options.onTamper ?? defaultTamperHandler;\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst report = await verifyEventChain(store, key);\n\t\t\toptions.onResult?.(report);\n\t\t\tif (!report.ok) onTamper(report);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] integrity check failed to run:', err);\n\t\t}\n\t};\n\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') {\n\t\t(timer as { unref: () => void }).unref();\n\t}\n\tvoid run(); // fire once immediately\n\n\treturn {\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tclearInterval(timer);\n\t\t},\n\t};\n}\n\nfunction defaultTamperHandler(report: IIntegrityReport): void {\n\tconsole.error(\n\t\t`[events] AUDIT LOG INTEGRITY FAILURE — ${report.tampered.length} tampered row(s) ` +\n\t\t\t`out of ${report.checked} checked: ${report.tampered.join(', ')}`,\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Retention for the append-only event/audit log. Events accumulate forever by\n// default; a retention policy disposes of them once they age past the window.\n// Call from a scheduled job. Per-consumer delivery rows are removed by\n// ON DELETE CASCADE. Note: purging is disposal, not tampering — it removes whole\n// aged rows wholesale and does not touch the HMAC of anything it keeps.\n\nexport interface IPurgeEventsOptions {\n\t// Delete events whose created_at is older than this many days.\n\tolderThanDays: number;\n}\n\nexport async function purgeEvents(\n\tstore: IStoreAdapter,\n\t{ olderThanDays }: IPurgeEventsOptions,\n): Promise<number> {\n\tif (!Number.isFinite(olderThanDays) || olderThanDays < 0) {\n\t\tthrow new Error('[events] purgeEvents: olderThanDays must be a non-negative number');\n\t}\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_events\n\t\t WHERE created_at < now() - make_interval(days => $1)\n\t\t RETURNING id`,\n\t\t[olderThanDays],\n\t);\n\treturn rows.length;\n}\n\n// Scheduled disposal (SOC 2 C1/P4). Runs purgeEvents on an interval so aged\n// audit/event rows don't accumulate past the policy window. Runs once\n// immediately, then every intervalMs. Non-blocking (timer unref'd). .stop() cancels.\nexport interface IRetentionScheduleOptions extends IPurgeEventsOptions {\n\tintervalMs?: number; // default 24h\n\tonPurge?: (deleted: number) => void;\n}\n\nexport function startEventRetention(\n\tstore: IStoreAdapter,\n\toptions: IRetentionScheduleOptions,\n): { stop: () => void } {\n\tconst intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1000;\n\tlet stopped = false;\n\tconst run = async () => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst deleted = await purgeEvents(store, { olderThanDays: options.olderThanDays });\n\t\t\toptions.onPurge?.(deleted);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] scheduled retention purge failed:', err);\n\t\t}\n\t};\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') (timer as { unref: () => void }).unref();\n\tvoid run();\n\treturn { stop: () => { stopped = true; clearInterval(timer); } };\n}\n"],"mappings":"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","names":["import_node_crypto","pg"]}
package/dist/index.js CHANGED
@@ -40,7 +40,8 @@ function matchesPattern(pattern, eventType) {
40
40
  }
41
41
 
42
42
  // src/integrity.ts
43
- import { createHmac, timingSafeEqual } from "crypto";
43
+ import { createHmac } from "crypto";
44
+ import { constantTimeEqual } from "@fonderie/core";
44
45
  function canonicalize(value) {
45
46
  return JSON.stringify(sortKeys(value));
46
47
  }
@@ -58,11 +59,6 @@ function sortKeys(value) {
58
59
  function computeEventHmac(key, event) {
59
60
  return createHmac("sha256", key).update(event.id).update("\n").update(event.type).update("\n").update(canonicalize(event.payload)).update("\n").update(canonicalize(event.meta)).digest("hex");
60
61
  }
61
- function hmacEquals(a, b) {
62
- const bufA = Buffer.from(a);
63
- const bufB = Buffer.from(b);
64
- return bufA.length === bufB.length && timingSafeEqual(bufA, bufB);
65
- }
66
62
  async function verifyEventChain(store, key) {
67
63
  const rows = await store.query(
68
64
  `SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`
@@ -75,7 +71,7 @@ async function verifyEventChain(store, key) {
75
71
  }
76
72
  report.checked += 1;
77
73
  const expected = computeEventHmac(key, row);
78
- if (!hmacEquals(expected, row.hmac)) {
74
+ if (!constantTimeEqual(expected, row.hmac)) {
79
75
  report.ok = false;
80
76
  report.tampered.push(row.id);
81
77
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bus.ts","../src/transports/pg.ts","../src/transports/pattern.ts","../src/integrity.ts","../src/module.ts","../src/transports/memory.ts","../src/integrity-job.ts","../src/retention.ts","../src/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { IEventTransport } from './transports/types';\nimport type { IEventMeta, IEventHandler } from './types';\n\nexport class EventBus {\n\tconstructor(private transport: IEventTransport) {}\n\n\tasync emit<T = unknown>(type: string, payload: T, opts?: { requestId?: string }): Promise<void> {\n\t\tconst meta: IEventMeta = {\n\t\t\tid: randomUUID(),\n\t\t\ttype,\n\t\t\temittedAt: new Date().toISOString(),\n\t\t\tattempts: 0,\n\t\t\t...(opts?.requestId !== undefined ? { requestId: opts.requestId } : {}),\n\t\t};\n\t\tawait this.transport.publish(type, payload, meta);\n\t}\n\n\t// consumer identifies the logical subscriber for per-consumer delivery tracking.\n\t// Defaults to the pattern string — stable and predictable for single-subscriber patterns.\n\ton<T = unknown>(type: string, handler: IEventHandler<T>, consumer: string = type): void {\n\t\tthis.transport.subscribe(type, handler as IEventHandler, consumer);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tawait this.transport.start();\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.transport.stop();\n\t}\n}\n","import pg from 'pg';\n\nimport { PGAdapter } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler, IEventRecord } from '../types';\nimport { matchesPattern } from './pattern';\nimport { computeEventHmac } from '../integrity';\n\nexport interface IPGTransportConfig {\n\tconnectionUrl: string;\n\tmaxRetries?: number; // default 3\n\tbatchSize?: number; // default 10 rows claimed per consumer per poll cycle\n\tpollInterval?: number; // default 1000ms fallback poll when no NOTIFY arrives\n\t// When set, every event is stored with a keyed HMAC over its immutable\n\t// content, making the audit log tamper-evident. Unset → no HMAC (unchanged\n\t// behaviour). Verify later with `verifyEventChain(store, integrityKey)`.\n\tintegrityKey?: string;\n}\n\ninterface Subscription {\n\tpattern: string;\n\thandler: IEventHandler;\n\tconsumer: string;\n}\n\nexport class PGTransport implements IEventTransport {\n\tprivate subscriptions: Subscription[] = [];\n\tprivate listenClient: pg.Client | null = null;\n\tprivate store!: IStoreAdapter;\n\tprivate running = false;\n\tprivate wakeResolvers: Array<() => void> = [];\n\n\tprivate readonly maxRetries: number;\n\tprivate readonly batchSize: number;\n\tprivate readonly pollInterval: number;\n\tprivate readonly integrityKey: string | undefined;\n\n\tconstructor(private config: IPGTransportConfig) {\n\t\tthis.maxRetries = config.maxRetries ?? 3;\n\t\tthis.batchSize = config.batchSize ?? 10;\n\t\tthis.pollInterval = config.pollInterval ?? 1_000;\n\t\tthis.integrityKey = config.integrityKey;\n\t}\n\n\t// ── Public API ──────────────────────────────────────────────────\n\n\tsubscribe(pattern: string, handler: IEventHandler, consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler, consumer });\n\t}\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst hmac = this.integrityKey\n\t\t\t? computeEventHmac(this.integrityKey, { id: meta.id, type, payload, meta })\n\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_events (id, type, payload, meta, hmac)\n\t\t\t VALUES ($1, $2, $3, $4, $5)`,\n\t\t\t[meta.id, type, JSON.stringify(payload), JSON.stringify(meta), hmac],\n\t\t);\n\n\t\tconst consumers = this.matchingConsumers(type);\n\t\tif (consumers.length > 0) {\n\t\t\tawait this.store.query(\n\t\t\t\t`INSERT INTO fonderie_event_consumers (event_id, consumer, status, attempts)\n\t\t\t\t SELECT $1, unnest($2::text[]), 'pending', 0\n\t\t\t\t ON CONFLICT (event_id, consumer) DO NOTHING`,\n\t\t\t\t[meta.id, consumers],\n\t\t\t);\n\t\t}\n\n\t\t// NOTIFY carries no payload — it is a wake signal only\n\t\tawait this.store.query(`SELECT pg_notify('fonderie_events', '')`);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.running = true;\n\t\tthis.store = new PGAdapter(this.config.connectionUrl);\n\n\t\t// Reset any rows left in 'processing' by a crashed instance\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_event_consumers SET status = 'failed' WHERE status = 'processing'`,\n\t\t);\n\n\t\tthis.listenClient = new pg.Client(this.config.connectionUrl);\n\t\tawait this.listenClient.connect();\n\t\tawait this.listenClient.query('LISTEN fonderie_events');\n\n\t\tthis.listenClient.on('notification', () => this.wake());\n\t\tthis.listenClient.on('error', (err) =>\n\t\t\tconsole.error('[events:pg] listen client error:', err.message),\n\t\t);\n\n\t\tthis.runPollLoop().catch((err) => console.error('[events:pg] poll loop crashed:', err));\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tthis.running = false;\n\t\tthis.wake();\n\t\tawait this.listenClient?.end();\n\t\tthis.listenClient = null;\n\t}\n\n\t// ── Poll loop ───────────────────────────────────────────────────\n\n\tprivate async runPollLoop(): Promise<void> {\n\t\twhile (this.running) {\n\t\t\ttry {\n\t\t\t\tconst hadWork = await this.pollAllConsumers();\n\t\t\t\tif (!hadWork) await this.sleep();\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error('[events:pg] poll error:', err);\n\t\t\t\tawait this.sleep();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async pollAllConsumers(): Promise<boolean> {\n\t\tconst consumers = [...new Set(this.subscriptions.map((s) => s.consumer))];\n\t\tconst results = await Promise.all(consumers.map((c) => this.pollConsumer(c)));\n\t\treturn results.some((n) => n > 0);\n\t}\n\n\tprivate async pollConsumer(consumer: string): Promise<number> {\n\t\tconst claimed = await this.store.query<{ event_id: string }>(\n\t\t\t`UPDATE fonderie_event_consumers c\n\t\t\t SET status = 'processing', attempts = c.attempts + 1\n\t\t\t FROM (\n\t\t\t SELECT event_id\n\t\t\t FROM fonderie_event_consumers\n\t\t\t WHERE consumer = $1\n\t\t\t AND status IN ('pending', 'failed')\n\t\t\t AND attempts < $2\n\t\t\t ORDER BY event_id\n\t\t\t LIMIT $3\n\t\t\t FOR UPDATE SKIP LOCKED\n\t\t\t ) AS locked\n\t\t\t WHERE c.event_id = locked.event_id\n\t\t\t AND c.consumer = $1\n\t\t\t RETURNING c.event_id`,\n\t\t\t[consumer, this.maxRetries, this.batchSize],\n\t\t);\n\n\t\tawait Promise.all(claimed.map((row) => this.processConsumerEvent(consumer, row.event_id)));\n\t\treturn claimed.length;\n\t}\n\n\t// ── Event processing ────────────────────────────────────────────\n\n\tprivate async processConsumerEvent(consumer: string, eventId: string): Promise<void> {\n\t\tconst [event] = await this.store.query<IEventRecord>(\n\t\t\t`SELECT type, payload, meta FROM fonderie_events WHERE id = $1`,\n\t\t\t[eventId],\n\t\t);\n\t\tif (!event) return;\n\n\t\tconst handlers = this.subscriptions\n\t\t\t.filter((s) => s.consumer === consumer && matchesPattern(s.pattern, event.type))\n\t\t\t.map((s) => s.handler);\n\n\t\ttry {\n\t\t\tawait Promise.all(handlers.map((h) => h(event.payload, event.meta)));\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = 'processed', processed_at = now()\n\t\t\t\t WHERE event_id = $1 AND consumer = $2`,\n\t\t\t\t[eventId, consumer],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = CASE WHEN attempts >= $1 THEN 'dead' ELSE 'failed' END,\n\t\t\t\t error = $2\n\t\t\t\t WHERE event_id = $3 AND consumer = $4`,\n\t\t\t\t[this.maxRetries, err instanceof Error ? err.message : String(err), eventId, consumer],\n\t\t\t);\n\t\t}\n\t}\n\n\t// ── Helpers ─────────────────────────────────────────────────────\n\n\tprivate matchingConsumers(eventType: string): string[] {\n\t\tconst seen = new Set<string>();\n\t\tfor (const sub of this.subscriptions) {\n\t\t\tif (matchesPattern(sub.pattern, eventType)) seen.add(sub.consumer);\n\t\t}\n\t\treturn [...seen];\n\t}\n\n\tprivate sleep(): Promise<void> {\n\t\treturn new Promise<void>((resolve) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\t\tconst wake = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tconst idx = this.wakeResolvers.indexOf(wake);\n\t\t\t\tif (idx !== -1) this.wakeResolvers.splice(idx, 1);\n\t\t\t\tresolve();\n\t\t\t}, this.pollInterval);\n\t\t\tthis.wakeResolvers.push(wake);\n\t\t});\n\t}\n\n\tprivate wake(): void {\n\t\tthis.wakeResolvers.shift()?.();\n\t}\n}\n","// Glob matching for event topic patterns.\n// '*' alone matches everything. Otherwise '*' is a wildcard for any\n// characters including dots, so 'sport.*' matches 'sport.event.created'.\nexport function matchesPattern(pattern: string, eventType: string): boolean {\n\tif (pattern === '*') return true;\n\tconst regex = new RegExp('^' + pattern.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*') + '$');\n\treturn regex.test(eventType);\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nimport type { IStoreAdapter } from '@fonderie/store';\n\n// Tamper-evidence for the append-only event log. Each row carries an HMAC-SHA256\n// over its immutable content, keyed by a server-held secret. An auditor (or a\n// scheduled job) re-derives every HMAC and compares: any modified or forged row\n// fails, because rewriting it without the key can't produce a matching HMAC.\n//\n// Scope: this detects *content* tampering and forged rows. It does not by itself\n// prove no whole row was deleted — that is the job of append-only grants,\n// restricted DB permissions, and backups. Kept deliberately keyed-per-row (not a\n// prev-hash chain) so publishing stays lock-free on the hot event-bus path.\n\n// Deterministic JSON: recursively sort object keys so the same logical value\n// always serialises identically, regardless of insertion order or a JSONB\n// round-trip through Postgres.\nexport function canonicalize(value: unknown): string {\n\treturn JSON.stringify(sortKeys(value));\n}\n\nfunction sortKeys(value: unknown): unknown {\n\tif (Array.isArray(value)) return value.map(sortKeys);\n\tif (value && typeof value === 'object') {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const k of Object.keys(value as Record<string, unknown>).sort()) {\n\t\t\tout[k] = sortKeys((value as Record<string, unknown>)[k]);\n\t\t}\n\t\treturn out;\n\t}\n\treturn value;\n}\n\nexport interface IHashableEvent {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n}\n\n// The HMAC over an event's immutable fields. Field separators (\\n) are safe\n// because they can't appear unescaped inside a JSON string or a UUID/type.\nexport function computeEventHmac(key: string, event: IHashableEvent): string {\n\treturn createHmac('sha256', key)\n\t\t.update(event.id)\n\t\t.update('\\n')\n\t\t.update(event.type)\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.payload))\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.meta))\n\t\t.digest('hex');\n}\n\nfunction hmacEquals(a: string, b: string): boolean {\n\tconst bufA = Buffer.from(a);\n\tconst bufB = Buffer.from(b);\n\treturn bufA.length === bufB.length && timingSafeEqual(bufA, bufB);\n}\n\nexport interface IIntegrityReport {\n\t// True when every HMAC-carrying row verified.\n\tok: boolean;\n\t// Rows that carried an HMAC and were checked.\n\tchecked: number;\n\t// Rows with no HMAC (published before integrity was enabled) — skipped.\n\tunprotected: number;\n\t// Ids of rows whose stored HMAC did not match a fresh computation.\n\ttampered: string[];\n}\n\ninterface IRawEventRow {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n\thmac: string | null;\n}\n\n// Walk the whole event log and re-verify every HMAC-carrying row. Intended for a\n// scheduled integrity job or an on-demand audit endpoint.\nexport async function verifyEventChain(store: IStoreAdapter, key: string): Promise<IIntegrityReport> {\n\tconst rows = await store.query<IRawEventRow>(\n\t\t`SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`,\n\t);\n\n\tconst report: IIntegrityReport = { ok: true, checked: 0, unprotected: 0, tampered: [] };\n\n\tfor (const row of rows) {\n\t\tif (row.hmac === null) {\n\t\t\treport.unprotected += 1;\n\t\t\tcontinue;\n\t\t}\n\t\treport.checked += 1;\n\t\tconst expected = computeEventHmac(key, row);\n\t\tif (!hmacEquals(expected, row.hmac)) {\n\t\t\treport.ok = false;\n\t\t\treport.tampered.push(row.id);\n\t\t}\n\t}\n\n\treturn report;\n}\n","import type { IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport { EventBus } from './bus';\nimport { PGTransport } from './transports/pg';\nimport type { IEventTransport } from './transports/types';\n\nexport type EventTransportConfig =\n\t| {\n\t\t\ttype: 'pg';\n\t\t\tconnectionUrl: string;\n\t\t\tmaxRetries?: number;\n\t\t\tbatchSize?: number;\n\t\t\tpollInterval?: number;\n\t\t\t// Enables tamper-evident audit logging (keyed HMAC per event).\n\t\t\tintegrityKey?: string;\n\t }\n\t| IEventTransport;\n\nexport interface IEventsConfig {\n\ttransport: EventTransportConfig;\n}\n\nfunction resolveTransport(config: EventTransportConfig): IEventTransport {\n\tif ('type' in config && config.type === 'pg') {\n\t\treturn new PGTransport({\n\t\t\tconnectionUrl: config.connectionUrl,\n\t\t\t...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),\n\t\t\t...(config.batchSize !== undefined ? { batchSize: config.batchSize } : {}),\n\t\t\t...(config.pollInterval !== undefined ? { pollInterval: config.pollInterval } : {}),\n\t\t\t...(config.integrityKey !== undefined ? { integrityKey: config.integrityKey } : {}),\n\t\t});\n\t}\n\n\treturn config as IEventTransport;\n}\n\nexport class EventsModule implements IFonderieModule {\n\treadonly name = '@fonderie/events';\n\treadonly bus: EventBus;\n\tprivate readonly config: IEventsConfig;\n\n\tconstructor(config: IEventsConfig) {\n\t\tthis.config = config;\n\t\tthis.bus = new EventBus(resolveTransport(config.transport));\n\t}\n\n\tinstall(_app: IFonderieApp): void {\n\t\tthis.bus.start().catch((err) => console.error('[events] failed to start transport', err));\n\t}\n\n\t// The event log doubles as the audit trail. Without an integrityKey it is\n\t// append-only but not tamper-evident, so a compromised DB write could alter\n\t// history undetectably — a finding worth surfacing (not fatal).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\tconst t = this.config.transport;\n\t\tif ('type' in t && t.type === 'pg' && !t.integrityKey) {\n\t\t\treturn [{\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: 'warning',\n\t\t\t\tmessage: 'no integrityKey — the event/audit log is not tamper-evident; set one to enable per-event HMACs',\n\t\t\t}];\n\t\t}\n\t\treturn [];\n\t}\n}\n","import type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler } from '../types';\nimport { matchesPattern } from './pattern';\n\nexport class MemoryTransport implements IEventTransport {\n\tprivate subscriptions: Array<{ pattern: string; handler: IEventHandler }> = [];\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst matching = this.subscriptions.filter((s) => matchesPattern(s.pattern, type));\n\t\tawait Promise.all(matching.map((s) => s.handler(payload, meta)));\n\t}\n\n\tsubscribe(pattern: string, handler: IEventHandler, _consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler });\n\t}\n\n\tasync start(): Promise<void> {}\n\tasync stop(): Promise<void> {}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport { verifyEventChain, type IIntegrityReport } from './integrity';\n\n// Scheduled tamper-detection for the audit/event log (SOC 2 CC7.2). Runs\n// `verifyEventChain` on an interval; if any HMAC-carrying row fails, it fires\n// `onTamper` — wire that to your alerting. Runs once immediately, then every\n// `intervalMs`. Non-blocking (the timer is unref'd). Call `.stop()` to cancel.\n\nexport interface IIntegrityCheckOptions {\n\t// Default 24h.\n\tintervalMs?: number;\n\t// Called after every run (ok or not) — e.g. to record a heartbeat.\n\tonResult?: (report: IIntegrityReport) => void;\n\t// Called only when the log failed verification. Defaults to a loud\n\t// console.error naming the tampered rows — override to page/alert.\n\tonTamper?: (report: IIntegrityReport) => void;\n}\n\nexport interface IIntegrityCheckHandle {\n\tstop: () => void;\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport function startIntegrityCheck(\n\tstore: IStoreAdapter,\n\tkey: string,\n\toptions: IIntegrityCheckOptions = {},\n): IIntegrityCheckHandle {\n\tconst intervalMs = options.intervalMs ?? DAY_MS;\n\tconst onTamper = options.onTamper ?? defaultTamperHandler;\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst report = await verifyEventChain(store, key);\n\t\t\toptions.onResult?.(report);\n\t\t\tif (!report.ok) onTamper(report);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] integrity check failed to run:', err);\n\t\t}\n\t};\n\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') {\n\t\t(timer as { unref: () => void }).unref();\n\t}\n\tvoid run(); // fire once immediately\n\n\treturn {\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tclearInterval(timer);\n\t\t},\n\t};\n}\n\nfunction defaultTamperHandler(report: IIntegrityReport): void {\n\tconsole.error(\n\t\t`[events] AUDIT LOG INTEGRITY FAILURE — ${report.tampered.length} tampered row(s) ` +\n\t\t\t`out of ${report.checked} checked: ${report.tampered.join(', ')}`,\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Retention for the append-only event/audit log. Events accumulate forever by\n// default; a retention policy disposes of them once they age past the window.\n// Call from a scheduled job. Per-consumer delivery rows are removed by\n// ON DELETE CASCADE. Note: purging is disposal, not tampering — it removes whole\n// aged rows wholesale and does not touch the HMAC of anything it keeps.\n\nexport interface IPurgeEventsOptions {\n\t// Delete events whose created_at is older than this many days.\n\tolderThanDays: number;\n}\n\nexport async function purgeEvents(\n\tstore: IStoreAdapter,\n\t{ olderThanDays }: IPurgeEventsOptions,\n): Promise<number> {\n\tif (!Number.isFinite(olderThanDays) || olderThanDays < 0) {\n\t\tthrow new Error('[events] purgeEvents: olderThanDays must be a non-negative number');\n\t}\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_events\n\t\t WHERE created_at < now() - make_interval(days => $1)\n\t\t RETURNING id`,\n\t\t[olderThanDays],\n\t);\n\treturn rows.length;\n}\n\n// Scheduled disposal (SOC 2 C1/P4). Runs purgeEvents on an interval so aged\n// audit/event rows don't accumulate past the policy window. Runs once\n// immediately, then every intervalMs. Non-blocking (timer unref'd). .stop() cancels.\nexport interface IRetentionScheduleOptions extends IPurgeEventsOptions {\n\tintervalMs?: number; // default 24h\n\tonPurge?: (deleted: number) => void;\n}\n\nexport function startEventRetention(\n\tstore: IStoreAdapter,\n\toptions: IRetentionScheduleOptions,\n): { stop: () => void } {\n\tconst intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1000;\n\tlet stopped = false;\n\tconst run = async () => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst deleted = await purgeEvents(store, { olderThanDays: options.olderThanDays });\n\t\t\toptions.onPurge?.(deleted);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] scheduled retention purge failed:', err);\n\t\t}\n\t};\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') (timer as { unref: () => void }).unref();\n\tvoid run();\n\treturn { stop: () => { stopped = true; clearInterval(timer); } };\n}\n","export { EventBus } from './bus';\nexport { EventsModule } from './module';\nexport type { IEventsConfig, EventTransportConfig } from './module';\n\nexport { MemoryTransport, PGTransport } from './transports';\nexport type { IEventTransport, IPGTransportConfig } from './transports';\n\nexport { matchesPattern } from './transports/pattern';\n\n// Audit-log tamper-evidence\nexport { computeEventHmac, verifyEventChain, canonicalize } from './integrity';\nexport { startIntegrityCheck } from './integrity-job';\nexport type { IIntegrityCheckOptions, IIntegrityCheckHandle } from './integrity-job';\nexport type { IHashableEvent, IIntegrityReport } from './integrity';\n\n// Retention / disposal\nexport { purgeEvents, startEventRetention } from './retention';\nexport type { IPurgeEventsOptions, IRetentionScheduleOptions } from './retention';\n\nexport type { IEventMeta, IEventHandler, IEventRecord, IConsumerRecord } from './types';\n\n// ── Typed event keys ─────────────────────────────────────────────\n// Each domain package re-exports its own EVENT_KEYS.\n// Consumers alias on import:\n// import { EVENT_KEYS as AUTH_EVENT_KEYS } from '@fonderie/auth'\n\nexport const NOTIFICATION_EVENT = 'fonderie.notification.send' as const;\nexport type NotificationEvent = typeof NOTIFICATION_EVENT;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/bus.ts","../src/transports/pg.ts","../src/transports/pattern.ts","../src/integrity.ts","../src/module.ts","../src/transports/memory.ts","../src/integrity-job.ts","../src/retention.ts","../src/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { IEventTransport } from './transports/types';\nimport type { IEventMeta, IEventHandler } from './types';\n\nexport class EventBus {\n\tconstructor(private transport: IEventTransport) {}\n\n\tasync emit<T = unknown>(type: string, payload: T, opts?: { requestId?: string }): Promise<void> {\n\t\tconst meta: IEventMeta = {\n\t\t\tid: randomUUID(),\n\t\t\ttype,\n\t\t\temittedAt: new Date().toISOString(),\n\t\t\tattempts: 0,\n\t\t\t...(opts?.requestId !== undefined ? { requestId: opts.requestId } : {}),\n\t\t};\n\t\tawait this.transport.publish(type, payload, meta);\n\t}\n\n\t// consumer identifies the logical subscriber for per-consumer delivery tracking.\n\t// Defaults to the pattern string — stable and predictable for single-subscriber patterns.\n\ton<T = unknown>(type: string, handler: IEventHandler<T>, consumer: string = type): void {\n\t\tthis.transport.subscribe(type, handler as IEventHandler, consumer);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tawait this.transport.start();\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tawait this.transport.stop();\n\t}\n}\n","import pg from 'pg';\n\nimport { PGAdapter } from '@fonderie/store';\nimport type { IStoreAdapter } from '@fonderie/store';\nimport type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler, IEventRecord } from '../types';\nimport { matchesPattern } from './pattern';\nimport { computeEventHmac } from '../integrity';\n\nexport interface IPGTransportConfig {\n\tconnectionUrl: string;\n\tmaxRetries?: number; // default 3\n\tbatchSize?: number; // default 10 rows claimed per consumer per poll cycle\n\tpollInterval?: number; // default 1000ms fallback poll when no NOTIFY arrives\n\t// When set, every event is stored with a keyed HMAC over its immutable\n\t// content, making the audit log tamper-evident. Unset → no HMAC (unchanged\n\t// behaviour). Verify later with `verifyEventChain(store, integrityKey)`.\n\tintegrityKey?: string;\n}\n\ninterface Subscription {\n\tpattern: string;\n\thandler: IEventHandler;\n\tconsumer: string;\n}\n\nexport class PGTransport implements IEventTransport {\n\tprivate subscriptions: Subscription[] = [];\n\tprivate listenClient: pg.Client | null = null;\n\tprivate store!: IStoreAdapter;\n\tprivate running = false;\n\tprivate wakeResolvers: Array<() => void> = [];\n\n\tprivate readonly maxRetries: number;\n\tprivate readonly batchSize: number;\n\tprivate readonly pollInterval: number;\n\tprivate readonly integrityKey: string | undefined;\n\n\tconstructor(private config: IPGTransportConfig) {\n\t\tthis.maxRetries = config.maxRetries ?? 3;\n\t\tthis.batchSize = config.batchSize ?? 10;\n\t\tthis.pollInterval = config.pollInterval ?? 1_000;\n\t\tthis.integrityKey = config.integrityKey;\n\t}\n\n\t// ── Public API ──────────────────────────────────────────────────\n\n\tsubscribe(pattern: string, handler: IEventHandler, consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler, consumer });\n\t}\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst hmac = this.integrityKey\n\t\t\t? computeEventHmac(this.integrityKey, { id: meta.id, type, payload, meta })\n\t\t\t: null;\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_events (id, type, payload, meta, hmac)\n\t\t\t VALUES ($1, $2, $3, $4, $5)`,\n\t\t\t[meta.id, type, JSON.stringify(payload), JSON.stringify(meta), hmac],\n\t\t);\n\n\t\tconst consumers = this.matchingConsumers(type);\n\t\tif (consumers.length > 0) {\n\t\t\tawait this.store.query(\n\t\t\t\t`INSERT INTO fonderie_event_consumers (event_id, consumer, status, attempts)\n\t\t\t\t SELECT $1, unnest($2::text[]), 'pending', 0\n\t\t\t\t ON CONFLICT (event_id, consumer) DO NOTHING`,\n\t\t\t\t[meta.id, consumers],\n\t\t\t);\n\t\t}\n\n\t\t// NOTIFY carries no payload — it is a wake signal only\n\t\tawait this.store.query(`SELECT pg_notify('fonderie_events', '')`);\n\t}\n\n\tasync start(): Promise<void> {\n\t\tthis.running = true;\n\t\tthis.store = new PGAdapter(this.config.connectionUrl);\n\n\t\t// Reset any rows left in 'processing' by a crashed instance\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_event_consumers SET status = 'failed' WHERE status = 'processing'`,\n\t\t);\n\n\t\tthis.listenClient = new pg.Client(this.config.connectionUrl);\n\t\tawait this.listenClient.connect();\n\t\tawait this.listenClient.query('LISTEN fonderie_events');\n\n\t\tthis.listenClient.on('notification', () => this.wake());\n\t\tthis.listenClient.on('error', (err) =>\n\t\t\tconsole.error('[events:pg] listen client error:', err.message),\n\t\t);\n\n\t\tthis.runPollLoop().catch((err) => console.error('[events:pg] poll loop crashed:', err));\n\t}\n\n\tasync stop(): Promise<void> {\n\t\tthis.running = false;\n\t\tthis.wake();\n\t\tawait this.listenClient?.end();\n\t\tthis.listenClient = null;\n\t}\n\n\t// ── Poll loop ───────────────────────────────────────────────────\n\n\tprivate async runPollLoop(): Promise<void> {\n\t\twhile (this.running) {\n\t\t\ttry {\n\t\t\t\tconst hadWork = await this.pollAllConsumers();\n\t\t\t\tif (!hadWork) await this.sleep();\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error('[events:pg] poll error:', err);\n\t\t\t\tawait this.sleep();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async pollAllConsumers(): Promise<boolean> {\n\t\tconst consumers = [...new Set(this.subscriptions.map((s) => s.consumer))];\n\t\tconst results = await Promise.all(consumers.map((c) => this.pollConsumer(c)));\n\t\treturn results.some((n) => n > 0);\n\t}\n\n\tprivate async pollConsumer(consumer: string): Promise<number> {\n\t\tconst claimed = await this.store.query<{ event_id: string }>(\n\t\t\t`UPDATE fonderie_event_consumers c\n\t\t\t SET status = 'processing', attempts = c.attempts + 1\n\t\t\t FROM (\n\t\t\t SELECT event_id\n\t\t\t FROM fonderie_event_consumers\n\t\t\t WHERE consumer = $1\n\t\t\t AND status IN ('pending', 'failed')\n\t\t\t AND attempts < $2\n\t\t\t ORDER BY event_id\n\t\t\t LIMIT $3\n\t\t\t FOR UPDATE SKIP LOCKED\n\t\t\t ) AS locked\n\t\t\t WHERE c.event_id = locked.event_id\n\t\t\t AND c.consumer = $1\n\t\t\t RETURNING c.event_id`,\n\t\t\t[consumer, this.maxRetries, this.batchSize],\n\t\t);\n\n\t\tawait Promise.all(claimed.map((row) => this.processConsumerEvent(consumer, row.event_id)));\n\t\treturn claimed.length;\n\t}\n\n\t// ── Event processing ────────────────────────────────────────────\n\n\tprivate async processConsumerEvent(consumer: string, eventId: string): Promise<void> {\n\t\tconst [event] = await this.store.query<IEventRecord>(\n\t\t\t`SELECT type, payload, meta FROM fonderie_events WHERE id = $1`,\n\t\t\t[eventId],\n\t\t);\n\t\tif (!event) return;\n\n\t\tconst handlers = this.subscriptions\n\t\t\t.filter((s) => s.consumer === consumer && matchesPattern(s.pattern, event.type))\n\t\t\t.map((s) => s.handler);\n\n\t\ttry {\n\t\t\tawait Promise.all(handlers.map((h) => h(event.payload, event.meta)));\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = 'processed', processed_at = now()\n\t\t\t\t WHERE event_id = $1 AND consumer = $2`,\n\t\t\t\t[eventId, consumer],\n\t\t\t);\n\t\t} catch (err) {\n\t\t\tawait this.store.query(\n\t\t\t\t`UPDATE fonderie_event_consumers\n\t\t\t\t SET status = CASE WHEN attempts >= $1 THEN 'dead' ELSE 'failed' END,\n\t\t\t\t error = $2\n\t\t\t\t WHERE event_id = $3 AND consumer = $4`,\n\t\t\t\t[this.maxRetries, err instanceof Error ? err.message : String(err), eventId, consumer],\n\t\t\t);\n\t\t}\n\t}\n\n\t// ── Helpers ─────────────────────────────────────────────────────\n\n\tprivate matchingConsumers(eventType: string): string[] {\n\t\tconst seen = new Set<string>();\n\t\tfor (const sub of this.subscriptions) {\n\t\t\tif (matchesPattern(sub.pattern, eventType)) seen.add(sub.consumer);\n\t\t}\n\t\treturn [...seen];\n\t}\n\n\tprivate sleep(): Promise<void> {\n\t\treturn new Promise<void>((resolve) => {\n\t\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\t\tconst wake = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tconst idx = this.wakeResolvers.indexOf(wake);\n\t\t\t\tif (idx !== -1) this.wakeResolvers.splice(idx, 1);\n\t\t\t\tresolve();\n\t\t\t}, this.pollInterval);\n\t\t\tthis.wakeResolvers.push(wake);\n\t\t});\n\t}\n\n\tprivate wake(): void {\n\t\tthis.wakeResolvers.shift()?.();\n\t}\n}\n","// Glob matching for event topic patterns.\n// '*' alone matches everything. Otherwise '*' is a wildcard for any\n// characters including dots, so 'sport.*' matches 'sport.event.created'.\nexport function matchesPattern(pattern: string, eventType: string): boolean {\n\tif (pattern === '*') return true;\n\tconst regex = new RegExp('^' + pattern.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*') + '$');\n\treturn regex.test(eventType);\n}\n","import { createHmac } from 'node:crypto';\n\nimport { constantTimeEqual } from '@fonderie/core';\n\nimport type { IStoreAdapter } from '@fonderie/store';\n\n// Tamper-evidence for the append-only event log. Each row carries an HMAC-SHA256\n// over its immutable content, keyed by a server-held secret. An auditor (or a\n// scheduled job) re-derives every HMAC and compares: any modified or forged row\n// fails, because rewriting it without the key can't produce a matching HMAC.\n//\n// Scope: this detects *content* tampering and forged rows. It does not by itself\n// prove no whole row was deleted — that is the job of append-only grants,\n// restricted DB permissions, and backups. Kept deliberately keyed-per-row (not a\n// prev-hash chain) so publishing stays lock-free on the hot event-bus path.\n\n// Deterministic JSON: recursively sort object keys so the same logical value\n// always serialises identically, regardless of insertion order or a JSONB\n// round-trip through Postgres.\nexport function canonicalize(value: unknown): string {\n\treturn JSON.stringify(sortKeys(value));\n}\n\nfunction sortKeys(value: unknown): unknown {\n\tif (Array.isArray(value)) return value.map(sortKeys);\n\tif (value && typeof value === 'object') {\n\t\tconst out: Record<string, unknown> = {};\n\t\tfor (const k of Object.keys(value as Record<string, unknown>).sort()) {\n\t\t\tout[k] = sortKeys((value as Record<string, unknown>)[k]);\n\t\t}\n\t\treturn out;\n\t}\n\treturn value;\n}\n\nexport interface IHashableEvent {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n}\n\n// The HMAC over an event's immutable fields. Field separators (\\n) are safe\n// because they can't appear unescaped inside a JSON string or a UUID/type.\nexport function computeEventHmac(key: string, event: IHashableEvent): string {\n\treturn createHmac('sha256', key)\n\t\t.update(event.id)\n\t\t.update('\\n')\n\t\t.update(event.type)\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.payload))\n\t\t.update('\\n')\n\t\t.update(canonicalize(event.meta))\n\t\t.digest('hex');\n}\n\nexport interface IIntegrityReport {\n\t// True when every HMAC-carrying row verified.\n\tok: boolean;\n\t// Rows that carried an HMAC and were checked.\n\tchecked: number;\n\t// Rows with no HMAC (published before integrity was enabled) — skipped.\n\tunprotected: number;\n\t// Ids of rows whose stored HMAC did not match a fresh computation.\n\ttampered: string[];\n}\n\ninterface IRawEventRow {\n\tid: string;\n\ttype: string;\n\tpayload: unknown;\n\tmeta: unknown;\n\thmac: string | null;\n}\n\n// Walk the whole event log and re-verify every HMAC-carrying row. Intended for a\n// scheduled integrity job or an on-demand audit endpoint.\nexport async function verifyEventChain(store: IStoreAdapter, key: string): Promise<IIntegrityReport> {\n\tconst rows = await store.query<IRawEventRow>(\n\t\t`SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`,\n\t);\n\n\tconst report: IIntegrityReport = { ok: true, checked: 0, unprotected: 0, tampered: [] };\n\n\tfor (const row of rows) {\n\t\tif (row.hmac === null) {\n\t\t\treport.unprotected += 1;\n\t\t\tcontinue;\n\t\t}\n\t\treport.checked += 1;\n\t\tconst expected = computeEventHmac(key, row);\n\t\tif (!constantTimeEqual(expected, row.hmac)) {\n\t\t\treport.ok = false;\n\t\t\treport.tampered.push(row.id);\n\t\t}\n\t}\n\n\treturn report;\n}\n","import type { IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';\n\nimport { EventBus } from './bus';\nimport { PGTransport } from './transports/pg';\nimport type { IEventTransport } from './transports/types';\n\nexport type EventTransportConfig =\n\t| {\n\t\t\ttype: 'pg';\n\t\t\tconnectionUrl: string;\n\t\t\tmaxRetries?: number;\n\t\t\tbatchSize?: number;\n\t\t\tpollInterval?: number;\n\t\t\t// Enables tamper-evident audit logging (keyed HMAC per event).\n\t\t\tintegrityKey?: string;\n\t }\n\t| IEventTransport;\n\nexport interface IEventsConfig {\n\ttransport: EventTransportConfig;\n}\n\nfunction resolveTransport(config: EventTransportConfig): IEventTransport {\n\tif ('type' in config && config.type === 'pg') {\n\t\treturn new PGTransport({\n\t\t\tconnectionUrl: config.connectionUrl,\n\t\t\t...(config.maxRetries !== undefined ? { maxRetries: config.maxRetries } : {}),\n\t\t\t...(config.batchSize !== undefined ? { batchSize: config.batchSize } : {}),\n\t\t\t...(config.pollInterval !== undefined ? { pollInterval: config.pollInterval } : {}),\n\t\t\t...(config.integrityKey !== undefined ? { integrityKey: config.integrityKey } : {}),\n\t\t});\n\t}\n\n\treturn config as IEventTransport;\n}\n\nexport class EventsModule implements IFonderieModule {\n\treadonly name = '@fonderie/events';\n\treadonly bus: EventBus;\n\tprivate readonly config: IEventsConfig;\n\n\tconstructor(config: IEventsConfig) {\n\t\tthis.config = config;\n\t\tthis.bus = new EventBus(resolveTransport(config.transport));\n\t}\n\n\tinstall(_app: IFonderieApp): void {\n\t\tthis.bus.start().catch((err) => console.error('[events] failed to start transport', err));\n\t}\n\n\t// The event log doubles as the audit trail. Without an integrityKey it is\n\t// append-only but not tamper-evident, so a compromised DB write could alter\n\t// history undetectably — a finding worth surfacing (not fatal).\n\tcheckReadiness(): IReadinessProblem[] {\n\t\tconst t = this.config.transport;\n\t\tif ('type' in t && t.type === 'pg' && !t.integrityKey) {\n\t\t\treturn [{\n\t\t\t\tmodule: this.name,\n\t\t\t\tseverity: 'warning',\n\t\t\t\tmessage: 'no integrityKey — the event/audit log is not tamper-evident; set one to enable per-event HMACs',\n\t\t\t}];\n\t\t}\n\t\treturn [];\n\t}\n}\n","import type { IEventTransport } from './types';\nimport type { IEventMeta, IEventHandler } from '../types';\nimport { matchesPattern } from './pattern';\n\nexport class MemoryTransport implements IEventTransport {\n\tprivate subscriptions: Array<{ pattern: string; handler: IEventHandler }> = [];\n\n\tasync publish(type: string, payload: unknown, meta: IEventMeta): Promise<void> {\n\t\tconst matching = this.subscriptions.filter((s) => matchesPattern(s.pattern, type));\n\t\tawait Promise.all(matching.map((s) => s.handler(payload, meta)));\n\t}\n\n\tsubscribe(pattern: string, handler: IEventHandler, _consumer: string): void {\n\t\tthis.subscriptions.push({ pattern, handler });\n\t}\n\n\tasync start(): Promise<void> {}\n\tasync stop(): Promise<void> {}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport { verifyEventChain, type IIntegrityReport } from './integrity';\n\n// Scheduled tamper-detection for the audit/event log (SOC 2 CC7.2). Runs\n// `verifyEventChain` on an interval; if any HMAC-carrying row fails, it fires\n// `onTamper` — wire that to your alerting. Runs once immediately, then every\n// `intervalMs`. Non-blocking (the timer is unref'd). Call `.stop()` to cancel.\n\nexport interface IIntegrityCheckOptions {\n\t// Default 24h.\n\tintervalMs?: number;\n\t// Called after every run (ok or not) — e.g. to record a heartbeat.\n\tonResult?: (report: IIntegrityReport) => void;\n\t// Called only when the log failed verification. Defaults to a loud\n\t// console.error naming the tampered rows — override to page/alert.\n\tonTamper?: (report: IIntegrityReport) => void;\n}\n\nexport interface IIntegrityCheckHandle {\n\tstop: () => void;\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nexport function startIntegrityCheck(\n\tstore: IStoreAdapter,\n\tkey: string,\n\toptions: IIntegrityCheckOptions = {},\n): IIntegrityCheckHandle {\n\tconst intervalMs = options.intervalMs ?? DAY_MS;\n\tconst onTamper = options.onTamper ?? defaultTamperHandler;\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst report = await verifyEventChain(store, key);\n\t\t\toptions.onResult?.(report);\n\t\t\tif (!report.ok) onTamper(report);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] integrity check failed to run:', err);\n\t\t}\n\t};\n\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') {\n\t\t(timer as { unref: () => void }).unref();\n\t}\n\tvoid run(); // fire once immediately\n\n\treturn {\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tclearInterval(timer);\n\t\t},\n\t};\n}\n\nfunction defaultTamperHandler(report: IIntegrityReport): void {\n\tconsole.error(\n\t\t`[events] AUDIT LOG INTEGRITY FAILURE — ${report.tampered.length} tampered row(s) ` +\n\t\t\t`out of ${report.checked} checked: ${report.tampered.join(', ')}`,\n\t);\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\n// Retention for the append-only event/audit log. Events accumulate forever by\n// default; a retention policy disposes of them once they age past the window.\n// Call from a scheduled job. Per-consumer delivery rows are removed by\n// ON DELETE CASCADE. Note: purging is disposal, not tampering — it removes whole\n// aged rows wholesale and does not touch the HMAC of anything it keeps.\n\nexport interface IPurgeEventsOptions {\n\t// Delete events whose created_at is older than this many days.\n\tolderThanDays: number;\n}\n\nexport async function purgeEvents(\n\tstore: IStoreAdapter,\n\t{ olderThanDays }: IPurgeEventsOptions,\n): Promise<number> {\n\tif (!Number.isFinite(olderThanDays) || olderThanDays < 0) {\n\t\tthrow new Error('[events] purgeEvents: olderThanDays must be a non-negative number');\n\t}\n\tconst rows = await store.query<{ id: string }>(\n\t\t`DELETE FROM fonderie_events\n\t\t WHERE created_at < now() - make_interval(days => $1)\n\t\t RETURNING id`,\n\t\t[olderThanDays],\n\t);\n\treturn rows.length;\n}\n\n// Scheduled disposal (SOC 2 C1/P4). Runs purgeEvents on an interval so aged\n// audit/event rows don't accumulate past the policy window. Runs once\n// immediately, then every intervalMs. Non-blocking (timer unref'd). .stop() cancels.\nexport interface IRetentionScheduleOptions extends IPurgeEventsOptions {\n\tintervalMs?: number; // default 24h\n\tonPurge?: (deleted: number) => void;\n}\n\nexport function startEventRetention(\n\tstore: IStoreAdapter,\n\toptions: IRetentionScheduleOptions,\n): { stop: () => void } {\n\tconst intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1000;\n\tlet stopped = false;\n\tconst run = async () => {\n\t\tif (stopped) return;\n\t\ttry {\n\t\t\tconst deleted = await purgeEvents(store, { olderThanDays: options.olderThanDays });\n\t\t\toptions.onPurge?.(deleted);\n\t\t} catch (err) {\n\t\t\tconsole.error('[events] scheduled retention purge failed:', err);\n\t\t}\n\t};\n\tconst timer = setInterval(run, intervalMs);\n\tif (typeof (timer as { unref?: () => void }).unref === 'function') (timer as { unref: () => void }).unref();\n\tvoid run();\n\treturn { stop: () => { stopped = true; clearInterval(timer); } };\n}\n","export { EventBus } from './bus';\nexport { EventsModule } from './module';\nexport type { IEventsConfig, EventTransportConfig } from './module';\n\nexport { MemoryTransport, PGTransport } from './transports';\nexport type { IEventTransport, IPGTransportConfig } from './transports';\n\nexport { matchesPattern } from './transports/pattern';\n\n// Audit-log tamper-evidence\nexport { computeEventHmac, verifyEventChain, canonicalize } from './integrity';\nexport { startIntegrityCheck } from './integrity-job';\nexport type { IIntegrityCheckOptions, IIntegrityCheckHandle } from './integrity-job';\nexport type { IHashableEvent, IIntegrityReport } from './integrity';\n\n// Retention / disposal\nexport { purgeEvents, startEventRetention } from './retention';\nexport type { IPurgeEventsOptions, IRetentionScheduleOptions } from './retention';\n\nexport type { IEventMeta, IEventHandler, IEventRecord, IConsumerRecord } from './types';\n\n// ── Typed event keys ─────────────────────────────────────────────\n// Each domain package re-exports its own EVENT_KEYS.\n// Consumers alias on import:\n// import { EVENT_KEYS as AUTH_EVENT_KEYS } from '@fonderie/auth'\n\nexport const NOTIFICATION_EVENT = 'fonderie.notification.send' as const;\nexport type NotificationEvent = typeof NOTIFICATION_EVENT;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/events",
3
- "version": "5.0.1",
3
+ "version": "5.0.2",
4
4
  "description": "Event bus for @fonderiejs — memory and PostgreSQL transports built-in, adapter interface for Redis/Kafka/RabbitMQ.",
5
5
  "keywords": [
6
6
  "fonderiejs",
@@ -39,7 +39,7 @@
39
39
  "check": "biome check --write src"
40
40
  },
41
41
  "peerDependencies": {
42
- "@fonderie/core": "^0.6.0",
42
+ "@fonderie/core": "^0.7.0",
43
43
  "@fonderie/store": "^0.2.0",
44
44
  "pg": "^8.0.0"
45
45
  },