@fonderie/events 5.0.0 → 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/README.md +2 -2
- package/brain/outcomes.md +1 -0
- package/brain/signatures.md +48 -0
- package/dist/index.cjs +146 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -2
- package/dist/index.d.ts +46 -2
- package/dist/index.js +139 -5
- package/dist/index.js.map +1 -1
- package/dist/migrations/sql/003_events_hmac.sql +4 -0
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ subscribe to exactly the events you care about.
|
|
|
34
34
|
You've shipped this plumbing before — auth, teams, billing, messaging —
|
|
35
35
|
and the next project will ask for it again. Fonderie packages it once:
|
|
36
36
|
plain TypeScript modules for
|
|
37
|
-
[`@fonderie/core`](https://github.com/
|
|
37
|
+
[`@fonderie/core`](https://github.com/fonderiejs/sdk/tree/main/packages/core),
|
|
38
38
|
PostgreSQL-backed, self-hosted, MIT. No external control plane, no
|
|
39
39
|
per-seat anything. Register the modules you need; skip the ones you don't.
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ per-seat anything. Register the modules you need; skip the ones you don't.
|
|
|
42
42
|
producers and consumers stay decoupled — swap transports, keep handlers.
|
|
43
43
|
|
|
44
44
|
Browse the whole set at
|
|
45
|
-
[
|
|
45
|
+
[fonderiejs/sdk](https://github.com/fonderiejs/sdk) · follow
|
|
46
46
|
[@fonderiejs](https://x.com/fonderiejs)
|
|
47
47
|
|
|
48
48
|
## License
|
package/brain/outcomes.md
CHANGED
|
@@ -29,6 +29,7 @@ type TEXT NOT NULL
|
|
|
29
29
|
payload JSONB NOT NULL DEFAULT '{}'
|
|
30
30
|
meta JSONB NOT NULL DEFAULT '{}'
|
|
31
31
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
32
|
+
hmac TEXT
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
Raw SQL ships in `node_modules/@fonderie/events/dist/migrations/sql/` — read it there if you must; never download tarballs.
|
package/brain/signatures.md
CHANGED
|
@@ -17,6 +17,7 @@ new EventsModule(config: IEventsConfig): EventsModule
|
|
|
17
17
|
.name: "@fonderie/events"
|
|
18
18
|
.bus: EventBus
|
|
19
19
|
.install(_app: IFonderieApp): void
|
|
20
|
+
.checkReadiness(): IReadinessProblem[]
|
|
20
21
|
|
|
21
22
|
interface IEventsConfig {
|
|
22
23
|
transport: EventTransportConfig;
|
|
@@ -28,6 +29,7 @@ type EventTransportConfig = {
|
|
|
28
29
|
maxRetries?: number;
|
|
29
30
|
batchSize?: number;
|
|
30
31
|
pollInterval?: number;
|
|
32
|
+
integrityKey?: string;
|
|
31
33
|
} | IEventTransport;
|
|
32
34
|
|
|
33
35
|
new MemoryTransport(): MemoryTransport
|
|
@@ -54,10 +56,56 @@ interface IPGTransportConfig {
|
|
|
54
56
|
maxRetries?: number;
|
|
55
57
|
batchSize?: number;
|
|
56
58
|
pollInterval?: number;
|
|
59
|
+
integrityKey?: string;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
function matchesPattern(pattern: string, eventType: string): boolean
|
|
60
63
|
|
|
64
|
+
function computeEventHmac(key: string, event: IHashableEvent): string
|
|
65
|
+
|
|
66
|
+
function verifyEventChain(store: IStoreAdapter, key: string): Promise<IIntegrityReport>
|
|
67
|
+
|
|
68
|
+
function canonicalize(value: unknown): string
|
|
69
|
+
|
|
70
|
+
function startIntegrityCheck(store: IStoreAdapter, key: string, options?: IIntegrityCheckOptions): IIntegrityCheckHandle
|
|
71
|
+
|
|
72
|
+
interface IIntegrityCheckOptions {
|
|
73
|
+
intervalMs?: number;
|
|
74
|
+
onResult?: (report: IIntegrityReport) => void;
|
|
75
|
+
onTamper?: (report: IIntegrityReport) => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface IIntegrityCheckHandle {
|
|
79
|
+
stop: () => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface IHashableEvent {
|
|
83
|
+
id: string;
|
|
84
|
+
type: string;
|
|
85
|
+
payload: unknown;
|
|
86
|
+
meta: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface IIntegrityReport {
|
|
90
|
+
ok: boolean;
|
|
91
|
+
checked: number;
|
|
92
|
+
unprotected: number;
|
|
93
|
+
tampered: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function purgeEvents(store: IStoreAdapter, { olderThanDays }: IPurgeEventsOptions): Promise<number>
|
|
97
|
+
|
|
98
|
+
function startEventRetention(store: IStoreAdapter, options: IRetentionScheduleOptions): { stop: () => void; }
|
|
99
|
+
|
|
100
|
+
interface IPurgeEventsOptions {
|
|
101
|
+
olderThanDays: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface IRetentionScheduleOptions extends IPurgeEventsOptions {
|
|
105
|
+
intervalMs?: number;
|
|
106
|
+
onPurge?: (deleted: number) => void;
|
|
107
|
+
}
|
|
108
|
+
|
|
61
109
|
interface IEventMeta {
|
|
62
110
|
id: string;
|
|
63
111
|
type: string;
|
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,13 @@ __export(index_exports, {
|
|
|
35
35
|
MemoryTransport: () => MemoryTransport,
|
|
36
36
|
NOTIFICATION_EVENT: () => NOTIFICATION_EVENT,
|
|
37
37
|
PGTransport: () => PGTransport,
|
|
38
|
-
|
|
38
|
+
canonicalize: () => canonicalize,
|
|
39
|
+
computeEventHmac: () => computeEventHmac,
|
|
40
|
+
matchesPattern: () => matchesPattern,
|
|
41
|
+
purgeEvents: () => purgeEvents,
|
|
42
|
+
startEventRetention: () => startEventRetention,
|
|
43
|
+
startIntegrityCheck: () => startIntegrityCheck,
|
|
44
|
+
verifyEventChain: () => verifyEventChain
|
|
39
45
|
});
|
|
40
46
|
module.exports = __toCommonJS(index_exports);
|
|
41
47
|
|
|
@@ -80,6 +86,46 @@ function matchesPattern(pattern, eventType) {
|
|
|
80
86
|
return regex.test(eventType);
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
// src/integrity.ts
|
|
90
|
+
var import_node_crypto2 = require("crypto");
|
|
91
|
+
var import_core = require("@fonderie/core");
|
|
92
|
+
function canonicalize(value) {
|
|
93
|
+
return JSON.stringify(sortKeys(value));
|
|
94
|
+
}
|
|
95
|
+
function sortKeys(value) {
|
|
96
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
97
|
+
if (value && typeof value === "object") {
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const k of Object.keys(value).sort()) {
|
|
100
|
+
out[k] = sortKeys(value[k]);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function computeEventHmac(key, event) {
|
|
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");
|
|
108
|
+
}
|
|
109
|
+
async function verifyEventChain(store, key) {
|
|
110
|
+
const rows = await store.query(
|
|
111
|
+
`SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`
|
|
112
|
+
);
|
|
113
|
+
const report = { ok: true, checked: 0, unprotected: 0, tampered: [] };
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
if (row.hmac === null) {
|
|
116
|
+
report.unprotected += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
report.checked += 1;
|
|
120
|
+
const expected = computeEventHmac(key, row);
|
|
121
|
+
if (!(0, import_core.constantTimeEqual)(expected, row.hmac)) {
|
|
122
|
+
report.ok = false;
|
|
123
|
+
report.tampered.push(row.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return report;
|
|
127
|
+
}
|
|
128
|
+
|
|
83
129
|
// src/transports/pg.ts
|
|
84
130
|
var PGTransport = class {
|
|
85
131
|
constructor(config) {
|
|
@@ -87,6 +133,7 @@ var PGTransport = class {
|
|
|
87
133
|
this.maxRetries = config.maxRetries ?? 3;
|
|
88
134
|
this.batchSize = config.batchSize ?? 10;
|
|
89
135
|
this.pollInterval = config.pollInterval ?? 1e3;
|
|
136
|
+
this.integrityKey = config.integrityKey;
|
|
90
137
|
}
|
|
91
138
|
config;
|
|
92
139
|
subscriptions = [];
|
|
@@ -97,15 +144,17 @@ var PGTransport = class {
|
|
|
97
144
|
maxRetries;
|
|
98
145
|
batchSize;
|
|
99
146
|
pollInterval;
|
|
147
|
+
integrityKey;
|
|
100
148
|
// ── Public API ──────────────────────────────────────────────────
|
|
101
149
|
subscribe(pattern, handler, consumer) {
|
|
102
150
|
this.subscriptions.push({ pattern, handler, consumer });
|
|
103
151
|
}
|
|
104
152
|
async publish(type, payload, meta) {
|
|
153
|
+
const hmac = this.integrityKey ? computeEventHmac(this.integrityKey, { id: meta.id, type, payload, meta }) : null;
|
|
105
154
|
await this.store.query(
|
|
106
|
-
`INSERT INTO fonderie_events (id, type, payload, meta)
|
|
107
|
-
VALUES ($1, $2, $3, $4)`,
|
|
108
|
-
[meta.id, type, JSON.stringify(payload), JSON.stringify(meta)]
|
|
155
|
+
`INSERT INTO fonderie_events (id, type, payload, meta, hmac)
|
|
156
|
+
VALUES ($1, $2, $3, $4, $5)`,
|
|
157
|
+
[meta.id, type, JSON.stringify(payload), JSON.stringify(meta), hmac]
|
|
109
158
|
);
|
|
110
159
|
const consumers = this.matchingConsumers(type);
|
|
111
160
|
if (consumers.length > 0) {
|
|
@@ -240,7 +289,8 @@ function resolveTransport(config) {
|
|
|
240
289
|
connectionUrl: config.connectionUrl,
|
|
241
290
|
...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
|
|
242
291
|
...config.batchSize !== void 0 ? { batchSize: config.batchSize } : {},
|
|
243
|
-
...config.pollInterval !== void 0 ? { pollInterval: config.pollInterval } : {}
|
|
292
|
+
...config.pollInterval !== void 0 ? { pollInterval: config.pollInterval } : {},
|
|
293
|
+
...config.integrityKey !== void 0 ? { integrityKey: config.integrityKey } : {}
|
|
244
294
|
});
|
|
245
295
|
}
|
|
246
296
|
return config;
|
|
@@ -248,12 +298,28 @@ function resolveTransport(config) {
|
|
|
248
298
|
var EventsModule = class {
|
|
249
299
|
name = "@fonderie/events";
|
|
250
300
|
bus;
|
|
301
|
+
config;
|
|
251
302
|
constructor(config) {
|
|
303
|
+
this.config = config;
|
|
252
304
|
this.bus = new EventBus(resolveTransport(config.transport));
|
|
253
305
|
}
|
|
254
306
|
install(_app) {
|
|
255
307
|
this.bus.start().catch((err) => console.error("[events] failed to start transport", err));
|
|
256
308
|
}
|
|
309
|
+
// The event log doubles as the audit trail. Without an integrityKey it is
|
|
310
|
+
// append-only but not tamper-evident, so a compromised DB write could alter
|
|
311
|
+
// history undetectably — a finding worth surfacing (not fatal).
|
|
312
|
+
checkReadiness() {
|
|
313
|
+
const t = this.config.transport;
|
|
314
|
+
if ("type" in t && t.type === "pg" && !t.integrityKey) {
|
|
315
|
+
return [{
|
|
316
|
+
module: this.name,
|
|
317
|
+
severity: "warning",
|
|
318
|
+
message: "no integrityKey \u2014 the event/audit log is not tamper-evident; set one to enable per-event HMACs"
|
|
319
|
+
}];
|
|
320
|
+
}
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
257
323
|
};
|
|
258
324
|
|
|
259
325
|
// src/transports/memory.ts
|
|
@@ -272,6 +338,74 @@ var MemoryTransport = class {
|
|
|
272
338
|
}
|
|
273
339
|
};
|
|
274
340
|
|
|
341
|
+
// src/integrity-job.ts
|
|
342
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
343
|
+
function startIntegrityCheck(store, key, options = {}) {
|
|
344
|
+
const intervalMs = options.intervalMs ?? DAY_MS;
|
|
345
|
+
const onTamper = options.onTamper ?? defaultTamperHandler;
|
|
346
|
+
let stopped = false;
|
|
347
|
+
const run = async () => {
|
|
348
|
+
if (stopped) return;
|
|
349
|
+
try {
|
|
350
|
+
const report = await verifyEventChain(store, key);
|
|
351
|
+
options.onResult?.(report);
|
|
352
|
+
if (!report.ok) onTamper(report);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error("[events] integrity check failed to run:", err);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const timer = setInterval(run, intervalMs);
|
|
358
|
+
if (typeof timer.unref === "function") {
|
|
359
|
+
timer.unref();
|
|
360
|
+
}
|
|
361
|
+
void run();
|
|
362
|
+
return {
|
|
363
|
+
stop: () => {
|
|
364
|
+
stopped = true;
|
|
365
|
+
clearInterval(timer);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function defaultTamperHandler(report) {
|
|
370
|
+
console.error(
|
|
371
|
+
`[events] AUDIT LOG INTEGRITY FAILURE \u2014 ${report.tampered.length} tampered row(s) out of ${report.checked} checked: ${report.tampered.join(", ")}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/retention.ts
|
|
376
|
+
async function purgeEvents(store, { olderThanDays }) {
|
|
377
|
+
if (!Number.isFinite(olderThanDays) || olderThanDays < 0) {
|
|
378
|
+
throw new Error("[events] purgeEvents: olderThanDays must be a non-negative number");
|
|
379
|
+
}
|
|
380
|
+
const rows = await store.query(
|
|
381
|
+
`DELETE FROM fonderie_events
|
|
382
|
+
WHERE created_at < now() - make_interval(days => $1)
|
|
383
|
+
RETURNING id`,
|
|
384
|
+
[olderThanDays]
|
|
385
|
+
);
|
|
386
|
+
return rows.length;
|
|
387
|
+
}
|
|
388
|
+
function startEventRetention(store, options) {
|
|
389
|
+
const intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1e3;
|
|
390
|
+
let stopped = false;
|
|
391
|
+
const run = async () => {
|
|
392
|
+
if (stopped) return;
|
|
393
|
+
try {
|
|
394
|
+
const deleted = await purgeEvents(store, { olderThanDays: options.olderThanDays });
|
|
395
|
+
options.onPurge?.(deleted);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error("[events] scheduled retention purge failed:", err);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const timer = setInterval(run, intervalMs);
|
|
401
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
402
|
+
void run();
|
|
403
|
+
return { stop: () => {
|
|
404
|
+
stopped = true;
|
|
405
|
+
clearInterval(timer);
|
|
406
|
+
} };
|
|
407
|
+
}
|
|
408
|
+
|
|
275
409
|
// src/index.ts
|
|
276
410
|
var NOTIFICATION_EVENT = "fonderie.notification.send";
|
|
277
411
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -281,6 +415,12 @@ var NOTIFICATION_EVENT = "fonderie.notification.send";
|
|
|
281
415
|
MemoryTransport,
|
|
282
416
|
NOTIFICATION_EVENT,
|
|
283
417
|
PGTransport,
|
|
284
|
-
|
|
418
|
+
canonicalize,
|
|
419
|
+
computeEventHmac,
|
|
420
|
+
matchesPattern,
|
|
421
|
+
purgeEvents,
|
|
422
|
+
startEventRetention,
|
|
423
|
+
startIntegrityCheck,
|
|
424
|
+
verifyEventChain
|
|
285
425
|
});
|
|
286
426
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/bus.ts","../src/transports/pg.ts","../src/transports/pattern.ts","../src/module.ts","../src/transports/memory.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\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';\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}\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\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}\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\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_events (id, type, payload, meta)\n\t\t\t VALUES ($1, $2, $3, $4)`,\n\t\t\t[meta.id, type, JSON.stringify(payload), JSON.stringify(meta)],\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 type { IFonderieModule, IFonderieApp } 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 }\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});\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\n\tconstructor(config: IEventsConfig) {\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","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"],"mappings":"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","names":["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.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
1
|
+
import { IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
|
|
2
|
+
import { IStoreAdapter } from '@fonderie/store';
|
|
2
3
|
|
|
3
4
|
interface IEventMeta {
|
|
4
5
|
id: string;
|
|
@@ -48,6 +49,7 @@ type EventTransportConfig = {
|
|
|
48
49
|
maxRetries?: number;
|
|
49
50
|
batchSize?: number;
|
|
50
51
|
pollInterval?: number;
|
|
52
|
+
integrityKey?: string;
|
|
51
53
|
} | IEventTransport;
|
|
52
54
|
interface IEventsConfig {
|
|
53
55
|
transport: EventTransportConfig;
|
|
@@ -55,8 +57,10 @@ interface IEventsConfig {
|
|
|
55
57
|
declare class EventsModule implements IFonderieModule {
|
|
56
58
|
readonly name = "@fonderie/events";
|
|
57
59
|
readonly bus: EventBus;
|
|
60
|
+
private readonly config;
|
|
58
61
|
constructor(config: IEventsConfig);
|
|
59
62
|
install(_app: IFonderieApp): void;
|
|
63
|
+
checkReadiness(): IReadinessProblem[];
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
declare class MemoryTransport implements IEventTransport {
|
|
@@ -72,6 +76,7 @@ interface IPGTransportConfig {
|
|
|
72
76
|
maxRetries?: number;
|
|
73
77
|
batchSize?: number;
|
|
74
78
|
pollInterval?: number;
|
|
79
|
+
integrityKey?: string;
|
|
75
80
|
}
|
|
76
81
|
declare class PGTransport implements IEventTransport {
|
|
77
82
|
private config;
|
|
@@ -83,6 +88,7 @@ declare class PGTransport implements IEventTransport {
|
|
|
83
88
|
private readonly maxRetries;
|
|
84
89
|
private readonly batchSize;
|
|
85
90
|
private readonly pollInterval;
|
|
91
|
+
private readonly integrityKey;
|
|
86
92
|
constructor(config: IPGTransportConfig);
|
|
87
93
|
subscribe(pattern: string, handler: IEventHandler, consumer: string): void;
|
|
88
94
|
publish(type: string, payload: unknown, meta: IEventMeta): Promise<void>;
|
|
@@ -99,7 +105,45 @@ declare class PGTransport implements IEventTransport {
|
|
|
99
105
|
|
|
100
106
|
declare function matchesPattern(pattern: string, eventType: string): boolean;
|
|
101
107
|
|
|
108
|
+
declare function canonicalize(value: unknown): string;
|
|
109
|
+
interface IHashableEvent {
|
|
110
|
+
id: string;
|
|
111
|
+
type: string;
|
|
112
|
+
payload: unknown;
|
|
113
|
+
meta: unknown;
|
|
114
|
+
}
|
|
115
|
+
declare function computeEventHmac(key: string, event: IHashableEvent): string;
|
|
116
|
+
interface IIntegrityReport {
|
|
117
|
+
ok: boolean;
|
|
118
|
+
checked: number;
|
|
119
|
+
unprotected: number;
|
|
120
|
+
tampered: string[];
|
|
121
|
+
}
|
|
122
|
+
declare function verifyEventChain(store: IStoreAdapter, key: string): Promise<IIntegrityReport>;
|
|
123
|
+
|
|
124
|
+
interface IIntegrityCheckOptions {
|
|
125
|
+
intervalMs?: number;
|
|
126
|
+
onResult?: (report: IIntegrityReport) => void;
|
|
127
|
+
onTamper?: (report: IIntegrityReport) => void;
|
|
128
|
+
}
|
|
129
|
+
interface IIntegrityCheckHandle {
|
|
130
|
+
stop: () => void;
|
|
131
|
+
}
|
|
132
|
+
declare function startIntegrityCheck(store: IStoreAdapter, key: string, options?: IIntegrityCheckOptions): IIntegrityCheckHandle;
|
|
133
|
+
|
|
134
|
+
interface IPurgeEventsOptions {
|
|
135
|
+
olderThanDays: number;
|
|
136
|
+
}
|
|
137
|
+
declare function purgeEvents(store: IStoreAdapter, { olderThanDays }: IPurgeEventsOptions): Promise<number>;
|
|
138
|
+
interface IRetentionScheduleOptions extends IPurgeEventsOptions {
|
|
139
|
+
intervalMs?: number;
|
|
140
|
+
onPurge?: (deleted: number) => void;
|
|
141
|
+
}
|
|
142
|
+
declare function startEventRetention(store: IStoreAdapter, options: IRetentionScheduleOptions): {
|
|
143
|
+
stop: () => void;
|
|
144
|
+
};
|
|
145
|
+
|
|
102
146
|
declare const NOTIFICATION_EVENT: "fonderie.notification.send";
|
|
103
147
|
type NotificationEvent = typeof NOTIFICATION_EVENT;
|
|
104
148
|
|
|
105
|
-
export { EventBus, type EventTransportConfig, EventsModule, type IConsumerRecord, type IEventHandler, type IEventMeta, type IEventRecord, type IEventTransport, type IEventsConfig, type IPGTransportConfig, MemoryTransport, NOTIFICATION_EVENT, type NotificationEvent, PGTransport, matchesPattern };
|
|
149
|
+
export { EventBus, type EventTransportConfig, EventsModule, type IConsumerRecord, type IEventHandler, type IEventMeta, type IEventRecord, type IEventTransport, type IEventsConfig, type IHashableEvent, type IIntegrityCheckHandle, type IIntegrityCheckOptions, type IIntegrityReport, type IPGTransportConfig, type IPurgeEventsOptions, type IRetentionScheduleOptions, MemoryTransport, NOTIFICATION_EVENT, type NotificationEvent, PGTransport, canonicalize, computeEventHmac, matchesPattern, purgeEvents, startEventRetention, startIntegrityCheck, verifyEventChain };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
1
|
+
import { IFonderieModule, IFonderieApp, IReadinessProblem } from '@fonderie/core';
|
|
2
|
+
import { IStoreAdapter } from '@fonderie/store';
|
|
2
3
|
|
|
3
4
|
interface IEventMeta {
|
|
4
5
|
id: string;
|
|
@@ -48,6 +49,7 @@ type EventTransportConfig = {
|
|
|
48
49
|
maxRetries?: number;
|
|
49
50
|
batchSize?: number;
|
|
50
51
|
pollInterval?: number;
|
|
52
|
+
integrityKey?: string;
|
|
51
53
|
} | IEventTransport;
|
|
52
54
|
interface IEventsConfig {
|
|
53
55
|
transport: EventTransportConfig;
|
|
@@ -55,8 +57,10 @@ interface IEventsConfig {
|
|
|
55
57
|
declare class EventsModule implements IFonderieModule {
|
|
56
58
|
readonly name = "@fonderie/events";
|
|
57
59
|
readonly bus: EventBus;
|
|
60
|
+
private readonly config;
|
|
58
61
|
constructor(config: IEventsConfig);
|
|
59
62
|
install(_app: IFonderieApp): void;
|
|
63
|
+
checkReadiness(): IReadinessProblem[];
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
declare class MemoryTransport implements IEventTransport {
|
|
@@ -72,6 +76,7 @@ interface IPGTransportConfig {
|
|
|
72
76
|
maxRetries?: number;
|
|
73
77
|
batchSize?: number;
|
|
74
78
|
pollInterval?: number;
|
|
79
|
+
integrityKey?: string;
|
|
75
80
|
}
|
|
76
81
|
declare class PGTransport implements IEventTransport {
|
|
77
82
|
private config;
|
|
@@ -83,6 +88,7 @@ declare class PGTransport implements IEventTransport {
|
|
|
83
88
|
private readonly maxRetries;
|
|
84
89
|
private readonly batchSize;
|
|
85
90
|
private readonly pollInterval;
|
|
91
|
+
private readonly integrityKey;
|
|
86
92
|
constructor(config: IPGTransportConfig);
|
|
87
93
|
subscribe(pattern: string, handler: IEventHandler, consumer: string): void;
|
|
88
94
|
publish(type: string, payload: unknown, meta: IEventMeta): Promise<void>;
|
|
@@ -99,7 +105,45 @@ declare class PGTransport implements IEventTransport {
|
|
|
99
105
|
|
|
100
106
|
declare function matchesPattern(pattern: string, eventType: string): boolean;
|
|
101
107
|
|
|
108
|
+
declare function canonicalize(value: unknown): string;
|
|
109
|
+
interface IHashableEvent {
|
|
110
|
+
id: string;
|
|
111
|
+
type: string;
|
|
112
|
+
payload: unknown;
|
|
113
|
+
meta: unknown;
|
|
114
|
+
}
|
|
115
|
+
declare function computeEventHmac(key: string, event: IHashableEvent): string;
|
|
116
|
+
interface IIntegrityReport {
|
|
117
|
+
ok: boolean;
|
|
118
|
+
checked: number;
|
|
119
|
+
unprotected: number;
|
|
120
|
+
tampered: string[];
|
|
121
|
+
}
|
|
122
|
+
declare function verifyEventChain(store: IStoreAdapter, key: string): Promise<IIntegrityReport>;
|
|
123
|
+
|
|
124
|
+
interface IIntegrityCheckOptions {
|
|
125
|
+
intervalMs?: number;
|
|
126
|
+
onResult?: (report: IIntegrityReport) => void;
|
|
127
|
+
onTamper?: (report: IIntegrityReport) => void;
|
|
128
|
+
}
|
|
129
|
+
interface IIntegrityCheckHandle {
|
|
130
|
+
stop: () => void;
|
|
131
|
+
}
|
|
132
|
+
declare function startIntegrityCheck(store: IStoreAdapter, key: string, options?: IIntegrityCheckOptions): IIntegrityCheckHandle;
|
|
133
|
+
|
|
134
|
+
interface IPurgeEventsOptions {
|
|
135
|
+
olderThanDays: number;
|
|
136
|
+
}
|
|
137
|
+
declare function purgeEvents(store: IStoreAdapter, { olderThanDays }: IPurgeEventsOptions): Promise<number>;
|
|
138
|
+
interface IRetentionScheduleOptions extends IPurgeEventsOptions {
|
|
139
|
+
intervalMs?: number;
|
|
140
|
+
onPurge?: (deleted: number) => void;
|
|
141
|
+
}
|
|
142
|
+
declare function startEventRetention(store: IStoreAdapter, options: IRetentionScheduleOptions): {
|
|
143
|
+
stop: () => void;
|
|
144
|
+
};
|
|
145
|
+
|
|
102
146
|
declare const NOTIFICATION_EVENT: "fonderie.notification.send";
|
|
103
147
|
type NotificationEvent = typeof NOTIFICATION_EVENT;
|
|
104
148
|
|
|
105
|
-
export { EventBus, type EventTransportConfig, EventsModule, type IConsumerRecord, type IEventHandler, type IEventMeta, type IEventRecord, type IEventTransport, type IEventsConfig, type IPGTransportConfig, MemoryTransport, NOTIFICATION_EVENT, type NotificationEvent, PGTransport, matchesPattern };
|
|
149
|
+
export { EventBus, type EventTransportConfig, EventsModule, type IConsumerRecord, type IEventHandler, type IEventMeta, type IEventRecord, type IEventTransport, type IEventsConfig, type IHashableEvent, type IIntegrityCheckHandle, type IIntegrityCheckOptions, type IIntegrityReport, type IPGTransportConfig, type IPurgeEventsOptions, type IRetentionScheduleOptions, MemoryTransport, NOTIFICATION_EVENT, type NotificationEvent, PGTransport, canonicalize, computeEventHmac, matchesPattern, purgeEvents, startEventRetention, startIntegrityCheck, verifyEventChain };
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,46 @@ function matchesPattern(pattern, eventType) {
|
|
|
39
39
|
return regex.test(eventType);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// src/integrity.ts
|
|
43
|
+
import { createHmac } from "crypto";
|
|
44
|
+
import { constantTimeEqual } from "@fonderie/core";
|
|
45
|
+
function canonicalize(value) {
|
|
46
|
+
return JSON.stringify(sortKeys(value));
|
|
47
|
+
}
|
|
48
|
+
function sortKeys(value) {
|
|
49
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
50
|
+
if (value && typeof value === "object") {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const k of Object.keys(value).sort()) {
|
|
53
|
+
out[k] = sortKeys(value[k]);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function computeEventHmac(key, event) {
|
|
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");
|
|
61
|
+
}
|
|
62
|
+
async function verifyEventChain(store, key) {
|
|
63
|
+
const rows = await store.query(
|
|
64
|
+
`SELECT id, type, payload, meta, hmac FROM fonderie_events ORDER BY created_at, id`
|
|
65
|
+
);
|
|
66
|
+
const report = { ok: true, checked: 0, unprotected: 0, tampered: [] };
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
if (row.hmac === null) {
|
|
69
|
+
report.unprotected += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
report.checked += 1;
|
|
73
|
+
const expected = computeEventHmac(key, row);
|
|
74
|
+
if (!constantTimeEqual(expected, row.hmac)) {
|
|
75
|
+
report.ok = false;
|
|
76
|
+
report.tampered.push(row.id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return report;
|
|
80
|
+
}
|
|
81
|
+
|
|
42
82
|
// src/transports/pg.ts
|
|
43
83
|
var PGTransport = class {
|
|
44
84
|
constructor(config) {
|
|
@@ -46,6 +86,7 @@ var PGTransport = class {
|
|
|
46
86
|
this.maxRetries = config.maxRetries ?? 3;
|
|
47
87
|
this.batchSize = config.batchSize ?? 10;
|
|
48
88
|
this.pollInterval = config.pollInterval ?? 1e3;
|
|
89
|
+
this.integrityKey = config.integrityKey;
|
|
49
90
|
}
|
|
50
91
|
config;
|
|
51
92
|
subscriptions = [];
|
|
@@ -56,15 +97,17 @@ var PGTransport = class {
|
|
|
56
97
|
maxRetries;
|
|
57
98
|
batchSize;
|
|
58
99
|
pollInterval;
|
|
100
|
+
integrityKey;
|
|
59
101
|
// ── Public API ──────────────────────────────────────────────────
|
|
60
102
|
subscribe(pattern, handler, consumer) {
|
|
61
103
|
this.subscriptions.push({ pattern, handler, consumer });
|
|
62
104
|
}
|
|
63
105
|
async publish(type, payload, meta) {
|
|
106
|
+
const hmac = this.integrityKey ? computeEventHmac(this.integrityKey, { id: meta.id, type, payload, meta }) : null;
|
|
64
107
|
await this.store.query(
|
|
65
|
-
`INSERT INTO fonderie_events (id, type, payload, meta)
|
|
66
|
-
VALUES ($1, $2, $3, $4)`,
|
|
67
|
-
[meta.id, type, JSON.stringify(payload), JSON.stringify(meta)]
|
|
108
|
+
`INSERT INTO fonderie_events (id, type, payload, meta, hmac)
|
|
109
|
+
VALUES ($1, $2, $3, $4, $5)`,
|
|
110
|
+
[meta.id, type, JSON.stringify(payload), JSON.stringify(meta), hmac]
|
|
68
111
|
);
|
|
69
112
|
const consumers = this.matchingConsumers(type);
|
|
70
113
|
if (consumers.length > 0) {
|
|
@@ -199,7 +242,8 @@ function resolveTransport(config) {
|
|
|
199
242
|
connectionUrl: config.connectionUrl,
|
|
200
243
|
...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
|
|
201
244
|
...config.batchSize !== void 0 ? { batchSize: config.batchSize } : {},
|
|
202
|
-
...config.pollInterval !== void 0 ? { pollInterval: config.pollInterval } : {}
|
|
245
|
+
...config.pollInterval !== void 0 ? { pollInterval: config.pollInterval } : {},
|
|
246
|
+
...config.integrityKey !== void 0 ? { integrityKey: config.integrityKey } : {}
|
|
203
247
|
});
|
|
204
248
|
}
|
|
205
249
|
return config;
|
|
@@ -207,12 +251,28 @@ function resolveTransport(config) {
|
|
|
207
251
|
var EventsModule = class {
|
|
208
252
|
name = "@fonderie/events";
|
|
209
253
|
bus;
|
|
254
|
+
config;
|
|
210
255
|
constructor(config) {
|
|
256
|
+
this.config = config;
|
|
211
257
|
this.bus = new EventBus(resolveTransport(config.transport));
|
|
212
258
|
}
|
|
213
259
|
install(_app) {
|
|
214
260
|
this.bus.start().catch((err) => console.error("[events] failed to start transport", err));
|
|
215
261
|
}
|
|
262
|
+
// The event log doubles as the audit trail. Without an integrityKey it is
|
|
263
|
+
// append-only but not tamper-evident, so a compromised DB write could alter
|
|
264
|
+
// history undetectably — a finding worth surfacing (not fatal).
|
|
265
|
+
checkReadiness() {
|
|
266
|
+
const t = this.config.transport;
|
|
267
|
+
if ("type" in t && t.type === "pg" && !t.integrityKey) {
|
|
268
|
+
return [{
|
|
269
|
+
module: this.name,
|
|
270
|
+
severity: "warning",
|
|
271
|
+
message: "no integrityKey \u2014 the event/audit log is not tamper-evident; set one to enable per-event HMACs"
|
|
272
|
+
}];
|
|
273
|
+
}
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
216
276
|
};
|
|
217
277
|
|
|
218
278
|
// src/transports/memory.ts
|
|
@@ -231,6 +291,74 @@ var MemoryTransport = class {
|
|
|
231
291
|
}
|
|
232
292
|
};
|
|
233
293
|
|
|
294
|
+
// src/integrity-job.ts
|
|
295
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
296
|
+
function startIntegrityCheck(store, key, options = {}) {
|
|
297
|
+
const intervalMs = options.intervalMs ?? DAY_MS;
|
|
298
|
+
const onTamper = options.onTamper ?? defaultTamperHandler;
|
|
299
|
+
let stopped = false;
|
|
300
|
+
const run = async () => {
|
|
301
|
+
if (stopped) return;
|
|
302
|
+
try {
|
|
303
|
+
const report = await verifyEventChain(store, key);
|
|
304
|
+
options.onResult?.(report);
|
|
305
|
+
if (!report.ok) onTamper(report);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.error("[events] integrity check failed to run:", err);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const timer = setInterval(run, intervalMs);
|
|
311
|
+
if (typeof timer.unref === "function") {
|
|
312
|
+
timer.unref();
|
|
313
|
+
}
|
|
314
|
+
void run();
|
|
315
|
+
return {
|
|
316
|
+
stop: () => {
|
|
317
|
+
stopped = true;
|
|
318
|
+
clearInterval(timer);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function defaultTamperHandler(report) {
|
|
323
|
+
console.error(
|
|
324
|
+
`[events] AUDIT LOG INTEGRITY FAILURE \u2014 ${report.tampered.length} tampered row(s) out of ${report.checked} checked: ${report.tampered.join(", ")}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/retention.ts
|
|
329
|
+
async function purgeEvents(store, { olderThanDays }) {
|
|
330
|
+
if (!Number.isFinite(olderThanDays) || olderThanDays < 0) {
|
|
331
|
+
throw new Error("[events] purgeEvents: olderThanDays must be a non-negative number");
|
|
332
|
+
}
|
|
333
|
+
const rows = await store.query(
|
|
334
|
+
`DELETE FROM fonderie_events
|
|
335
|
+
WHERE created_at < now() - make_interval(days => $1)
|
|
336
|
+
RETURNING id`,
|
|
337
|
+
[olderThanDays]
|
|
338
|
+
);
|
|
339
|
+
return rows.length;
|
|
340
|
+
}
|
|
341
|
+
function startEventRetention(store, options) {
|
|
342
|
+
const intervalMs = options.intervalMs ?? 24 * 60 * 60 * 1e3;
|
|
343
|
+
let stopped = false;
|
|
344
|
+
const run = async () => {
|
|
345
|
+
if (stopped) return;
|
|
346
|
+
try {
|
|
347
|
+
const deleted = await purgeEvents(store, { olderThanDays: options.olderThanDays });
|
|
348
|
+
options.onPurge?.(deleted);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error("[events] scheduled retention purge failed:", err);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const timer = setInterval(run, intervalMs);
|
|
354
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
355
|
+
void run();
|
|
356
|
+
return { stop: () => {
|
|
357
|
+
stopped = true;
|
|
358
|
+
clearInterval(timer);
|
|
359
|
+
} };
|
|
360
|
+
}
|
|
361
|
+
|
|
234
362
|
// src/index.ts
|
|
235
363
|
var NOTIFICATION_EVENT = "fonderie.notification.send";
|
|
236
364
|
export {
|
|
@@ -239,6 +367,12 @@ export {
|
|
|
239
367
|
MemoryTransport,
|
|
240
368
|
NOTIFICATION_EVENT,
|
|
241
369
|
PGTransport,
|
|
242
|
-
|
|
370
|
+
canonicalize,
|
|
371
|
+
computeEventHmac,
|
|
372
|
+
matchesPattern,
|
|
373
|
+
purgeEvents,
|
|
374
|
+
startEventRetention,
|
|
375
|
+
startIntegrityCheck,
|
|
376
|
+
verifyEventChain
|
|
243
377
|
};
|
|
244
378
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bus.ts","../src/transports/pg.ts","../src/transports/pattern.ts","../src/module.ts","../src/transports/memory.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';\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}\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\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}\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\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_events (id, type, payload, meta)\n\t\t\t VALUES ($1, $2, $3, $4)`,\n\t\t\t[meta.id, type, JSON.stringify(payload), JSON.stringify(meta)],\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 type { IFonderieModule, IFonderieApp } 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 }\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});\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\n\tconstructor(config: IEventsConfig) {\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","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","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\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,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/events",
|
|
3
|
-
"version": "5.0.
|
|
4
|
-
"description": "Event bus for @
|
|
3
|
+
"version": "5.0.2",
|
|
4
|
+
"description": "Event bus for @fonderiejs — memory and PostgreSQL transports built-in, adapter interface for Redis/Kafka/RabbitMQ.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
6
|
+
"fonderiejs",
|
|
7
7
|
"events",
|
|
8
8
|
"event-bus",
|
|
9
9
|
"pubsub",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"check": "biome check --write src"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@fonderie/core": "^0.
|
|
42
|
+
"@fonderie/core": "^0.7.0",
|
|
43
43
|
"@fonderie/store": "^0.2.0",
|
|
44
44
|
"pg": "^8.0.0"
|
|
45
45
|
},
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@fonderie/core": "../core",
|
|
56
56
|
"@fonderie/store": "../store",
|
|
57
|
-
"@types/node": "^
|
|
58
|
-
"@types/pg": "^8.
|
|
59
|
-
"pg": "^8.
|
|
57
|
+
"@types/node": "^26.4.0",
|
|
58
|
+
"@types/pg": "^8.21.0",
|
|
59
|
+
"pg": "^8.23.0",
|
|
60
60
|
"tsup": "^8.5.1",
|
|
61
|
-
"tsx": "^4.
|
|
61
|
+
"tsx": "^4.23.12",
|
|
62
62
|
"typescript": "^6.0.3"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
@@ -72,11 +72,11 @@
|
|
|
72
72
|
],
|
|
73
73
|
"repository": {
|
|
74
74
|
"type": "git",
|
|
75
|
-
"url": "git+https://github.com/fonderiejs/
|
|
75
|
+
"url": "git+https://github.com/fonderiejs/fonderie.git",
|
|
76
76
|
"directory": "packages/events"
|
|
77
77
|
},
|
|
78
|
-
"homepage": "https://github.com/fonderiejs/
|
|
78
|
+
"homepage": "https://github.com/fonderiejs/fonderie/tree/main/packages/events#readme",
|
|
79
79
|
"bugs": {
|
|
80
|
-
"url": "https://github.com/fonderiejs/
|
|
80
|
+
"url": "https://github.com/fonderiejs/fonderie/issues"
|
|
81
81
|
}
|
|
82
82
|
}
|