hierarchical-approval 2.2.0 → 2.4.0

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +25 -0
  3. package/dist/{ApprovalEngine-CbFBAMlO.d.cts → ApprovalEngine-C1dUPnLM.d.cts} +45 -2
  4. package/dist/{ApprovalEngine-B3ChZxS7.d.ts → ApprovalEngine-Ci9urTDI.d.ts} +45 -2
  5. package/dist/{IStorageAdapter-DjRvHUF0.d.ts → IStorageAdapter-BU3sau5W.d.ts} +14 -0
  6. package/dist/{IStorageAdapter-Bk7ybd3z.d.cts → IStorageAdapter-DdHO4Rf1.d.cts} +14 -0
  7. package/dist/adapters/MemoryAdapter.cjs +4 -0
  8. package/dist/adapters/MemoryAdapter.cjs.map +1 -1
  9. package/dist/adapters/MemoryAdapter.d.cts +2 -1
  10. package/dist/adapters/MemoryAdapter.d.ts +2 -1
  11. package/dist/adapters/MemoryAdapter.js +4 -0
  12. package/dist/adapters/MemoryAdapter.js.map +1 -1
  13. package/dist/adapters/PostgresAdapter.cjs +12 -0
  14. package/dist/adapters/PostgresAdapter.cjs.map +1 -1
  15. package/dist/adapters/PostgresAdapter.d.cts +2 -1
  16. package/dist/adapters/PostgresAdapter.d.ts +2 -1
  17. package/dist/adapters/PostgresAdapter.js +12 -0
  18. package/dist/adapters/PostgresAdapter.js.map +1 -1
  19. package/dist/index.cjs +81 -0
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +3 -3
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.js +81 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/nestjs.cjs +77 -0
  26. package/dist/nestjs.cjs.map +1 -1
  27. package/dist/nestjs.d.cts +2 -2
  28. package/dist/nestjs.d.ts +2 -2
  29. package/dist/nestjs.js +77 -0
  30. package/dist/nestjs.js.map +1 -1
  31. package/dist/plugins/notify.cjs +105 -9
  32. package/dist/plugins/notify.cjs.map +1 -1
  33. package/dist/plugins/notify.d.cts +89 -1
  34. package/dist/plugins/notify.d.ts +89 -1
  35. package/dist/plugins/notify.js +105 -10
  36. package/dist/plugins/notify.js.map +1 -1
  37. package/dist/testing.cjs +81 -0
  38. package/dist/testing.cjs.map +1 -1
  39. package/dist/testing.d.cts +2 -2
  40. package/dist/testing.d.ts +2 -2
  41. package/dist/testing.js +81 -0
  42. package/dist/testing.js.map +1 -1
  43. package/package.json +1 -1
@@ -154,12 +154,16 @@ var OutboxNotificationAdapter = class {
154
154
  record.lastError = message;
155
155
  if (record.attempts >= this.maxAttempts) {
156
156
  record.status = "dead";
157
- this.logger.error("OutboxNotificationAdapter: dead-lettered after exhausting retries", err, {
158
- id: record.id,
159
- tenantId: record.tenantId,
160
- attempts: record.attempts,
161
- maxAttempts: this.maxAttempts
162
- });
157
+ this.logger.error(
158
+ "OutboxNotificationAdapter: dead-lettered after exhausting retries",
159
+ err,
160
+ {
161
+ id: record.id,
162
+ tenantId: record.tenantId,
163
+ attempts: record.attempts,
164
+ maxAttempts: this.maxAttempts
165
+ }
166
+ );
163
167
  } else {
164
168
  record.nextAttemptAt = this.clock.now().getTime() + this.computeBackoff(record.attempts);
165
169
  this.logger.warn("OutboxNotificationAdapter: delivery failed, scheduling retry", {
@@ -239,9 +243,7 @@ var CompositeNotificationAdapter = class {
239
243
  const results = await Promise.allSettled(
240
244
  // Wrap each call so a synchronous throw inside a child's notify is also
241
245
  // captured as a rejection rather than escaping the fan-out.
242
- this.children.map(
243
- (child) => Promise.resolve().then(() => child.adapter.notify(event))
244
- )
246
+ this.children.map((child) => Promise.resolve().then(() => child.adapter.notify(event)))
245
247
  );
246
248
  results.forEach((result, i) => {
247
249
  if (result.status === "rejected") {
@@ -365,6 +367,99 @@ var TemplatedNotificationAdapter = class {
365
367
  }
366
368
  };
367
369
 
368
- export { CompositeNotificationAdapter, InMemoryOutboxStore, OutboxNotificationAdapter, TemplatedNotificationAdapter };
370
+ // src/plugins/notify/DigestNotificationAdapter.ts
371
+ var DEFAULT_PASSTHROUGH = [
372
+ "approval:rejected",
373
+ "approval:completed",
374
+ "approval:sla_breached",
375
+ "approval:expired"
376
+ ];
377
+ var DEFAULT_MAX_BATCH_SIZE = 50;
378
+ var DigestNotificationAdapter = class {
379
+ constructor(opts) {
380
+ this.buffers = /* @__PURE__ */ new Map();
381
+ this.timer = null;
382
+ this.send = opts.send;
383
+ this.passthrough = new Set(opts.passthrough ?? DEFAULT_PASSTHROUGH);
384
+ this.maxBatchSize = opts.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
385
+ this.intervalMs = opts.intervalMs;
386
+ this.logger = opts.logger ?? noopLogger;
387
+ this.clock = opts.clock ?? systemClock;
388
+ if (this.intervalMs !== void 0) {
389
+ if (this.intervalMs <= 0) {
390
+ throw new Error("DigestNotificationAdapter: intervalMs must be a positive number.");
391
+ }
392
+ this.timer = setInterval(() => {
393
+ void this.flush().catch((err) => {
394
+ this.logger.error("DigestNotificationAdapter: scheduled flush failed", err);
395
+ });
396
+ }, this.intervalMs);
397
+ this.timer.unref?.();
398
+ }
399
+ }
400
+ /** Recipients currently holding buffered events. */
401
+ get pendingRecipients() {
402
+ return this.buffers.size;
403
+ }
404
+ async notify(event) {
405
+ if (this.passthrough.has(event.type)) {
406
+ await this.deliver({
407
+ recipient: "",
408
+ events: [event],
409
+ since: event.timestamp,
410
+ flushedAt: this.clock.now()
411
+ });
412
+ return;
413
+ }
414
+ const now = this.clock.now();
415
+ const full = [];
416
+ for (const recipient of event.recipients) {
417
+ const buffer = this.buffers.get(recipient) ?? { events: [], since: now };
418
+ buffer.events.push(event);
419
+ this.buffers.set(recipient, buffer);
420
+ if (buffer.events.length >= this.maxBatchSize) full.push(recipient);
421
+ }
422
+ for (const recipient of full) {
423
+ await this.flushRecipient(recipient);
424
+ }
425
+ }
426
+ /** Deliver every buffered digest now. Safe to call from a cron job or on shutdown. */
427
+ async flush() {
428
+ for (const recipient of [...this.buffers.keys()]) {
429
+ await this.flushRecipient(recipient);
430
+ }
431
+ }
432
+ /** Stop the timer and deliver whatever is buffered. */
433
+ async stop() {
434
+ if (this.timer !== null) {
435
+ clearInterval(this.timer);
436
+ this.timer = null;
437
+ }
438
+ await this.flush();
439
+ }
440
+ async flushRecipient(recipient) {
441
+ const buffer = this.buffers.get(recipient);
442
+ if (!buffer || buffer.events.length === 0) return;
443
+ this.buffers.delete(recipient);
444
+ await this.deliver({
445
+ recipient,
446
+ events: buffer.events,
447
+ since: buffer.since,
448
+ flushedAt: this.clock.now()
449
+ });
450
+ }
451
+ async deliver(digest) {
452
+ try {
453
+ await this.send(digest);
454
+ } catch (err) {
455
+ this.logger.error("DigestNotificationAdapter: send failed", err, {
456
+ recipient: digest.recipient,
457
+ events: digest.events.length
458
+ });
459
+ }
460
+ }
461
+ };
462
+
463
+ export { CompositeNotificationAdapter, DigestNotificationAdapter, InMemoryOutboxStore, OutboxNotificationAdapter, TemplatedNotificationAdapter };
369
464
  //# sourceMappingURL=notify.js.map
370
465
  //# sourceMappingURL=notify.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/Clock.ts","../../src/utils/Logger.ts","../../src/plugins/notify/InMemoryOutboxStore.ts","../../src/plugins/notify/OutboxNotificationAdapter.ts","../../src/plugins/notify/CompositeNotificationAdapter.ts","../../src/plugins/notify/TemplatedNotificationAdapter.ts"],"names":[],"mappings":"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","file":"notify.js","sourcesContent":["export interface Clock {\n now(): Date;\n}\n\nexport const systemClock: Clock = { now: () => new Date() };\n","export interface Logger {\n info(msg: string, context?: Record<string, unknown>): void;\n warn(msg: string, context?: Record<string, unknown>): void;\n error(msg: string, err?: unknown, context?: Record<string, unknown>): void;\n fatal(msg: string, err?: unknown, context?: Record<string, unknown>): void;\n debug(msg: string, context?: Record<string, unknown>): void;\n}\n\nexport const noopLogger: Logger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n fatal: () => {},\n debug: () => {},\n};\n","import type { IOutboxStore, OutboxRecord } from './IOutboxStore.js';\n\n/**\n * Default, dependency-free {@link IOutboxStore} backed by an in-process Map.\n *\n * Ordering: {@link due} and {@link pending} return records sorted by\n * `enqueuedAt` (then by id as a tiebreaker), giving FIFO best-effort within a\n * single `(tenant, instance)` partition. There is no cross-partition ordering\n * guarantee.\n *\n * Records are stored by reference; the adapter mutates and writes them back via\n * {@link update}, so reads reflect the latest state. This is intentional for the\n * in-memory case — a remote store would serialize instead.\n */\nexport class InMemoryOutboxStore implements IOutboxStore {\n private readonly records = new Map<string, OutboxRecord>();\n\n async enqueue(record: OutboxRecord): Promise<void> {\n this.records.set(record.id, record);\n }\n\n async due(now: number): Promise<OutboxRecord[]> {\n return this.sorted().filter((r) => r.status === 'pending' && r.nextAttemptAt <= now);\n }\n\n async update(record: OutboxRecord): Promise<void> {\n // Only persist if the record is still tracked (not removed concurrently).\n if (this.records.has(record.id)) {\n this.records.set(record.id, record);\n }\n }\n\n async remove(id: string): Promise<void> {\n this.records.delete(id);\n }\n\n async pending(): Promise<OutboxRecord[]> {\n return this.sorted().filter((r) => r.status === 'pending');\n }\n\n async deadLettered(): Promise<OutboxRecord[]> {\n return this.sorted().filter((r) => r.status === 'dead');\n }\n\n /** Test/ops helper — total records currently retained (pending + dead). */\n get size(): number {\n return this.records.size;\n }\n\n private sorted(): OutboxRecord[] {\n return [...this.records.values()].sort((a, b) =>\n a.enqueuedAt !== b.enqueuedAt ? a.enqueuedAt - b.enqueuedAt : a.id.localeCompare(b.id),\n );\n }\n}\n","import type { Clock } from '../../utils/Clock.js';\nimport { systemClock } from '../../utils/Clock.js';\nimport type { Logger } from '../../utils/Logger.js';\nimport { noopLogger } from '../../utils/Logger.js';\nimport type { INotificationAdapter, NotificationEvent } from '../../adapters/INotificationAdapter.js';\nimport type { IOutboxStore, OutboxRecord } from './IOutboxStore.js';\nimport { InMemoryOutboxStore } from './InMemoryOutboxStore.js';\n\n/**\n * Transport that performs the actual side-effecting delivery of a single event\n * (send an email, post to a queue, call a webhook, …).\n *\n * It MAY throw synchronously or reject asynchronously — both are treated\n * identically as a failed attempt and trigger a retry. A normal resolution\n * counts as a successful delivery.\n */\nexport type NotificationTransport = (event: NotificationEvent) => void | Promise<void>;\n\n/** Configuration for {@link OutboxNotificationAdapter}. All fields optional except `transport`. */\nexport interface OutboxNotificationAdapterOptions {\n /** Side-effecting delivery function. Required. */\n transport: NotificationTransport;\n /** Persistence for queued events. Defaults to an {@link InMemoryOutboxStore}. */\n store?: IOutboxStore;\n /** Time source. Defaults to {@link systemClock}. Inject a manual clock for deterministic tests. */\n clock?: Clock;\n /** Structured logger. Defaults to {@link noopLogger}. */\n logger?: Logger;\n /**\n * Maximum delivery attempts before an event is dead-lettered. Must be >= 1.\n * `1` means no retries (single failure → dead-letter). Defaults to `5`.\n */\n maxAttempts?: number;\n /** Base backoff in milliseconds for the first retry. Defaults to `1000`. */\n baseDelayMs?: number;\n /** Multiplier applied per attempt (exponential). Defaults to `2`. */\n backoffFactor?: number;\n /**\n * Upper bound on a single backoff delay, in milliseconds. Caps the schedule so\n * very high attempt counts never overflow to `Infinity`/negative. Defaults to\n * `5 * 60_000` (5 minutes).\n */\n maxDelayMs?: number;\n /** Monotonic id generator for records. Defaults to a counter + timestamp. */\n idGenerator?: () => string;\n}\n\n/**\n * Reliable, store-and-forward {@link INotificationAdapter}.\n *\n * `notify()` only enqueues the event into a pluggable outbox store and returns;\n * it never throws (enqueue failures are caught, logged, and swallowed). Actual\n * delivery happens in {@link drain}, which is driven by ops (a poller/cron) or\n * tests. Delivery retries on failure with deterministic exponential backoff\n * computed from the injected {@link Clock}; on exhausting `maxAttempts` the\n * record is moved to a dead-letter list rather than dropped.\n *\n * Ordering: within a single `(tenantId, instanceId)` partition delivery is FIFO\n * best-effort (oldest-enqueued due record first). There is no ordering guarantee\n * across partitions, and a record awaiting a future retry does not block later\n * records in the same partition from being attempted.\n *\n * Drop-in for `ApprovalEngineOptions.notificationAdapter` with no engine change.\n */\nexport class OutboxNotificationAdapter implements INotificationAdapter {\n private readonly transport: NotificationTransport;\n private readonly store: IOutboxStore;\n private readonly clock: Clock;\n private readonly logger: Logger;\n private readonly maxAttempts: number;\n private readonly baseDelayMs: number;\n private readonly backoffFactor: number;\n private readonly maxDelayMs: number;\n private readonly idGenerator: () => string;\n private seq = 0;\n\n /** Guards against concurrent {@link drain} runs causing double-delivery. */\n private draining: Promise<number> | null = null;\n\n constructor(options: OutboxNotificationAdapterOptions) {\n this.transport = options.transport;\n this.store = options.store ?? new InMemoryOutboxStore();\n this.clock = options.clock ?? systemClock;\n this.logger = options.logger ?? noopLogger;\n this.maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? 5));\n this.baseDelayMs = Math.max(0, options.baseDelayMs ?? 1000);\n this.backoffFactor = options.backoffFactor ?? 2;\n this.maxDelayMs = Math.max(0, options.maxDelayMs ?? 5 * 60_000);\n this.idGenerator = options.idGenerator ?? (() => `${this.clock.now().getTime()}-${this.seq++}`);\n }\n\n /**\n * Enqueue an event for reliable delivery. Never throws: a failure to persist\n * is logged and swallowed so the engine's emit path is never broken.\n */\n async notify(event: NotificationEvent): Promise<void> {\n try {\n const now = this.clock.now().getTime();\n const record: OutboxRecord = {\n id: this.idGenerator(),\n partitionKey: `${event.tenantId}:${event.instanceId}`,\n tenantId: event.tenantId,\n event,\n status: 'pending',\n attempts: 0,\n nextAttemptAt: now,\n enqueuedAt: now,\n };\n await this.store.enqueue(record);\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to enqueue event', err, {\n type: event.type,\n instanceId: event.instanceId,\n tenantId: event.tenantId,\n });\n }\n }\n\n /**\n * Attempt delivery of all currently due-and-pending records.\n *\n * Idempotent and safe to call repeatedly and concurrently: if a drain is\n * already in flight, the same promise is returned rather than starting a\n * second pass, so a delivered event is never delivered twice beyond\n * at-least-once semantics. Records whose `nextAttemptAt` is in the future are\n * not attempted prematurely. Never throws — store/transport errors are caught\n * and logged.\n *\n * @returns the number of records successfully delivered in this pass.\n */\n async drain(): Promise<number> {\n if (this.draining) return this.draining;\n this.draining = this.runDrain();\n try {\n return await this.draining;\n } finally {\n this.draining = null;\n }\n }\n\n private async runDrain(): Promise<number> {\n let delivered = 0;\n let due: OutboxRecord[];\n try {\n due = await this.store.due(this.clock.now().getTime());\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to read due records', err);\n return 0;\n }\n\n for (const record of due) {\n // Re-check status defensively in case the store handed back a stale row.\n if (record.status !== 'pending') continue;\n const ok = await this.attemptDelivery(record);\n if (ok) delivered++;\n }\n return delivered;\n }\n\n /** Run one delivery attempt for a record and persist the resulting state. */\n private async attemptDelivery(record: OutboxRecord): Promise<boolean> {\n record.attempts++;\n try {\n // Await covers both async rejection and a returned promise; the try also\n // catches a synchronous throw from the transport.\n await this.transport(record.event);\n try {\n await this.store.remove(record.id);\n } catch (err) {\n // Delivery succeeded but cleanup failed: log. At-least-once means a\n // future drain may redeliver — acceptable and documented.\n this.logger.error('OutboxNotificationAdapter: delivered but failed to remove record', err, {\n id: record.id,\n tenantId: record.tenantId,\n });\n }\n this.logger.debug('OutboxNotificationAdapter: delivered', {\n id: record.id,\n type: record.event.type,\n attempts: record.attempts,\n });\n return true;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n record.lastError = message;\n if (record.attempts >= this.maxAttempts) {\n record.status = 'dead';\n this.logger.error('OutboxNotificationAdapter: dead-lettered after exhausting retries', err, {\n id: record.id,\n tenantId: record.tenantId,\n attempts: record.attempts,\n maxAttempts: this.maxAttempts,\n });\n } else {\n record.nextAttemptAt = this.clock.now().getTime() + this.computeBackoff(record.attempts);\n this.logger.warn('OutboxNotificationAdapter: delivery failed, scheduling retry', {\n id: record.id,\n tenantId: record.tenantId,\n attempts: record.attempts,\n nextAttemptAt: record.nextAttemptAt,\n error: message,\n });\n }\n try {\n await this.store.update(record);\n } catch (updateErr) {\n this.logger.error('OutboxNotificationAdapter: failed to persist record state', updateErr, {\n id: record.id,\n });\n }\n return false;\n }\n }\n\n /**\n * Deterministic exponential backoff for the Nth attempt (1-based), capped at\n * `maxDelayMs`. Guards against overflow: a non-finite intermediate value\n * collapses to the cap, so very high attempt counts never yield\n * `Infinity`/`NaN`/negative delays.\n */\n private computeBackoff(attempt: number): number {\n const raw = this.baseDelayMs * Math.pow(this.backoffFactor, attempt - 1);\n if (!Number.isFinite(raw) || raw < 0) return this.maxDelayMs;\n return Math.min(raw, this.maxDelayMs);\n }\n\n /**\n * Records still awaiting first delivery or a retry. Exposed for ops dashboards\n * and tests so a stuck transport (growing pending list) is observable.\n */\n async pending(): Promise<OutboxRecord[]> {\n try {\n return await this.store.pending();\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to read pending records', err);\n return [];\n }\n }\n\n /**\n * Records that exhausted all retries. Exposed so ops can detect and replay a\n * stuck transport; the list is never silently dropped/truncated.\n */\n async deadLettered(): Promise<OutboxRecord[]> {\n try {\n return await this.store.deadLettered();\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to read dead-lettered records', err);\n return [];\n }\n }\n}\n","import type { Logger } from '../../utils/Logger.js';\nimport { noopLogger } from '../../utils/Logger.js';\nimport type { INotificationAdapter, NotificationEvent } from '../../adapters/INotificationAdapter.js';\n\n/** A child adapter paired with a stable name for diagnostics/logging. */\nexport interface NamedNotificationChild {\n /** Human-readable identity used when logging this child's failures. */\n name: string;\n adapter: INotificationAdapter;\n}\n\n/** Either a bare adapter or a {@link NamedNotificationChild}. */\nexport type CompositeChild = INotificationAdapter | NamedNotificationChild;\n\n/** Configuration for {@link CompositeNotificationAdapter}. */\nexport interface CompositeNotificationAdapterOptions {\n /** Child adapters to fan out to. May be empty (resolves as a no-op). */\n children: CompositeChild[];\n /** Structured logger. Defaults to {@link noopLogger}. */\n logger?: Logger;\n}\n\nfunction isNamed(child: CompositeChild): child is NamedNotificationChild {\n // A real bare adapter always exposes its own notify(); never treat it as a\n // NamedNotificationChild even if it happens to also carry an `adapter` property.\n if (typeof (child as INotificationAdapter).notify === 'function') return false;\n // Otherwise it's a NamedNotificationChild only if it wraps a non-null `adapter`\n // object whose own `.notify` is callable.\n const adapter = (child as NamedNotificationChild).adapter as unknown;\n return (\n typeof adapter === 'object' &&\n adapter !== null &&\n typeof (adapter as INotificationAdapter).notify === 'function'\n );\n}\n\n/**\n * Fans a single {@link notify} call out to N child {@link INotificationAdapter}s\n * concurrently.\n *\n * Uses `Promise.allSettled` so one failing/slow child never blocks delivery to\n * the others. Never throws: every child rejection is collected and logged with\n * that child's identity. Zero children resolves immediately as a no-op.\n *\n * Drop-in for `ApprovalEngineOptions.notificationAdapter`.\n */\nexport class CompositeNotificationAdapter implements INotificationAdapter {\n private readonly children: NamedNotificationChild[];\n private readonly logger: Logger;\n\n constructor(options: CompositeNotificationAdapterOptions) {\n this.children = options.children.map((child, i) =>\n isNamed(child) ? child : { name: `child[${i}]`, adapter: child },\n );\n this.logger = options.logger ?? noopLogger;\n }\n\n /**\n * Deliver the event to every child concurrently. Resolves once all children\n * settle; never rejects.\n */\n async notify(event: NotificationEvent): Promise<void> {\n if (this.children.length === 0) return;\n\n const results = await Promise.allSettled(\n // Wrap each call so a synchronous throw inside a child's notify is also\n // captured as a rejection rather than escaping the fan-out.\n this.children.map((child) =>\n Promise.resolve().then(() => child.adapter.notify(event)),\n ),\n );\n\n results.forEach((result, i) => {\n if (result.status === 'rejected') {\n const child = this.children[i]!;\n this.logger.error('CompositeNotificationAdapter: child failed to notify', result.reason, {\n child: child.name,\n type: event.type,\n instanceId: event.instanceId,\n tenantId: event.tenantId,\n });\n }\n });\n }\n}\n","import type { Logger } from '../../utils/Logger.js';\nimport { noopLogger } from '../../utils/Logger.js';\nimport type { ApprovalEventName } from '../../types/events.js';\nimport type { INotificationAdapter, NotificationEvent } from '../../adapters/INotificationAdapter.js';\n\n/** The fully rendered, channel-ready message handed to the send fn. */\nexport interface RenderedNotification {\n /** Logical delivery channel (e.g. 'email', 'slack', 'sms'). */\n channel: string;\n /** Resolved recipient address(es) for the channel. */\n to: string[];\n /** Short headline / email subject. */\n subject: string;\n /** Human-readable body. */\n body: string;\n}\n\n/** The shape a template function returns (channel/to are derived separately). */\nexport interface RenderedMessage {\n subject: string;\n body: string;\n}\n\n/**\n * A template entry for one {@link ApprovalEventName}. Either:\n * - a function `(event) => { subject, body }`, for full programmatic control, or\n * - a `{ subject, body }` pair of strings with `{placeholder}` tokens that are\n * interpolated from the event and its payload.\n */\nexport type NotificationTemplate =\n | ((event: NotificationEvent) => RenderedMessage)\n | { subject: string; body: string };\n\n/** Map of event name → template. Any subset of events may be configured. */\nexport type TemplateMap = Partial<Record<ApprovalEventName, NotificationTemplate>>;\n\n/** Side-effecting send function the adapter forwards rendered messages to. */\nexport type SendFn = (message: RenderedNotification) => void | Promise<void>;\n\n/** Configuration for {@link TemplatedNotificationAdapter}. */\nexport interface TemplatedNotificationAdapterOptions {\n /** Side-effecting send function. Required. */\n send: SendFn;\n /** Per-event templates. Events without an entry use {@link fallbackTemplate} (if any). */\n templates?: TemplateMap;\n /**\n * Template used when no per-event entry exists. If omitted, events without a\n * template are skipped (logged) rather than throwing. Set to a function or a\n * `{subject, body}` string pair to guarantee a message for every event.\n */\n fallbackTemplate?: NotificationTemplate;\n /**\n * Derive the channel for an event. Defaults to the constant `'default'`.\n */\n channelFor?: (event: NotificationEvent) => string;\n /**\n * Derive recipient address(es). Defaults to `event.recipients`. When this\n * returns an empty array the adapter falls back to {@link defaultRecipients}\n * (if set) and otherwise skips the send gracefully.\n */\n recipientsFor?: (event: NotificationEvent) => string[];\n /**\n * Recipients used when {@link recipientsFor} yields none (e.g. cancelled /\n * expired / sla_breached events carry empty `recipients`). If also empty the\n * send is skipped rather than dispatched to nobody.\n */\n defaultRecipients?: string[];\n /**\n * Token substituted for a `{placeholder}` that resolves to `undefined`/`null`\n * or references a field absent on the payload. Defaults to `''` (empty\n * string). Interpolation never throws on unknown placeholders.\n */\n unknownPlaceholderToken?: string;\n /** Structured logger. Defaults to {@link noopLogger}. */\n logger?: Logger;\n}\n\nconst PLACEHOLDER = /\\{([^{}]+)\\}/g;\n\n/**\n * Renders a human-readable message per {@link ApprovalEventName} from a\n * configurable template map and forwards `{ channel, to, subject, body }` to an\n * injected send function.\n *\n * Templates are resolved by event name; a missing template falls back to\n * `fallbackTemplate` or — if none is configured — the event is skipped (logged)\n * rather than throwing. String templates support `{placeholder}` interpolation\n * pulled from top-level event fields and `event.payload` fields; an unknown or\n * absent field renders to `unknownPlaceholderToken` and never throws.\n *\n * `notify()` never throws — send errors and any rendering issues are caught and\n * logged. Drop-in for `ApprovalEngineOptions.notificationAdapter`.\n */\nexport class TemplatedNotificationAdapter implements INotificationAdapter {\n private readonly send: SendFn;\n private readonly templates: TemplateMap;\n private readonly fallbackTemplate?: NotificationTemplate;\n private readonly channelFor: (event: NotificationEvent) => string;\n private readonly recipientsFor: (event: NotificationEvent) => string[];\n private readonly defaultRecipients: string[];\n private readonly unknownPlaceholderToken: string;\n private readonly logger: Logger;\n\n constructor(options: TemplatedNotificationAdapterOptions) {\n this.send = options.send;\n this.templates = options.templates ?? {};\n this.fallbackTemplate = options.fallbackTemplate;\n this.channelFor = options.channelFor ?? (() => 'default');\n this.recipientsFor = options.recipientsFor ?? ((event) => event.recipients ?? []);\n this.defaultRecipients = options.defaultRecipients ?? [];\n this.unknownPlaceholderToken = options.unknownPlaceholderToken ?? '';\n this.logger = options.logger ?? noopLogger;\n }\n\n /**\n * Render and dispatch the event. Never throws: missing templates, empty\n * recipients, and send failures are all handled and logged.\n */\n async notify(event: NotificationEvent): Promise<void> {\n try {\n const template = this.templates[event.type] ?? this.fallbackTemplate;\n if (!template) {\n this.logger.debug('TemplatedNotificationAdapter: no template for event, skipping', {\n type: event.type,\n instanceId: event.instanceId,\n });\n return;\n }\n\n const to = this.resolveRecipients(event);\n if (to.length === 0) {\n this.logger.debug('TemplatedNotificationAdapter: no recipients for event, skipping', {\n type: event.type,\n instanceId: event.instanceId,\n });\n return;\n }\n\n const rendered = this.render(template, event);\n const message: RenderedNotification = {\n channel: this.channelFor(event),\n to,\n subject: rendered.subject,\n body: rendered.body,\n };\n\n await this.send(message);\n } catch (err) {\n this.logger.error('TemplatedNotificationAdapter: failed to render/send notification', err, {\n type: event.type,\n instanceId: event.instanceId,\n tenantId: event.tenantId,\n });\n }\n }\n\n private resolveRecipients(event: NotificationEvent): string[] {\n const derived = this.recipientsFor(event);\n if (derived.length > 0) return derived;\n return this.defaultRecipients;\n }\n\n private render(template: NotificationTemplate, event: NotificationEvent): RenderedMessage {\n if (typeof template === 'function') {\n return template(event);\n }\n return {\n subject: this.interpolate(template.subject, event),\n body: this.interpolate(template.body, event),\n };\n }\n\n /**\n * Replace `{token}` occurrences in `text`. A token is resolved against\n * top-level event fields first, then `event.payload`. Dotted paths\n * (`payload.level`, `a.b.c`) are supported. Anything unresolved renders to\n * `unknownPlaceholderToken`. Never throws.\n */\n private interpolate(text: string, event: NotificationEvent): string {\n return text.replace(PLACEHOLDER, (_match, rawKey: string) => {\n const key = rawKey.trim();\n const value = this.lookup(event, key);\n if (value === undefined || value === null) return this.unknownPlaceholderToken;\n return this.stringify(value);\n });\n }\n\n /** Resolve a (possibly dotted) key against the event then its payload. */\n private lookup(event: NotificationEvent, key: string): unknown {\n const fromEvent = this.dig(event as unknown as Record<string, unknown>, key);\n if (fromEvent !== undefined) return fromEvent;\n return this.dig(event.payload as unknown as Record<string, unknown>, key);\n }\n\n private dig(root: Record<string, unknown> | undefined, path: string): unknown {\n if (!root) return undefined;\n let current: unknown = root;\n for (const segment of path.split('.')) {\n if (current === null || typeof current !== 'object') return undefined;\n current = (current as Record<string, unknown>)[segment];\n }\n return current;\n }\n\n private stringify(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n if (Array.isArray(value)) return value.map((v) => this.stringify(v)).join(', ');\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return this.unknownPlaceholderToken;\n }\n }\n return String(value);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/Clock.ts","../../src/utils/Logger.ts","../../src/plugins/notify/InMemoryOutboxStore.ts","../../src/plugins/notify/OutboxNotificationAdapter.ts","../../src/plugins/notify/CompositeNotificationAdapter.ts","../../src/plugins/notify/TemplatedNotificationAdapter.ts","../../src/plugins/notify/DigestNotificationAdapter.ts"],"names":[],"mappings":"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","file":"notify.js","sourcesContent":["export interface Clock {\n now(): Date;\n}\n\nexport const systemClock: Clock = { now: () => new Date() };\n","export interface Logger {\n info(msg: string, context?: Record<string, unknown>): void;\n warn(msg: string, context?: Record<string, unknown>): void;\n error(msg: string, err?: unknown, context?: Record<string, unknown>): void;\n fatal(msg: string, err?: unknown, context?: Record<string, unknown>): void;\n debug(msg: string, context?: Record<string, unknown>): void;\n}\n\nexport const noopLogger: Logger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n fatal: () => {},\n debug: () => {},\n};\n","import type { IOutboxStore, OutboxRecord } from './IOutboxStore.js';\n\n/**\n * Default, dependency-free {@link IOutboxStore} backed by an in-process Map.\n *\n * Ordering: {@link due} and {@link pending} return records sorted by\n * `enqueuedAt` (then by id as a tiebreaker), giving FIFO best-effort within a\n * single `(tenant, instance)` partition. There is no cross-partition ordering\n * guarantee.\n *\n * Records are stored by reference; the adapter mutates and writes them back via\n * {@link update}, so reads reflect the latest state. This is intentional for the\n * in-memory case — a remote store would serialize instead.\n */\nexport class InMemoryOutboxStore implements IOutboxStore {\n private readonly records = new Map<string, OutboxRecord>();\n\n async enqueue(record: OutboxRecord): Promise<void> {\n this.records.set(record.id, record);\n }\n\n async due(now: number): Promise<OutboxRecord[]> {\n return this.sorted().filter((r) => r.status === 'pending' && r.nextAttemptAt <= now);\n }\n\n async update(record: OutboxRecord): Promise<void> {\n // Only persist if the record is still tracked (not removed concurrently).\n if (this.records.has(record.id)) {\n this.records.set(record.id, record);\n }\n }\n\n async remove(id: string): Promise<void> {\n this.records.delete(id);\n }\n\n async pending(): Promise<OutboxRecord[]> {\n return this.sorted().filter((r) => r.status === 'pending');\n }\n\n async deadLettered(): Promise<OutboxRecord[]> {\n return this.sorted().filter((r) => r.status === 'dead');\n }\n\n /** Test/ops helper — total records currently retained (pending + dead). */\n get size(): number {\n return this.records.size;\n }\n\n private sorted(): OutboxRecord[] {\n return [...this.records.values()].sort((a, b) =>\n a.enqueuedAt !== b.enqueuedAt ? a.enqueuedAt - b.enqueuedAt : a.id.localeCompare(b.id),\n );\n }\n}\n","import type { Clock } from '../../utils/Clock.js';\nimport { systemClock } from '../../utils/Clock.js';\nimport type { Logger } from '../../utils/Logger.js';\nimport { noopLogger } from '../../utils/Logger.js';\nimport type {\n INotificationAdapter,\n NotificationEvent,\n} from '../../adapters/INotificationAdapter.js';\nimport type { IOutboxStore, OutboxRecord } from './IOutboxStore.js';\nimport { InMemoryOutboxStore } from './InMemoryOutboxStore.js';\n\n/**\n * Transport that performs the actual side-effecting delivery of a single event\n * (send an email, post to a queue, call a webhook, …).\n *\n * It MAY throw synchronously or reject asynchronously — both are treated\n * identically as a failed attempt and trigger a retry. A normal resolution\n * counts as a successful delivery.\n */\nexport type NotificationTransport = (event: NotificationEvent) => void | Promise<void>;\n\n/** Configuration for {@link OutboxNotificationAdapter}. All fields optional except `transport`. */\nexport interface OutboxNotificationAdapterOptions {\n /** Side-effecting delivery function. Required. */\n transport: NotificationTransport;\n /** Persistence for queued events. Defaults to an {@link InMemoryOutboxStore}. */\n store?: IOutboxStore;\n /** Time source. Defaults to {@link systemClock}. Inject a manual clock for deterministic tests. */\n clock?: Clock;\n /** Structured logger. Defaults to {@link noopLogger}. */\n logger?: Logger;\n /**\n * Maximum delivery attempts before an event is dead-lettered. Must be >= 1.\n * `1` means no retries (single failure → dead-letter). Defaults to `5`.\n */\n maxAttempts?: number;\n /** Base backoff in milliseconds for the first retry. Defaults to `1000`. */\n baseDelayMs?: number;\n /** Multiplier applied per attempt (exponential). Defaults to `2`. */\n backoffFactor?: number;\n /**\n * Upper bound on a single backoff delay, in milliseconds. Caps the schedule so\n * very high attempt counts never overflow to `Infinity`/negative. Defaults to\n * `5 * 60_000` (5 minutes).\n */\n maxDelayMs?: number;\n /** Monotonic id generator for records. Defaults to a counter + timestamp. */\n idGenerator?: () => string;\n}\n\n/**\n * Reliable, store-and-forward {@link INotificationAdapter}.\n *\n * `notify()` only enqueues the event into a pluggable outbox store and returns;\n * it never throws (enqueue failures are caught, logged, and swallowed). Actual\n * delivery happens in {@link drain}, which is driven by ops (a poller/cron) or\n * tests. Delivery retries on failure with deterministic exponential backoff\n * computed from the injected {@link Clock}; on exhausting `maxAttempts` the\n * record is moved to a dead-letter list rather than dropped.\n *\n * Ordering: within a single `(tenantId, instanceId)` partition delivery is FIFO\n * best-effort (oldest-enqueued due record first). There is no ordering guarantee\n * across partitions, and a record awaiting a future retry does not block later\n * records in the same partition from being attempted.\n *\n * Drop-in for `ApprovalEngineOptions.notificationAdapter` with no engine change.\n */\nexport class OutboxNotificationAdapter implements INotificationAdapter {\n private readonly transport: NotificationTransport;\n private readonly store: IOutboxStore;\n private readonly clock: Clock;\n private readonly logger: Logger;\n private readonly maxAttempts: number;\n private readonly baseDelayMs: number;\n private readonly backoffFactor: number;\n private readonly maxDelayMs: number;\n private readonly idGenerator: () => string;\n private seq = 0;\n\n /** Guards against concurrent {@link drain} runs causing double-delivery. */\n private draining: Promise<number> | null = null;\n\n constructor(options: OutboxNotificationAdapterOptions) {\n this.transport = options.transport;\n this.store = options.store ?? new InMemoryOutboxStore();\n this.clock = options.clock ?? systemClock;\n this.logger = options.logger ?? noopLogger;\n this.maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? 5));\n this.baseDelayMs = Math.max(0, options.baseDelayMs ?? 1000);\n this.backoffFactor = options.backoffFactor ?? 2;\n this.maxDelayMs = Math.max(0, options.maxDelayMs ?? 5 * 60_000);\n this.idGenerator = options.idGenerator ?? (() => `${this.clock.now().getTime()}-${this.seq++}`);\n }\n\n /**\n * Enqueue an event for reliable delivery. Never throws: a failure to persist\n * is logged and swallowed so the engine's emit path is never broken.\n */\n async notify(event: NotificationEvent): Promise<void> {\n try {\n const now = this.clock.now().getTime();\n const record: OutboxRecord = {\n id: this.idGenerator(),\n partitionKey: `${event.tenantId}:${event.instanceId}`,\n tenantId: event.tenantId,\n event,\n status: 'pending',\n attempts: 0,\n nextAttemptAt: now,\n enqueuedAt: now,\n };\n await this.store.enqueue(record);\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to enqueue event', err, {\n type: event.type,\n instanceId: event.instanceId,\n tenantId: event.tenantId,\n });\n }\n }\n\n /**\n * Attempt delivery of all currently due-and-pending records.\n *\n * Idempotent and safe to call repeatedly and concurrently: if a drain is\n * already in flight, the same promise is returned rather than starting a\n * second pass, so a delivered event is never delivered twice beyond\n * at-least-once semantics. Records whose `nextAttemptAt` is in the future are\n * not attempted prematurely. Never throws — store/transport errors are caught\n * and logged.\n *\n * @returns the number of records successfully delivered in this pass.\n */\n async drain(): Promise<number> {\n if (this.draining) return this.draining;\n this.draining = this.runDrain();\n try {\n return await this.draining;\n } finally {\n this.draining = null;\n }\n }\n\n private async runDrain(): Promise<number> {\n let delivered = 0;\n let due: OutboxRecord[];\n try {\n due = await this.store.due(this.clock.now().getTime());\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to read due records', err);\n return 0;\n }\n\n for (const record of due) {\n // Re-check status defensively in case the store handed back a stale row.\n if (record.status !== 'pending') continue;\n const ok = await this.attemptDelivery(record);\n if (ok) delivered++;\n }\n return delivered;\n }\n\n /** Run one delivery attempt for a record and persist the resulting state. */\n private async attemptDelivery(record: OutboxRecord): Promise<boolean> {\n record.attempts++;\n try {\n // Await covers both async rejection and a returned promise; the try also\n // catches a synchronous throw from the transport.\n await this.transport(record.event);\n try {\n await this.store.remove(record.id);\n } catch (err) {\n // Delivery succeeded but cleanup failed: log. At-least-once means a\n // future drain may redeliver — acceptable and documented.\n this.logger.error('OutboxNotificationAdapter: delivered but failed to remove record', err, {\n id: record.id,\n tenantId: record.tenantId,\n });\n }\n this.logger.debug('OutboxNotificationAdapter: delivered', {\n id: record.id,\n type: record.event.type,\n attempts: record.attempts,\n });\n return true;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n record.lastError = message;\n if (record.attempts >= this.maxAttempts) {\n record.status = 'dead';\n this.logger.error(\n 'OutboxNotificationAdapter: dead-lettered after exhausting retries',\n err,\n {\n id: record.id,\n tenantId: record.tenantId,\n attempts: record.attempts,\n maxAttempts: this.maxAttempts,\n },\n );\n } else {\n record.nextAttemptAt = this.clock.now().getTime() + this.computeBackoff(record.attempts);\n this.logger.warn('OutboxNotificationAdapter: delivery failed, scheduling retry', {\n id: record.id,\n tenantId: record.tenantId,\n attempts: record.attempts,\n nextAttemptAt: record.nextAttemptAt,\n error: message,\n });\n }\n try {\n await this.store.update(record);\n } catch (updateErr) {\n this.logger.error('OutboxNotificationAdapter: failed to persist record state', updateErr, {\n id: record.id,\n });\n }\n return false;\n }\n }\n\n /**\n * Deterministic exponential backoff for the Nth attempt (1-based), capped at\n * `maxDelayMs`. Guards against overflow: a non-finite intermediate value\n * collapses to the cap, so very high attempt counts never yield\n * `Infinity`/`NaN`/negative delays.\n */\n private computeBackoff(attempt: number): number {\n const raw = this.baseDelayMs * Math.pow(this.backoffFactor, attempt - 1);\n if (!Number.isFinite(raw) || raw < 0) return this.maxDelayMs;\n return Math.min(raw, this.maxDelayMs);\n }\n\n /**\n * Records still awaiting first delivery or a retry. Exposed for ops dashboards\n * and tests so a stuck transport (growing pending list) is observable.\n */\n async pending(): Promise<OutboxRecord[]> {\n try {\n return await this.store.pending();\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to read pending records', err);\n return [];\n }\n }\n\n /**\n * Records that exhausted all retries. Exposed so ops can detect and replay a\n * stuck transport; the list is never silently dropped/truncated.\n */\n async deadLettered(): Promise<OutboxRecord[]> {\n try {\n return await this.store.deadLettered();\n } catch (err) {\n this.logger.error('OutboxNotificationAdapter: failed to read dead-lettered records', err);\n return [];\n }\n }\n}\n","import type { Logger } from '../../utils/Logger.js';\nimport { noopLogger } from '../../utils/Logger.js';\nimport type {\n INotificationAdapter,\n NotificationEvent,\n} from '../../adapters/INotificationAdapter.js';\n\n/** A child adapter paired with a stable name for diagnostics/logging. */\nexport interface NamedNotificationChild {\n /** Human-readable identity used when logging this child's failures. */\n name: string;\n adapter: INotificationAdapter;\n}\n\n/** Either a bare adapter or a {@link NamedNotificationChild}. */\nexport type CompositeChild = INotificationAdapter | NamedNotificationChild;\n\n/** Configuration for {@link CompositeNotificationAdapter}. */\nexport interface CompositeNotificationAdapterOptions {\n /** Child adapters to fan out to. May be empty (resolves as a no-op). */\n children: CompositeChild[];\n /** Structured logger. Defaults to {@link noopLogger}. */\n logger?: Logger;\n}\n\nfunction isNamed(child: CompositeChild): child is NamedNotificationChild {\n // A real bare adapter always exposes its own notify(); never treat it as a\n // NamedNotificationChild even if it happens to also carry an `adapter` property.\n if (typeof (child as INotificationAdapter).notify === 'function') return false;\n // Otherwise it's a NamedNotificationChild only if it wraps a non-null `adapter`\n // object whose own `.notify` is callable.\n const adapter = (child as NamedNotificationChild).adapter as unknown;\n return (\n typeof adapter === 'object' &&\n adapter !== null &&\n typeof (adapter as INotificationAdapter).notify === 'function'\n );\n}\n\n/**\n * Fans a single {@link notify} call out to N child {@link INotificationAdapter}s\n * concurrently.\n *\n * Uses `Promise.allSettled` so one failing/slow child never blocks delivery to\n * the others. Never throws: every child rejection is collected and logged with\n * that child's identity. Zero children resolves immediately as a no-op.\n *\n * Drop-in for `ApprovalEngineOptions.notificationAdapter`.\n */\nexport class CompositeNotificationAdapter implements INotificationAdapter {\n private readonly children: NamedNotificationChild[];\n private readonly logger: Logger;\n\n constructor(options: CompositeNotificationAdapterOptions) {\n this.children = options.children.map((child, i) =>\n isNamed(child) ? child : { name: `child[${i}]`, adapter: child },\n );\n this.logger = options.logger ?? noopLogger;\n }\n\n /**\n * Deliver the event to every child concurrently. Resolves once all children\n * settle; never rejects.\n */\n async notify(event: NotificationEvent): Promise<void> {\n if (this.children.length === 0) return;\n\n const results = await Promise.allSettled(\n // Wrap each call so a synchronous throw inside a child's notify is also\n // captured as a rejection rather than escaping the fan-out.\n this.children.map((child) => Promise.resolve().then(() => child.adapter.notify(event))),\n );\n\n results.forEach((result, i) => {\n if (result.status === 'rejected') {\n const child = this.children[i]!;\n this.logger.error('CompositeNotificationAdapter: child failed to notify', result.reason, {\n child: child.name,\n type: event.type,\n instanceId: event.instanceId,\n tenantId: event.tenantId,\n });\n }\n });\n }\n}\n","import type { Logger } from '../../utils/Logger.js';\nimport { noopLogger } from '../../utils/Logger.js';\nimport type { ApprovalEventName } from '../../types/events.js';\nimport type {\n INotificationAdapter,\n NotificationEvent,\n} from '../../adapters/INotificationAdapter.js';\n\n/** The fully rendered, channel-ready message handed to the send fn. */\nexport interface RenderedNotification {\n /** Logical delivery channel (e.g. 'email', 'slack', 'sms'). */\n channel: string;\n /** Resolved recipient address(es) for the channel. */\n to: string[];\n /** Short headline / email subject. */\n subject: string;\n /** Human-readable body. */\n body: string;\n}\n\n/** The shape a template function returns (channel/to are derived separately). */\nexport interface RenderedMessage {\n subject: string;\n body: string;\n}\n\n/**\n * A template entry for one {@link ApprovalEventName}. Either:\n * - a function `(event) => { subject, body }`, for full programmatic control, or\n * - a `{ subject, body }` pair of strings with `{placeholder}` tokens that are\n * interpolated from the event and its payload.\n */\nexport type NotificationTemplate =\n | ((event: NotificationEvent) => RenderedMessage)\n | { subject: string; body: string };\n\n/** Map of event name → template. Any subset of events may be configured. */\nexport type TemplateMap = Partial<Record<ApprovalEventName, NotificationTemplate>>;\n\n/** Side-effecting send function the adapter forwards rendered messages to. */\nexport type SendFn = (message: RenderedNotification) => void | Promise<void>;\n\n/** Configuration for {@link TemplatedNotificationAdapter}. */\nexport interface TemplatedNotificationAdapterOptions {\n /** Side-effecting send function. Required. */\n send: SendFn;\n /** Per-event templates. Events without an entry use {@link fallbackTemplate} (if any). */\n templates?: TemplateMap;\n /**\n * Template used when no per-event entry exists. If omitted, events without a\n * template are skipped (logged) rather than throwing. Set to a function or a\n * `{subject, body}` string pair to guarantee a message for every event.\n */\n fallbackTemplate?: NotificationTemplate;\n /**\n * Derive the channel for an event. Defaults to the constant `'default'`.\n */\n channelFor?: (event: NotificationEvent) => string;\n /**\n * Derive recipient address(es). Defaults to `event.recipients`. When this\n * returns an empty array the adapter falls back to {@link defaultRecipients}\n * (if set) and otherwise skips the send gracefully.\n */\n recipientsFor?: (event: NotificationEvent) => string[];\n /**\n * Recipients used when {@link recipientsFor} yields none (e.g. cancelled /\n * expired / sla_breached events carry empty `recipients`). If also empty the\n * send is skipped rather than dispatched to nobody.\n */\n defaultRecipients?: string[];\n /**\n * Token substituted for a `{placeholder}` that resolves to `undefined`/`null`\n * or references a field absent on the payload. Defaults to `''` (empty\n * string). Interpolation never throws on unknown placeholders.\n */\n unknownPlaceholderToken?: string;\n /** Structured logger. Defaults to {@link noopLogger}. */\n logger?: Logger;\n}\n\nconst PLACEHOLDER = /\\{([^{}]+)\\}/g;\n\n/**\n * Renders a human-readable message per {@link ApprovalEventName} from a\n * configurable template map and forwards `{ channel, to, subject, body }` to an\n * injected send function.\n *\n * Templates are resolved by event name; a missing template falls back to\n * `fallbackTemplate` or — if none is configured — the event is skipped (logged)\n * rather than throwing. String templates support `{placeholder}` interpolation\n * pulled from top-level event fields and `event.payload` fields; an unknown or\n * absent field renders to `unknownPlaceholderToken` and never throws.\n *\n * `notify()` never throws — send errors and any rendering issues are caught and\n * logged. Drop-in for `ApprovalEngineOptions.notificationAdapter`.\n */\nexport class TemplatedNotificationAdapter implements INotificationAdapter {\n private readonly send: SendFn;\n private readonly templates: TemplateMap;\n private readonly fallbackTemplate?: NotificationTemplate;\n private readonly channelFor: (event: NotificationEvent) => string;\n private readonly recipientsFor: (event: NotificationEvent) => string[];\n private readonly defaultRecipients: string[];\n private readonly unknownPlaceholderToken: string;\n private readonly logger: Logger;\n\n constructor(options: TemplatedNotificationAdapterOptions) {\n this.send = options.send;\n this.templates = options.templates ?? {};\n this.fallbackTemplate = options.fallbackTemplate;\n this.channelFor = options.channelFor ?? (() => 'default');\n this.recipientsFor = options.recipientsFor ?? ((event) => event.recipients ?? []);\n this.defaultRecipients = options.defaultRecipients ?? [];\n this.unknownPlaceholderToken = options.unknownPlaceholderToken ?? '';\n this.logger = options.logger ?? noopLogger;\n }\n\n /**\n * Render and dispatch the event. Never throws: missing templates, empty\n * recipients, and send failures are all handled and logged.\n */\n async notify(event: NotificationEvent): Promise<void> {\n try {\n const template = this.templates[event.type] ?? this.fallbackTemplate;\n if (!template) {\n this.logger.debug('TemplatedNotificationAdapter: no template for event, skipping', {\n type: event.type,\n instanceId: event.instanceId,\n });\n return;\n }\n\n const to = this.resolveRecipients(event);\n if (to.length === 0) {\n this.logger.debug('TemplatedNotificationAdapter: no recipients for event, skipping', {\n type: event.type,\n instanceId: event.instanceId,\n });\n return;\n }\n\n const rendered = this.render(template, event);\n const message: RenderedNotification = {\n channel: this.channelFor(event),\n to,\n subject: rendered.subject,\n body: rendered.body,\n };\n\n await this.send(message);\n } catch (err) {\n this.logger.error('TemplatedNotificationAdapter: failed to render/send notification', err, {\n type: event.type,\n instanceId: event.instanceId,\n tenantId: event.tenantId,\n });\n }\n }\n\n private resolveRecipients(event: NotificationEvent): string[] {\n const derived = this.recipientsFor(event);\n if (derived.length > 0) return derived;\n return this.defaultRecipients;\n }\n\n private render(template: NotificationTemplate, event: NotificationEvent): RenderedMessage {\n if (typeof template === 'function') {\n return template(event);\n }\n return {\n subject: this.interpolate(template.subject, event),\n body: this.interpolate(template.body, event),\n };\n }\n\n /**\n * Replace `{token}` occurrences in `text`. A token is resolved against\n * top-level event fields first, then `event.payload`. Dotted paths\n * (`payload.level`, `a.b.c`) are supported. Anything unresolved renders to\n * `unknownPlaceholderToken`. Never throws.\n */\n private interpolate(text: string, event: NotificationEvent): string {\n return text.replace(PLACEHOLDER, (_match, rawKey: string) => {\n const key = rawKey.trim();\n const value = this.lookup(event, key);\n if (value === undefined || value === null) return this.unknownPlaceholderToken;\n return this.stringify(value);\n });\n }\n\n /** Resolve a (possibly dotted) key against the event then its payload. */\n private lookup(event: NotificationEvent, key: string): unknown {\n const fromEvent = this.dig(event as unknown as Record<string, unknown>, key);\n if (fromEvent !== undefined) return fromEvent;\n return this.dig(event.payload as unknown as Record<string, unknown>, key);\n }\n\n private dig(root: Record<string, unknown> | undefined, path: string): unknown {\n if (!root) return undefined;\n let current: unknown = root;\n for (const segment of path.split('.')) {\n if (current === null || typeof current !== 'object') return undefined;\n current = (current as Record<string, unknown>)[segment];\n }\n return current;\n }\n\n private stringify(value: unknown): string {\n if (value instanceof Date) return value.toISOString();\n if (Array.isArray(value)) return value.map((v) => this.stringify(v)).join(', ');\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return this.unknownPlaceholderToken;\n }\n }\n return String(value);\n }\n}\n","import type { Clock } from '../../utils/Clock.js';\nimport { systemClock } from '../../utils/Clock.js';\nimport type { Logger } from '../../utils/Logger.js';\nimport { noopLogger } from '../../utils/Logger.js';\nimport type {\n INotificationAdapter,\n NotificationEvent,\n} from '../../adapters/INotificationAdapter.js';\n\n/** One recipient's accumulated events, handed to {@link DigestSendFn} on flush. */\nexport interface Digest {\n recipient: string;\n /** Events for this recipient, oldest first. */\n events: NotificationEvent[];\n /** When the earliest event in this digest arrived. */\n since: Date;\n /** When the digest was flushed. */\n flushedAt: Date;\n}\n\n/** Delivers one recipient's digest. Must not throw — failures are logged and swallowed. */\nexport type DigestSendFn = (digest: Digest) => Promise<void> | void;\n\nexport interface DigestNotificationAdapterOptions {\n /** Called once per recipient per flush. */\n send: DigestSendFn;\n /**\n * Event types delivered immediately instead of being batched. A rejection or\n * a completed approval is news the recipient acts on now; batching it behind\n * a digest window would make the library's own notifications the reason a\n * decision was late.\n *\n * Defaults to rejections, completions, SLA breaches and expiries.\n */\n passthrough?: NotificationEvent['type'][];\n /**\n * Flush a recipient's digest once it reaches this many events, regardless of\n * the timer. Prevents an unbounded buffer under a burst.\n */\n maxBatchSize?: number;\n /**\n * Flush every recipient this often, in milliseconds. Omit to disable the\n * timer and flush only via {@link DigestNotificationAdapter.flush} — the right\n * choice when a cron job or queue worker owns the schedule.\n */\n intervalMs?: number;\n logger?: Logger;\n clock?: Clock;\n}\n\n/** Event types that reach the recipient immediately unless the caller says otherwise. */\nconst DEFAULT_PASSTHROUGH: NotificationEvent['type'][] = [\n 'approval:rejected',\n 'approval:completed',\n 'approval:sla_breached',\n 'approval:expired',\n];\n\nconst DEFAULT_MAX_BATCH_SIZE = 50;\n\n/**\n * Batches notifications per recipient instead of sending one per event.\n *\n * An approver on twenty documents receives twenty separate messages a day from\n * a naive adapter, which is how approval email ends up filtered into a folder\n * nobody reads — the notifications defeat themselves. This collects events per\n * recipient and delivers one digest.\n *\n * **Urgent events still go straight through.** Batching a rejection or a\n * completed approval behind a digest window would make the library's own\n * notifications the reason a decision was late, so those bypass the buffer by\n * default; see {@link DigestNotificationAdapterOptions.passthrough}.\n *\n * Buffers live in memory. A process restart drops whatever has not been\n * flushed, which is the right trade for a convenience digest but not for\n * delivery guarantees — put {@link OutboxNotificationAdapter} underneath when\n * an event must not be lost.\n *\n * @example\n * ```ts\n * const digest = new DigestNotificationAdapter({\n * intervalMs: 15 * 60_000,\n * send: async ({ recipient, events }) => mailer.send(recipient, summarise(events)),\n * });\n * const engine = new ApprovalEngine({ adapter, notificationAdapter: digest });\n * // ...on shutdown\n * await digest.stop();\n * ```\n */\nexport class DigestNotificationAdapter implements INotificationAdapter {\n private readonly buffers = new Map<string, { events: NotificationEvent[]; since: Date }>();\n private readonly send: DigestSendFn;\n private readonly passthrough: Set<NotificationEvent['type']>;\n private readonly maxBatchSize: number;\n private readonly intervalMs?: number;\n private readonly logger: Logger;\n private readonly clock: Clock;\n private timer: ReturnType<typeof setInterval> | null = null;\n\n constructor(opts: DigestNotificationAdapterOptions) {\n this.send = opts.send;\n this.passthrough = new Set(opts.passthrough ?? DEFAULT_PASSTHROUGH);\n this.maxBatchSize = opts.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;\n this.intervalMs = opts.intervalMs;\n this.logger = opts.logger ?? noopLogger;\n this.clock = opts.clock ?? systemClock;\n\n if (this.intervalMs !== undefined) {\n if (this.intervalMs <= 0) {\n throw new Error('DigestNotificationAdapter: intervalMs must be a positive number.');\n }\n this.timer = setInterval(() => {\n void this.flush().catch((err) => {\n this.logger.error('DigestNotificationAdapter: scheduled flush failed', err);\n });\n }, this.intervalMs);\n // Never hold the process open for a convenience digest.\n this.timer.unref?.();\n }\n }\n\n /** Recipients currently holding buffered events. */\n get pendingRecipients(): number {\n return this.buffers.size;\n }\n\n async notify(event: NotificationEvent): Promise<void> {\n if (this.passthrough.has(event.type)) {\n await this.deliver({\n recipient: '',\n events: [event],\n since: event.timestamp,\n flushedAt: this.clock.now(),\n });\n return;\n }\n\n const now = this.clock.now();\n const full: string[] = [];\n\n for (const recipient of event.recipients) {\n const buffer = this.buffers.get(recipient) ?? { events: [], since: now };\n buffer.events.push(event);\n this.buffers.set(recipient, buffer);\n if (buffer.events.length >= this.maxBatchSize) full.push(recipient);\n }\n\n // Flush over-full recipients only; a burst aimed at one person must not\n // force everybody else's digest out early.\n for (const recipient of full) {\n await this.flushRecipient(recipient);\n }\n }\n\n /** Deliver every buffered digest now. Safe to call from a cron job or on shutdown. */\n async flush(): Promise<void> {\n for (const recipient of [...this.buffers.keys()]) {\n await this.flushRecipient(recipient);\n }\n }\n\n /** Stop the timer and deliver whatever is buffered. */\n async stop(): Promise<void> {\n if (this.timer !== null) {\n clearInterval(this.timer);\n this.timer = null;\n }\n await this.flush();\n }\n\n private async flushRecipient(recipient: string): Promise<void> {\n const buffer = this.buffers.get(recipient);\n if (!buffer || buffer.events.length === 0) return;\n // Drop the buffer before sending: a send that throws must not replay the\n // same events into the next digest forever.\n this.buffers.delete(recipient);\n\n await this.deliver({\n recipient,\n events: buffer.events,\n since: buffer.since,\n flushedAt: this.clock.now(),\n });\n }\n\n private async deliver(digest: Digest): Promise<void> {\n try {\n await this.send(digest);\n } catch (err) {\n this.logger.error('DigestNotificationAdapter: send failed', err, {\n recipient: digest.recipient,\n events: digest.events.length,\n });\n }\n }\n}\n"]}
package/dist/testing.cjs CHANGED
@@ -915,6 +915,7 @@ function computeTimingStats(samples) {
915
915
 
916
916
  // src/engine/ApprovalEngine.ts
917
917
  var MAX_SUBWORKFLOW_DEPTH = 5;
918
+ var DEFAULT_PURGE_LIMIT = 1e3;
918
919
  var DEFAULT_MAX_REMINDERS = 3;
919
920
  var DEFAULT_MAX_ATTEMPTS = 3;
920
921
  var DEFAULT_BASE_DELAY_MS = 50;
@@ -3120,6 +3121,82 @@ var ApprovalEngine = class _ApprovalEngine {
3120
3121
  });
3121
3122
  return result;
3122
3123
  }
3124
+ /**
3125
+ * Permanently remove finished approvals older than a cut-off.
3126
+ *
3127
+ * Approval tables only grow, and data-minimisation rules eventually require
3128
+ * old records to go. There was no way to remove one, so operators reached
3129
+ * around the library and deleted rows directly — which is exactly where
3130
+ * orphaned audit rows and half-deleted instances come from.
3131
+ *
3132
+ * **Only terminal instances are eligible.** A pending approval is live work;
3133
+ * deleting one would strand a document with no way to finish and no record of
3134
+ * why. Passing a non-terminal status is rejected rather than quietly ignored,
3135
+ * because a caller who asked to purge pending work has misunderstood
3136
+ * something and should hear about it.
3137
+ *
3138
+ * This is irreversible and removes the audit trail with the instance. In many
3139
+ * deployments that trail *is* the compliance record, which is why the
3140
+ * underlying `deleteInstance` is an optional adapter method: an adapter that
3141
+ * does not implement it makes the whole operation unavailable, and this
3142
+ * throws rather than reporting a successful purge of nothing.
3143
+ *
3144
+ * @param opts - Cut-off, optional status/type scoping, safety limit, dry run.
3145
+ * @returns What was removed, or would be under `dryRun`.
3146
+ */
3147
+ async purgeInstances(opts) {
3148
+ const dryRun = opts.dryRun ?? false;
3149
+ const limit = opts.limit ?? DEFAULT_PURGE_LIMIT;
3150
+ if (!(opts.olderThan instanceof Date) || Number.isNaN(opts.olderThan.getTime())) {
3151
+ throw new ApprovalValidationError("purgeInstances requires a valid olderThan date.");
3152
+ }
3153
+ const requested = opts.statuses ?? [...TERMINAL_STATUSES];
3154
+ const nonTerminal = requested.filter((st) => !TERMINAL_STATUSES.has(st));
3155
+ if (nonTerminal.length > 0) {
3156
+ throw new ApprovalValidationError(
3157
+ `purgeInstances refuses non-terminal statuses (${nonTerminal.join(", ")}): a pending approval is live work, and removing it would strand the document it belongs to.`
3158
+ );
3159
+ }
3160
+ const deleteInstance = this.opts.adapter.deleteInstance?.bind(this.opts.adapter);
3161
+ if (!deleteInstance && !dryRun) {
3162
+ throw new ApprovalValidationError(
3163
+ "The configured storage adapter does not implement deleteInstance(), so instances cannot be purged."
3164
+ );
3165
+ }
3166
+ const result = { purged: [], scanned: 0, dryRun };
3167
+ for (const status of requested) {
3168
+ if (result.purged.length >= limit) break;
3169
+ const page = await this.opts.adapter.getInstancesByFilter(
3170
+ this.tenantId,
3171
+ {
3172
+ status,
3173
+ toDate: opts.olderThan,
3174
+ ...opts.documentType ? { documentType: opts.documentType } : {}
3175
+ },
3176
+ { limit, offset: 0 }
3177
+ );
3178
+ for (const instance of page.items) {
3179
+ if (result.purged.length >= limit) break;
3180
+ result.scanned++;
3181
+ if (!TERMINAL_STATUSES.has(instance.status)) continue;
3182
+ if (new Date(instance.createdAt) > opts.olderThan) continue;
3183
+ if (!dryRun) await deleteInstance(this.tenantId, instance.id);
3184
+ result.purged.push({
3185
+ instanceId: instance.id,
3186
+ documentId: instance.documentId,
3187
+ status: instance.status
3188
+ });
3189
+ }
3190
+ }
3191
+ this.logger.info("purgeInstances: sweep complete", {
3192
+ tenantId: this.tenantId,
3193
+ olderThan: opts.olderThan.toISOString(),
3194
+ scanned: result.scanned,
3195
+ purged: result.purged.length,
3196
+ dryRun
3197
+ });
3198
+ return result;
3199
+ }
3123
3200
  async getStatistics(filter = {}) {
3124
3201
  const statuses = [
3125
3202
  "pending",
@@ -4122,6 +4199,10 @@ var MemoryAdapter = class {
4122
4199
  return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry || hasDueReminder;
4123
4200
  }).map((i) => reviveDates(deepClone(i)));
4124
4201
  }
4202
+ async deleteInstance(tenantId, id) {
4203
+ const key = `${tenantId}:${id}`;
4204
+ return this.instances.delete(key);
4205
+ }
4125
4206
  async countInstances(tenantId, filter) {
4126
4207
  let count = 0;
4127
4208
  for (const instance of this.instances.values()) {