hierarchical-approval 0.5.0 → 0.6.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 (77) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +66 -0
  3. package/dist/{ApprovalEngine-mkh6D9n9.d.cts → ApprovalEngine-CIQFYhsX.d.cts} +100 -18
  4. package/dist/{ApprovalEngine-jy4cYeES.d.ts → ApprovalEngine-D3rHIli1.d.ts} +100 -18
  5. package/dist/ISchedulerAdapter-DKv_QjVN.d.cts +13 -0
  6. package/dist/ISchedulerAdapter-DKv_QjVN.d.ts +13 -0
  7. package/dist/{IStorageAdapter-ClBdEeTj.d.cts → IStorageAdapter-BibHlgPw.d.cts} +1 -1
  8. package/dist/{IStorageAdapter-8ajiGUeu.d.ts → IStorageAdapter-D-oPxxun.d.ts} +1 -1
  9. package/dist/{Logger-CIzvalFc.d.cts → Logger-BplhlU7l.d.cts} +1 -0
  10. package/dist/{Logger-CIzvalFc.d.ts → Logger-BplhlU7l.d.ts} +1 -0
  11. package/dist/adapters/MemoryAdapter.cjs +19 -7
  12. package/dist/adapters/MemoryAdapter.cjs.map +1 -1
  13. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  14. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  15. package/dist/adapters/MemoryAdapter.js +19 -7
  16. package/dist/adapters/MemoryAdapter.js.map +1 -1
  17. package/dist/adapters/PostgresAdapter.cjs +60 -31
  18. package/dist/adapters/PostgresAdapter.cjs.map +1 -1
  19. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  20. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  21. package/dist/adapters/PostgresAdapter.js +60 -31
  22. package/dist/adapters/PostgresAdapter.js.map +1 -1
  23. package/dist/index.cjs +717 -99
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +6 -5
  26. package/dist/index.d.ts +6 -5
  27. package/dist/index.js +717 -99
  28. package/dist/index.js.map +1 -1
  29. package/dist/nestjs.cjs +698 -92
  30. package/dist/nestjs.cjs.map +1 -1
  31. package/dist/nestjs.d.cts +4 -3
  32. package/dist/nestjs.d.ts +4 -3
  33. package/dist/nestjs.js +698 -92
  34. package/dist/nestjs.js.map +1 -1
  35. package/dist/plugins/audit.cjs +2 -0
  36. package/dist/plugins/audit.cjs.map +1 -1
  37. package/dist/plugins/audit.d.cts +1 -1
  38. package/dist/plugins/audit.d.ts +1 -1
  39. package/dist/plugins/audit.js +2 -0
  40. package/dist/plugins/audit.js.map +1 -1
  41. package/dist/plugins/metrics.cjs +2 -0
  42. package/dist/plugins/metrics.cjs.map +1 -1
  43. package/dist/plugins/metrics.d.cts +1 -1
  44. package/dist/plugins/metrics.d.ts +1 -1
  45. package/dist/plugins/metrics.js +2 -0
  46. package/dist/plugins/metrics.js.map +1 -1
  47. package/dist/plugins/notify.cjs +2 -0
  48. package/dist/plugins/notify.cjs.map +1 -1
  49. package/dist/plugins/notify.d.cts +1 -1
  50. package/dist/plugins/notify.d.ts +1 -1
  51. package/dist/plugins/notify.js +2 -0
  52. package/dist/plugins/notify.js.map +1 -1
  53. package/dist/plugins/resilience.cjs +2 -0
  54. package/dist/plugins/resilience.cjs.map +1 -1
  55. package/dist/plugins/resilience.d.cts +1 -1
  56. package/dist/plugins/resilience.d.ts +1 -1
  57. package/dist/plugins/resilience.js +2 -0
  58. package/dist/plugins/resilience.js.map +1 -1
  59. package/dist/plugins/scheduler.cjs +69 -0
  60. package/dist/plugins/scheduler.cjs.map +1 -0
  61. package/dist/plugins/scheduler.d.cts +43 -0
  62. package/dist/plugins/scheduler.d.ts +43 -0
  63. package/dist/plugins/scheduler.js +67 -0
  64. package/dist/plugins/scheduler.js.map +1 -0
  65. package/dist/plugins/webhook.cjs +226 -0
  66. package/dist/plugins/webhook.cjs.map +1 -0
  67. package/dist/plugins/webhook.d.cts +214 -0
  68. package/dist/plugins/webhook.d.ts +214 -0
  69. package/dist/plugins/webhook.js +221 -0
  70. package/dist/plugins/webhook.js.map +1 -0
  71. package/dist/testing.cjs +717 -99
  72. package/dist/testing.cjs.map +1 -1
  73. package/dist/testing.d.cts +4 -3
  74. package/dist/testing.d.ts +4 -3
  75. package/dist/testing.js +717 -99
  76. package/dist/testing.js.map +1 -1
  77. package/package.json +11 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,134 @@
3
3
  All notable changes to `hierarchical-approval` are documented here. This project
4
4
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [Unreleased]
7
+
8
+ _Nothing yet._
9
+
10
+ ## [0.6.0] - 2026-08-21
11
+
12
+ ### Fixed — event delivery, template reads, and the CI lint gate
13
+
14
+ - **A throwing `engine.on()` listener no longer breaks the operation that emitted
15
+ the event.** `EventBus` delivered events straight through `eventemitter3`, which
16
+ invokes listeners synchronously and does not swallow exceptions — and the engine
17
+ emits *after* persisting but *before* dispatching notifications and running
18
+ after-middleware. So one buggy subscriber would reject `approve()` with its own
19
+ error (while the instance stayed persisted as approved), skip notification
20
+ dispatch entirely, leave a tracing span opened and never ended, and suppress the
21
+ `approval:completed` emit on the following line. Listener failures are now
22
+ isolated per listener and reported via the engine's logger; a rejecting `async`
23
+ listener is caught too, instead of surfacing as a process-level
24
+ `unhandledRejection`. **Behaviour change:** a listener error no longer propagates
25
+ to the caller. Register an `onListenerError` sink by passing a `logger`, or use
26
+ `EventBus.setListenerErrorHandler` directly.
27
+ - **`approval:completed` is now delivered to notification adapters.** It was
28
+ emitted on the in-process bus only, so a webhook/email integrator never learned
29
+ a document had been fully approved and had to infer it from `approval:approved`
30
+ plus `isFinal`. Dispatched on both the normal-completion and `override` paths.
31
+ - **`PostgresAdapter.getTemplate`/`listTemplates` now return `createdAt` as a real
32
+ `Date`.** They returned the JSONB payload raw, so `createdAt` was an ISO string
33
+ despite `ApprovalTemplate` typing it as a `Date`. `TemplateRegistry.update()`
34
+ threaded that string into the next `saveTemplate()`, which called
35
+ `createdAt.toISOString()` on it — meaning **`engine.updateTemplate()` failed 100%
36
+ of the time against real PostgreSQL** while passing against `MemoryAdapter`,
37
+ which tolerates the string silently. No migration needed.
38
+ - **CI enforces lint again.** The workflow ran `npm run lint || true`, so lint could
39
+ never fail a build; an unused import had already accumulated on `main` as a
40
+ result. The `|| true` is gone.
41
+
42
+ ### Added — cycle-time analytics
43
+
44
+ - **`getStatistics()` now reports time-to-decision.** The returned
45
+ `ApprovalStatistics` gains `cycleTime: CycleTimeStats` and
46
+ `cycleTimeByTemplate: Record<string, CycleTimeStats>`, each shaped as
47
+ `{ count, averageMs, p50Ms, p95Ms, minMs, maxMs }` (all durations in
48
+ milliseconds; new `CycleTimeStats` interface, exported from the package root).
49
+ - Counts instances in status `approved`, `rejected`, or `cancelled`.
50
+ **`expired` is deliberately excluded** — its terminal timestamp reflects a
51
+ scheduler deadline firing, not a decision, so including it would skew the
52
+ distribution rather than describe it.
53
+ - Elapsed time per instance is `updatedAt - createdAt`; no new fields were
54
+ added to `ApprovalInstance` or either storage adapter.
55
+ - Every field is `0` (never `NaN`) when `count` is `0`. `cycleTimeByTemplate`
56
+ mirrors `byTemplate`'s population rule: a template only appears once it has
57
+ at least one completed instance.
58
+ - Adapter-agnostic — computed from existing `getInstancesByFilter` pages, so
59
+ `MemoryAdapter` and `PostgresAdapter` both support it with no changes.
60
+
61
+ ### Added — `plugins/webhook`
62
+
63
+ - **`hierarchical-approval/plugins/webhook`** — a sixth built-in plug-in
64
+ subpath: an HTTP `INotificationAdapter` with signing and retry, on its own
65
+ tree-shakeable import path with **zero new dependencies**.
66
+ - `WebhookNotificationAdapter` POSTs each event as JSON to a configured URL.
67
+ `notify()` never throws (per the `INotificationAdapter` contract) — it logs
68
+ and drops the notification once retries are exhausted.
69
+ - **Signing (opt-in via `secret`).** Each request carries an
70
+ `X-Approval-Signature: t=<unix-seconds>,v1=<hex-hmac>` header (Stripe-style),
71
+ the HMAC-SHA256 digest of the signing string `` `<unix-seconds>.<json-body>` ``.
72
+ Unsigned when `secret` is omitted. `DEFAULT_SIGNATURE_HEADER` names the
73
+ default header (`'X-Approval-Signature'`); override via `signatureHeader`.
74
+ - **Retry.** `5xx`, `408`, `429`, and network/timeout errors are retried with
75
+ exponential backoff and full jitter, up to `maxAttempts` (default `3`); a
76
+ `429` honors `Retry-After` (seconds or an HTTP date) in place of the
77
+ computed backoff. Any other `4xx` fails on the first attempt.
78
+ - **Durability.** The public `deliver()` method throws
79
+ `WebhookDeliveryError` (`status?`, `attempts`, `cause?`) on final failure
80
+ instead of swallowing it, so it can be bound as the `transport` of
81
+ `plugins/notify`'s `OutboxNotificationAdapter` for at-least-once delivery
82
+ across process restarts — no adapter shim needed.
83
+ - Ships a new `HttpClient` port (a plain `fetch`-shaped function type) and
84
+ `getDefaultHttpClient()`. **No new dependency** — the port is structurally
85
+ satisfied by the global `fetch` (Node.js 18+); pass a custom `httpClient`
86
+ to use a different implementation.
87
+
88
+ ### Added — `plugins/scheduler`, and the `schedulerAdapter` option now works
89
+
90
+ - **`hierarchical-approval/plugins/scheduler`** — ships `InMemorySchedulerAdapter`,
91
+ the first reference implementation of the `ISchedulerAdapter` port, which until
92
+ now was exported from the package root with no implementation anywhere to copy.
93
+ - **`ApprovalEngineOptions.schedulerAdapter` was a no-op.** It was declared and
94
+ documented as "Replaces built-in setInterval polling", but `scheduleAt` and
95
+ `cancel` were never called — an injected BullMQ/Temporal/cron scheduler was
96
+ silently ignored while the built-in poller kept running. When supplied it now
97
+ drives the periodic scan via self-rescheduling one-shot calls. Default
98
+ behaviour with no option supplied is unchanged.
99
+
100
+ ### Fixed — `PostgresAdapter`
101
+
102
+ Three defects affecting users on published `0.5.0`. If you use `PostgresAdapter`,
103
+ upgrading is recommended.
104
+
105
+ - **Cursor pagination silently corrupted every page after the first.**
106
+ `getInstancesByCursor`'s cursor decoder split the `updatedAt_iso:id` string on
107
+ the _first_ colon, but an ISO-8601 timestamp (e.g.
108
+ `2026-06-26T09:00:00.000Z`) itself contains colons — so every decoded cursor
109
+ had a truncated timestamp and a corrupted id, breaking every subsequent page
110
+ fetch via `getInstancesByCursor`. Also hardened cursor encoding to normalize
111
+ through `Date.prototype.toISOString()` regardless of the row value's shape.
112
+ No migration needed.
113
+ - **`templateId` was silently dropped on every write and always read back as
114
+ `''`.** `PostgresAdapter` had no `template_id` column, so
115
+ `ApprovalInstance.templateId` was permanently lost for any instance persisted
116
+ through this adapter. Fixed by adding a `template_id` column and including it
117
+ in both the insert and read paths.
118
+ **Action required on upgrade:** run the adapter's migration (its
119
+ `CREATE TABLE`/`ALTER TABLE ... ADD COLUMN IF NOT EXISTS` statements, executed
120
+ automatically the next time the adapter initializes) to add the new column —
121
+ existing rows backfill to `''` until rewritten.
122
+ - **The `schema` option was interpolated into SQL without the safe-identifier
123
+ validation `tablePrefix` already had**, an inconsistent injection surface at
124
+ construction time. `schema` is now validated against the same
125
+ `/^[a-z][a-z0-9_]*$/` pattern as `tablePrefix`, throwing
126
+ `ApprovalValidationError` on an invalid value. No migration needed; only
127
+ affects adapter construction with an attacker-controlled `schema` value.
128
+
129
+ ### Fixed — `MemoryAdapter` and notifications
130
+
131
+ - **`TemplatedNotificationAdapter`** — now logs fatal events (notification render/send failures) at the `fatal` level instead of `error`.
132
+ - **`MemoryAdapter`** — `getInstancesByFilter` now honors `fromDate`/`toDate` (previously the JSON-cloned string dates made the comparison a no-op and both bounds silently matched nothing). `getTemplate`/`listTemplates` now return `createdAt` as a real `Date`, matching the `ApprovalTemplate` contract.
133
+
6
134
  ## [0.5.0] - 2026-07-23
7
135
 
8
136
  ### Added — NestJS integration
package/README.md CHANGED
@@ -721,9 +721,27 @@ const stats = await engine.getStatistics({ documentType: 'purchase_order' });
721
721
  // byStatus: { pending, approved, rejected, cancelled, expired },
722
722
  // overdue: number, // pending past an escalation/expiry deadline
723
723
  // approvalRate: number, // approved / (approved + rejected); 0 when none resolved
724
+ // byTemplate: Record<string, { total, approved, rejected, pending }>,
725
+ // cycleTime: CycleTimeStats,
726
+ // cycleTimeByTemplate: Record<string, CycleTimeStats>,
724
727
  // }
725
728
  ```
726
729
 
730
+ `byTemplate` breaks the same counts down per template name (only populated for templates with at least one matching instance).
731
+
732
+ **Cycle-time analytics.** `cycleTime` (and its per-template mirror `cycleTimeByTemplate`) reports time-to-decision, in milliseconds, as `{ count, averageMs, p50Ms, p95Ms, minMs, maxMs }`:
733
+
734
+ ```ts
735
+ console.log(`Median time-to-decision: ${(stats.cycleTime.p50Ms / 60_000).toFixed(1)} min`);
736
+ console.log(`p95 over ${stats.cycleTime.count} completed instances: ${stats.cycleTime.p95Ms}ms`);
737
+
738
+ for (const [templateName, timing] of Object.entries(stats.cycleTimeByTemplate)) {
739
+ console.log(templateName, timing.averageMs);
740
+ }
741
+ ```
742
+
743
+ Only instances in status `approved`, `rejected`, or `cancelled` count as "completed" for this calculation — **`expired` is deliberately excluded**: its terminal timestamp reflects a scheduler deadline firing, not a decision being made, so including it would skew the distribution rather than describe it. Elapsed time per instance is `updatedAt - createdAt`. When `count` is `0` every other field is `0`, never `NaN`.
744
+
727
745
  ### Health check
728
746
 
729
747
  ```ts
@@ -1072,6 +1090,54 @@ const engine = new ApprovalEngine({
1072
1090
 
1073
1091
  Every operation is wrapped in a span named `approval.<operation>` (e.g. `approval.approve`) carrying `approval.tenant_id`, `approval.actor_id` and `approval.instance_id` attributes. On success the span records the resulting `approval.result_status`/`approval.result_level` and status `OK`; on failure it calls `recordException`, tags `approval.error_code`, and sets status `ERROR` — then re-throws (tracing never swallows an error). There is **no hard dependency** on `@opentelemetry/api`: the middleware defaults to a no-op tracer, so you install and wire OpenTelemetry only if you want traces.
1074
1092
 
1093
+ ### `plugins/webhook` — signed HTTP delivery with retries
1094
+
1095
+ ```ts
1096
+ import { WebhookNotificationAdapter } from 'hierarchical-approval/plugins/webhook';
1097
+
1098
+ const webhook = new WebhookNotificationAdapter({
1099
+ url: 'https://example.com/hooks/approvals',
1100
+ secret: process.env.WEBHOOK_SECRET, // opt-in signing; omit to send unsigned requests
1101
+ });
1102
+
1103
+ const engine = new ApprovalEngine({ adapter, notificationAdapter: webhook });
1104
+ ```
1105
+
1106
+ Every event is POSTed to `url` as JSON. When `secret` is configured, each request also carries an `X-Approval-Signature: t=<unix-seconds>,v1=<hex-hmac>` header (Stripe-style), where `v1` is the HMAC-SHA256 digest of the signing string `` `${timestamp}.${body}` ``. Folding the timestamp into the signed payload lets a receiver reject stale/replayed requests. A receiver verifies it like this:
1107
+
1108
+ ```ts
1109
+ import { createHmac, timingSafeEqual } from 'node:crypto';
1110
+
1111
+ function verifyApprovalSignature(rawBody: string, header: string, secret: string): boolean {
1112
+ const [tPart, vPart] = header.split(',');
1113
+ const timestamp = tPart!.slice(2); // strip "t="
1114
+ const signature = vPart!.slice(3); // strip "v1="
1115
+ const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
1116
+ const sigBuf = Buffer.from(signature, 'hex');
1117
+ const expBuf = Buffer.from(expected, 'hex');
1118
+ return sigBuf.length === expBuf.length && timingSafeEqual(sigBuf, expBuf);
1119
+ }
1120
+ ```
1121
+
1122
+ `5xx` responses, `408`, and `429`, plus network/timeout errors, are retried with exponential backoff and full jitter up to `maxAttempts` (default `3`); a `429` honors a `Retry-After` header (seconds or an HTTP date) in place of the computed backoff. Any other `4xx` (e.g. `400`, `401`, `404`) is treated as permanent and fails on the first attempt. `notify()` — the method the engine calls — never throws, per the `INotificationAdapter` contract: on exhausting `maxAttempts` it logs and drops the notification, which makes the adapter used alone _at-most-once_.
1123
+
1124
+ For at-least-once delivery that survives a process restart, compose the adapter's throwing `deliver()` method as the `transport` of `OutboxNotificationAdapter` instead of wiring `notify()` directly:
1125
+
1126
+ ```ts
1127
+ import { OutboxNotificationAdapter } from 'hierarchical-approval/plugins/notify';
1128
+ import { WebhookNotificationAdapter } from 'hierarchical-approval/plugins/webhook';
1129
+
1130
+ const webhook = new WebhookNotificationAdapter({
1131
+ url: 'https://example.com/hooks/approvals',
1132
+ secret,
1133
+ });
1134
+ const durable = new OutboxNotificationAdapter({ transport: webhook.deliver.bind(webhook) });
1135
+
1136
+ const engine = new ApprovalEngine({ adapter, notificationAdapter: durable });
1137
+ ```
1138
+
1139
+ `deliver` and `OutboxNotificationAdapter`'s `NotificationTransport` type are structurally compatible, so this composition needs no adapter shim. This adapter takes **no new dependency**: its `HttpClient` port is a plain `fetch`-shaped function, and the global `fetch` (Node.js 18+) satisfies it directly — pass a custom `httpClient` in the options only if you need different behavior (a proxy, request mocking, a different runtime's fetch).
1140
+
1075
1141
  ---
1076
1142
 
1077
1143
  ## NestJS integration
@@ -1,12 +1,13 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-ClBdEeTj.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-BibHlgPw.cjs';
2
2
  import { m as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-D8D7b07N.cjs';
3
3
  import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-DVVmXU6a.cjs';
4
4
  import { z } from 'zod';
5
- import { L as Logger } from './Logger-CIzvalFc.cjs';
5
+ import { L as Logger } from './Logger-BplhlU7l.cjs';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.cjs';
7
7
  import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CXgXmGUF.cjs';
8
8
  import { I as IAuditAdapter } from './IAuditAdapter-B_DhuPsU.cjs';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.cjs';
10
+ import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
10
11
  import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-B6JzRNUk.cjs';
11
12
 
12
13
  declare const SubmitOptionsSchema: z.ZodObject<{
@@ -107,7 +108,8 @@ interface WeekendCalendarOptions {
107
108
  */
108
109
  declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
109
110
 
110
- type IdGeneratorFn = (prefix: 'inst' | 'tpl') => string;
111
+ type IdGeneratorPrefix = 'inst' | 'tpl';
112
+ type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
111
113
  declare const defaultIdGenerator: IdGeneratorFn;
112
114
 
113
115
  interface OrgProvider {
@@ -129,18 +131,6 @@ type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
129
131
 
130
132
  type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
131
133
 
132
- interface ISchedulerAdapter {
133
- /**
134
- * Schedule a one-shot callback to run at the given date.
135
- * Returns an opaque handle that can be passed to cancel().
136
- */
137
- scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
138
- /** Cancel a previously scheduled callback by its handle. */
139
- cancel(handle: string): Promise<void>;
140
- /** Gracefully shut down the scheduler and release resources. */
141
- shutdown(): Promise<void>;
142
- }
143
-
144
134
  interface ValidationResult {
145
135
  valid: boolean;
146
136
  errors: Array<{
@@ -192,6 +182,49 @@ interface ApprovalStatistics {
192
182
  rejected: number;
193
183
  pending: number;
194
184
  }>;
185
+ /**
186
+ * Time-to-decision ("cycle time") analytics, in milliseconds, for
187
+ * **completed** instances matching the filter — status `'approved'`,
188
+ * `'rejected'`, or `'cancelled'`. `'expired'` instances are excluded: their
189
+ * terminal timestamp reflects a scheduler deadline firing, not a decision
190
+ * being made, so they would skew the distribution rather than describe it.
191
+ *
192
+ * Elapsed time per instance is `updatedAt - createdAt`: `createdAt` is the
193
+ * submission instant, and `updatedAt` is set at the moment the instance
194
+ * transitions to its terminal status (see {@link ApprovalEngine.approve},
195
+ * {@link ApprovalEngine.reject}, {@link ApprovalEngine.cancel}). See
196
+ * {@link CycleTimeStats} for the zeroed shape returned when there are no
197
+ * completed instances.
198
+ */
199
+ cycleTime: CycleTimeStats;
200
+ /**
201
+ * The same {@link cycleTime} analytics broken down per template name.
202
+ * Mirrors the {@link byTemplate} population rule: a template only appears
203
+ * here when at least one of its instances is completed (`count > 0`).
204
+ */
205
+ cycleTimeByTemplate: Record<string, CycleTimeStats>;
206
+ }
207
+ /**
208
+ * Time-to-decision ("cycle time") statistics for a set of completed approval
209
+ * instances. All duration fields are in **milliseconds**.
210
+ *
211
+ * When {@link count} is `0` (no completed instances matched), every other
212
+ * field is `0` — never `NaN` — mirroring {@link computeTimingStats}'s
213
+ * empty-input behavior, which this type's values are derived from.
214
+ */
215
+ interface CycleTimeStats {
216
+ /** Number of completed instances included in this computation. */
217
+ count: number;
218
+ /** Arithmetic mean time-to-decision. `0` when {@link count} is `0`. */
219
+ averageMs: number;
220
+ /** 50th percentile (median) time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
221
+ p50Ms: number;
222
+ /** 95th percentile time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
223
+ p95Ms: number;
224
+ /** Smallest observed time-to-decision. `0` when {@link count} is `0`. */
225
+ minMs: number;
226
+ /** Largest observed time-to-decision. `0` when {@link count} is `0`. */
227
+ maxMs: number;
195
228
  }
196
229
  interface HealthResult {
197
230
  status: 'healthy' | 'degraded' | 'unhealthy';
@@ -225,7 +258,7 @@ interface ApprovalEngineOptions {
225
258
  */
226
259
  calendar?: BusinessCalendar;
227
260
  /** Custom ID generator for instances and templates. Defaults to timestamp+random. */
228
- generateId?: IdGeneratorFn;
261
+ generateId?: (prefix: IdGeneratorPrefix) => string;
229
262
  /** Custom optimistic locking retry policy. */
230
263
  retryPolicy?: RetryPolicy;
231
264
  /** Custom idempotency key derivation function. Default: SHA-256 of tenant+documentType+documentId+templateName. */
@@ -236,7 +269,20 @@ interface ApprovalEngineOptions {
236
269
  auditAdapter?: IAuditAdapter;
237
270
  /** Metrics adapter for Prometheus / Datadog / OpenTelemetry. */
238
271
  metricsAdapter?: IMetricsAdapter;
239
- /** Custom scheduler adapter (BullMQ, Temporal, cron). Replaces built-in setInterval polling. */
272
+ /**
273
+ * Custom scheduler adapter (BullMQ, Temporal, cron) that drives the recurring
274
+ * escalation/expiry/SLA-breach/delegation-revert scan.
275
+ *
276
+ * When provided, the built-in `setInterval` poll (see
277
+ * {@link EscalationScheduler.start}) is never started. Instead,
278
+ * {@link ISchedulerAdapter.scheduleAt} schedules each scan, and its callback
279
+ * reschedules the next one itself once the scan completes — the adapter
280
+ * changes *how* the periodic scan is triggered, not *what* it scans; every
281
+ * tick still runs the exact same overdue-instance query the built-in poller
282
+ * runs. {@link ISchedulerAdapter.cancel} and {@link ISchedulerAdapter.shutdown}
283
+ * are invoked during {@link ApprovalEngine.shutdown}. Omitting this option
284
+ * preserves the built-in `setInterval` polling behavior unchanged.
285
+ */
240
286
  schedulerAdapter?: ISchedulerAdapter;
241
287
  /** Authorization policy called before every mutating operation. */
242
288
  authorizationPolicy?: IAuthorizationPolicy;
@@ -249,6 +295,7 @@ declare class ApprovalEngine {
249
295
  private readonly registry;
250
296
  private readonly resolver;
251
297
  private readonly escalation;
298
+ private readonly escalationPollIntervalMs;
252
299
  private readonly tenantId;
253
300
  private readonly logger;
254
301
  private readonly clock;
@@ -257,6 +304,10 @@ declare class ApprovalEngine {
257
304
  private readonly maxBulkItems;
258
305
  private readonly retryPolicy;
259
306
  private readonly idempotencyKeyFn;
307
+ /** Handle for the currently-scheduled escalation tick when {@link ApprovalEngineOptions.schedulerAdapter} is set. */
308
+ private schedulerAdapterHandle;
309
+ /** Set by {@link shutdown}; stops the self-rescheduling loop from scheduling another tick. */
310
+ private schedulerStopped;
260
311
  constructor(opts: ApprovalEngineOptions);
261
312
  on<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
262
313
  off<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
@@ -313,6 +364,37 @@ declare class ApprovalEngine {
313
364
  */
314
365
  getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
315
366
  shutdown(): Promise<void>;
367
+ /**
368
+ * Schedules the next escalation scan via {@link ApprovalEngineOptions.schedulerAdapter}.
369
+ *
370
+ * Called once from the constructor to start the loop, then re-invoked by the
371
+ * scheduled callback itself after each scan completes — a self-rescheduling
372
+ * chain of one-shot {@link ISchedulerAdapter.scheduleAt} calls standing in for
373
+ * the `setInterval` that {@link EscalationScheduler.start} would otherwise
374
+ * use. Each invocation runs the exact same {@link EscalationScheduler.tick}
375
+ * scan the built-in poller runs; only the timer mechanism differs.
376
+ *
377
+ * A no-op once {@link shutdown} has set {@link schedulerStopped} — this is
378
+ * what stops the chain from rescheduling itself forever after teardown.
379
+ */
380
+ private scheduleNextEscalationTick;
381
+ /**
382
+ * Compute {@link CycleTimeStats} for every "completed" instance (see
383
+ * {@link CYCLE_TIME_STATUSES}) matching `filter`. Adapter-agnostic: fetches
384
+ * full instances (not just counts) via {@link fetchAllByFilter} so the
385
+ * actual `createdAt`/`updatedAt` timestamps are available, then reuses the
386
+ * shared {@link computeTimingStats} quantile routine from the metrics
387
+ * plugin rather than a second implementation.
388
+ */
389
+ private computeCycleTimeStats;
390
+ /**
391
+ * Page through every instance matching `filter` via the adapter's
392
+ * `getInstancesByFilter`, accumulating pages until the adapter reports no
393
+ * more results. Needed because adapters may impose a default page size
394
+ * (e.g. `PostgresAdapter` defaults to 50) when no explicit `limit` is given,
395
+ * so a single unbounded call cannot be relied on to return everything.
396
+ */
397
+ private fetchAllByFilter;
316
398
  private escalateInternal;
317
399
  private expireInstance;
318
400
  private markSlaBreached;
@@ -333,4 +415,4 @@ declare class ApprovalEngine {
333
415
  private runExternalAudit;
334
416
  }
335
417
 
336
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type ISchedulerAdapter as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type IdGeneratorFn as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, weekendCalendar as w };
418
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, weekendCalendar as w };
@@ -1,12 +1,13 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-8ajiGUeu.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-D-oPxxun.js';
2
2
  import { m as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-D8D7b07N.js';
3
3
  import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BdfVjYa8.js';
4
4
  import { z } from 'zod';
5
- import { L as Logger } from './Logger-CIzvalFc.js';
5
+ import { L as Logger } from './Logger-BplhlU7l.js';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.js';
7
7
  import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-KGAwT9f-.js';
8
8
  import { I as IAuditAdapter } from './IAuditAdapter-B3vvU09m.js';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-D9PUz4tM.js';
10
+ import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
10
11
  import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CZESF3CJ.js';
11
12
 
12
13
  declare const SubmitOptionsSchema: z.ZodObject<{
@@ -107,7 +108,8 @@ interface WeekendCalendarOptions {
107
108
  */
108
109
  declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
109
110
 
110
- type IdGeneratorFn = (prefix: 'inst' | 'tpl') => string;
111
+ type IdGeneratorPrefix = 'inst' | 'tpl';
112
+ type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
111
113
  declare const defaultIdGenerator: IdGeneratorFn;
112
114
 
113
115
  interface OrgProvider {
@@ -129,18 +131,6 @@ type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
129
131
 
130
132
  type ConditionOperatorFn = (actual: unknown, expected: unknown) => boolean;
131
133
 
132
- interface ISchedulerAdapter {
133
- /**
134
- * Schedule a one-shot callback to run at the given date.
135
- * Returns an opaque handle that can be passed to cancel().
136
- */
137
- scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
138
- /** Cancel a previously scheduled callback by its handle. */
139
- cancel(handle: string): Promise<void>;
140
- /** Gracefully shut down the scheduler and release resources. */
141
- shutdown(): Promise<void>;
142
- }
143
-
144
134
  interface ValidationResult {
145
135
  valid: boolean;
146
136
  errors: Array<{
@@ -192,6 +182,49 @@ interface ApprovalStatistics {
192
182
  rejected: number;
193
183
  pending: number;
194
184
  }>;
185
+ /**
186
+ * Time-to-decision ("cycle time") analytics, in milliseconds, for
187
+ * **completed** instances matching the filter — status `'approved'`,
188
+ * `'rejected'`, or `'cancelled'`. `'expired'` instances are excluded: their
189
+ * terminal timestamp reflects a scheduler deadline firing, not a decision
190
+ * being made, so they would skew the distribution rather than describe it.
191
+ *
192
+ * Elapsed time per instance is `updatedAt - createdAt`: `createdAt` is the
193
+ * submission instant, and `updatedAt` is set at the moment the instance
194
+ * transitions to its terminal status (see {@link ApprovalEngine.approve},
195
+ * {@link ApprovalEngine.reject}, {@link ApprovalEngine.cancel}). See
196
+ * {@link CycleTimeStats} for the zeroed shape returned when there are no
197
+ * completed instances.
198
+ */
199
+ cycleTime: CycleTimeStats;
200
+ /**
201
+ * The same {@link cycleTime} analytics broken down per template name.
202
+ * Mirrors the {@link byTemplate} population rule: a template only appears
203
+ * here when at least one of its instances is completed (`count > 0`).
204
+ */
205
+ cycleTimeByTemplate: Record<string, CycleTimeStats>;
206
+ }
207
+ /**
208
+ * Time-to-decision ("cycle time") statistics for a set of completed approval
209
+ * instances. All duration fields are in **milliseconds**.
210
+ *
211
+ * When {@link count} is `0` (no completed instances matched), every other
212
+ * field is `0` — never `NaN` — mirroring {@link computeTimingStats}'s
213
+ * empty-input behavior, which this type's values are derived from.
214
+ */
215
+ interface CycleTimeStats {
216
+ /** Number of completed instances included in this computation. */
217
+ count: number;
218
+ /** Arithmetic mean time-to-decision. `0` when {@link count} is `0`. */
219
+ averageMs: number;
220
+ /** 50th percentile (median) time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
221
+ p50Ms: number;
222
+ /** 95th percentile time-to-decision, via nearest-rank. `0` when {@link count} is `0`. */
223
+ p95Ms: number;
224
+ /** Smallest observed time-to-decision. `0` when {@link count} is `0`. */
225
+ minMs: number;
226
+ /** Largest observed time-to-decision. `0` when {@link count} is `0`. */
227
+ maxMs: number;
195
228
  }
196
229
  interface HealthResult {
197
230
  status: 'healthy' | 'degraded' | 'unhealthy';
@@ -225,7 +258,7 @@ interface ApprovalEngineOptions {
225
258
  */
226
259
  calendar?: BusinessCalendar;
227
260
  /** Custom ID generator for instances and templates. Defaults to timestamp+random. */
228
- generateId?: IdGeneratorFn;
261
+ generateId?: (prefix: IdGeneratorPrefix) => string;
229
262
  /** Custom optimistic locking retry policy. */
230
263
  retryPolicy?: RetryPolicy;
231
264
  /** Custom idempotency key derivation function. Default: SHA-256 of tenant+documentType+documentId+templateName. */
@@ -236,7 +269,20 @@ interface ApprovalEngineOptions {
236
269
  auditAdapter?: IAuditAdapter;
237
270
  /** Metrics adapter for Prometheus / Datadog / OpenTelemetry. */
238
271
  metricsAdapter?: IMetricsAdapter;
239
- /** Custom scheduler adapter (BullMQ, Temporal, cron). Replaces built-in setInterval polling. */
272
+ /**
273
+ * Custom scheduler adapter (BullMQ, Temporal, cron) that drives the recurring
274
+ * escalation/expiry/SLA-breach/delegation-revert scan.
275
+ *
276
+ * When provided, the built-in `setInterval` poll (see
277
+ * {@link EscalationScheduler.start}) is never started. Instead,
278
+ * {@link ISchedulerAdapter.scheduleAt} schedules each scan, and its callback
279
+ * reschedules the next one itself once the scan completes — the adapter
280
+ * changes *how* the periodic scan is triggered, not *what* it scans; every
281
+ * tick still runs the exact same overdue-instance query the built-in poller
282
+ * runs. {@link ISchedulerAdapter.cancel} and {@link ISchedulerAdapter.shutdown}
283
+ * are invoked during {@link ApprovalEngine.shutdown}. Omitting this option
284
+ * preserves the built-in `setInterval` polling behavior unchanged.
285
+ */
240
286
  schedulerAdapter?: ISchedulerAdapter;
241
287
  /** Authorization policy called before every mutating operation. */
242
288
  authorizationPolicy?: IAuthorizationPolicy;
@@ -249,6 +295,7 @@ declare class ApprovalEngine {
249
295
  private readonly registry;
250
296
  private readonly resolver;
251
297
  private readonly escalation;
298
+ private readonly escalationPollIntervalMs;
252
299
  private readonly tenantId;
253
300
  private readonly logger;
254
301
  private readonly clock;
@@ -257,6 +304,10 @@ declare class ApprovalEngine {
257
304
  private readonly maxBulkItems;
258
305
  private readonly retryPolicy;
259
306
  private readonly idempotencyKeyFn;
307
+ /** Handle for the currently-scheduled escalation tick when {@link ApprovalEngineOptions.schedulerAdapter} is set. */
308
+ private schedulerAdapterHandle;
309
+ /** Set by {@link shutdown}; stops the self-rescheduling loop from scheduling another tick. */
310
+ private schedulerStopped;
260
311
  constructor(opts: ApprovalEngineOptions);
261
312
  on<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
262
313
  off<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
@@ -313,6 +364,37 @@ declare class ApprovalEngine {
313
364
  */
314
365
  getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
315
366
  shutdown(): Promise<void>;
367
+ /**
368
+ * Schedules the next escalation scan via {@link ApprovalEngineOptions.schedulerAdapter}.
369
+ *
370
+ * Called once from the constructor to start the loop, then re-invoked by the
371
+ * scheduled callback itself after each scan completes — a self-rescheduling
372
+ * chain of one-shot {@link ISchedulerAdapter.scheduleAt} calls standing in for
373
+ * the `setInterval` that {@link EscalationScheduler.start} would otherwise
374
+ * use. Each invocation runs the exact same {@link EscalationScheduler.tick}
375
+ * scan the built-in poller runs; only the timer mechanism differs.
376
+ *
377
+ * A no-op once {@link shutdown} has set {@link schedulerStopped} — this is
378
+ * what stops the chain from rescheduling itself forever after teardown.
379
+ */
380
+ private scheduleNextEscalationTick;
381
+ /**
382
+ * Compute {@link CycleTimeStats} for every "completed" instance (see
383
+ * {@link CYCLE_TIME_STATUSES}) matching `filter`. Adapter-agnostic: fetches
384
+ * full instances (not just counts) via {@link fetchAllByFilter} so the
385
+ * actual `createdAt`/`updatedAt` timestamps are available, then reuses the
386
+ * shared {@link computeTimingStats} quantile routine from the metrics
387
+ * plugin rather than a second implementation.
388
+ */
389
+ private computeCycleTimeStats;
390
+ /**
391
+ * Page through every instance matching `filter` via the adapter's
392
+ * `getInstancesByFilter`, accumulating pages until the adapter reports no
393
+ * more results. Needed because adapters may impose a default page size
394
+ * (e.g. `PostgresAdapter` defaults to 50) when no explicit `limit` is given,
395
+ * so a single unbounded call cannot be relied on to return everything.
396
+ */
397
+ private fetchAllByFilter;
316
398
  private escalateInternal;
317
399
  private expireInstance;
318
400
  private markSlaBreached;
@@ -333,4 +415,4 @@ declare class ApprovalEngine {
333
415
  private runExternalAudit;
334
416
  }
335
417
 
336
- export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type ISchedulerAdapter as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type IdGeneratorFn as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, weekendCalendar as w };
418
+ export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OverrideOptions as k, type PreviewResult as l, type RejectOptions as m, type ResubmitOptions as n, type RetryPolicy as o, defaultIdGenerator as p, weekendCalendar as w };
@@ -0,0 +1,13 @@
1
+ interface ISchedulerAdapter {
2
+ /**
3
+ * Schedule a one-shot callback to run at the given date.
4
+ * Returns an opaque handle that can be passed to cancel().
5
+ */
6
+ scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
7
+ /** Cancel a previously scheduled callback by its handle. */
8
+ cancel(handle: string): Promise<void>;
9
+ /** Gracefully shut down the scheduler and release resources. */
10
+ shutdown(): Promise<void>;
11
+ }
12
+
13
+ export type { ISchedulerAdapter as I };
@@ -0,0 +1,13 @@
1
+ interface ISchedulerAdapter {
2
+ /**
3
+ * Schedule a one-shot callback to run at the given date.
4
+ * Returns an opaque handle that can be passed to cancel().
5
+ */
6
+ scheduleAt(id: string, runAt: Date, callback: () => Promise<void>): Promise<string>;
7
+ /** Cancel a previously scheduled callback by its handle. */
8
+ cancel(handle: string): Promise<void>;
9
+ /** Gracefully shut down the scheduler and release resources. */
10
+ shutdown(): Promise<void>;
11
+ }
12
+
13
+ export type { ISchedulerAdapter as I };
@@ -40,7 +40,7 @@ interface IStorageAdapter {
40
40
  getInstancesByFilter(tenantId: string, filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
41
41
  /** Optional cursor-based pagination — more efficient than offset at scale. */
42
42
  getInstancesByCursor?(tenantId: string, filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
43
- getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]>;
43
+ getOverdueInstances(tenantId: string, asOf: Date, filter?: InstanceFilter): Promise<ApprovalInstance[]>;
44
44
  getIdempotentInstance(tenantId: string, idempotencyKey: string): Promise<ApprovalInstance | null>;
45
45
  appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void>;
46
46
  }
@@ -40,7 +40,7 @@ interface IStorageAdapter {
40
40
  getInstancesByFilter(tenantId: string, filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
41
41
  /** Optional cursor-based pagination — more efficient than offset at scale. */
42
42
  getInstancesByCursor?(tenantId: string, filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
43
- getOverdueInstances(tenantId: string, asOf: Date): Promise<ApprovalInstance[]>;
43
+ getOverdueInstances(tenantId: string, asOf: Date, filter?: InstanceFilter): Promise<ApprovalInstance[]>;
44
44
  getIdempotentInstance(tenantId: string, idempotencyKey: string): Promise<ApprovalInstance | null>;
45
45
  appendAuditEntry(tenantId: string, instanceId: string, entry: AuditEntry): Promise<void>;
46
46
  }
@@ -2,6 +2,7 @@ interface Logger {
2
2
  info(msg: string, context?: Record<string, unknown>): void;
3
3
  warn(msg: string, context?: Record<string, unknown>): void;
4
4
  error(msg: string, err?: unknown, context?: Record<string, unknown>): void;
5
+ fatal(msg: string, err?: unknown, context?: Record<string, unknown>): void;
5
6
  debug(msg: string, context?: Record<string, unknown>): void;
6
7
  }
7
8
  declare const noopLogger: Logger;