hierarchical-approval 2.0.0 → 2.2.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 (49) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +60 -0
  3. package/dist/{ApprovalEngine-B8bPZq01.d.ts → ApprovalEngine-B3ChZxS7.d.ts} +109 -8
  4. package/dist/{ApprovalEngine-C7p9cOA3.d.cts → ApprovalEngine-CbFBAMlO.d.cts} +109 -8
  5. package/dist/{IAuditAdapter-UJNdJanq.d.cts → IAuditAdapter-BHVKMMv6.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-DDV4Rf9F.d.ts → IAuditAdapter-C4OASS6u.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-CFJ-gXKl.d.ts → IAuthorizationPolicy-BA_m2Dg9.d.ts} +1 -1
  8. package/dist/{IAuthorizationPolicy-CN6LAaKg.d.cts → IAuthorizationPolicy-CtLkwQ8I.d.cts} +1 -1
  9. package/dist/{INotificationAdapter-Dv_MuYwS.d.ts → INotificationAdapter-BdgfUrIn.d.ts} +12 -2
  10. package/dist/{INotificationAdapter-Dy2d0JKy.d.cts → INotificationAdapter-JJAiMyMd.d.cts} +12 -2
  11. package/dist/{IOperationMiddleware-DWYXVuJD.d.ts → IOperationMiddleware-BGYrAqep.d.ts} +1 -1
  12. package/dist/{IOperationMiddleware-DGG-guxK.d.cts → IOperationMiddleware-D0ksiFM3.d.cts} +1 -1
  13. package/dist/{IStorageAdapter-Bgd6p8XL.d.cts → IStorageAdapter-Bk7ybd3z.d.cts} +1 -1
  14. package/dist/{IStorageAdapter-B8aRYeGI.d.ts → IStorageAdapter-DjRvHUF0.d.ts} +1 -1
  15. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  16. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  17. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  18. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  19. package/dist/index.cjs +385 -13
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +8 -8
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +385 -14
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-BE0uJmg3.d.cts → instance-DUJY_Axf.d.cts} +41 -3
  26. package/dist/{instance-BE0uJmg3.d.ts → instance-DUJY_Axf.d.ts} +41 -3
  27. package/dist/nestjs.cjs +384 -13
  28. package/dist/nestjs.cjs.map +1 -1
  29. package/dist/nestjs.d.cts +7 -7
  30. package/dist/nestjs.d.ts +7 -7
  31. package/dist/nestjs.js +384 -13
  32. package/dist/nestjs.js.map +1 -1
  33. package/dist/plugins/audit.d.cts +2 -2
  34. package/dist/plugins/audit.d.ts +2 -2
  35. package/dist/plugins/notify.d.cts +2 -2
  36. package/dist/plugins/notify.d.ts +2 -2
  37. package/dist/plugins/resilience.d.cts +3 -3
  38. package/dist/plugins/resilience.d.ts +3 -3
  39. package/dist/plugins/tracing.d.cts +2 -2
  40. package/dist/plugins/tracing.d.ts +2 -2
  41. package/dist/plugins/webhook.d.cts +2 -2
  42. package/dist/plugins/webhook.d.ts +2 -2
  43. package/dist/testing.cjs +384 -13
  44. package/dist/testing.cjs.map +1 -1
  45. package/dist/testing.d.cts +7 -7
  46. package/dist/testing.d.ts +7 -7
  47. package/dist/testing.js +384 -13
  48. package/dist/testing.js.map +1 -1
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,84 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [2.2.0] - 2026-09-04
11
+
12
+ ### Added — template export / import
13
+
14
+ - **`exportTemplates()` and `importTemplates()` move approval configuration
15
+ between environments.** Templates are authored in a sandbox, reviewed, then
16
+ promoted — but the only way to carry them across was to read `listTemplates()`
17
+ and re-post the rows, which dragged each environment's own `id`, `tenantId`
18
+ and version lineage along. Those either collided on arrival or silently
19
+ claimed a history the target never had.
20
+
21
+ ```ts
22
+ const bundle = await sandbox.exportTemplates(['PO', 'INV']);
23
+ await production.importTemplates(bundle, { mode: 'upsert', dryRun: true });
24
+ ```
25
+
26
+ A bundle is plain JSON, version-stamped, and carries no environment-specific
27
+ fields — they are stripped, not blanked, so a round trip cannot reintroduce a
28
+ stale id. The target assigns its own identity.
29
+
30
+ - **`mode: 'create'`** (default) skips templates that already exist;
31
+ **`'upsert'`** updates them, bumping the version and recording
32
+ `previousVersionId` exactly as `updateTemplate()` does. `dryRun` reports
33
+ without writing.
34
+
35
+ - **Every template is validated before any is written.** A half-applied bundle
36
+ is worse than one rejected outright: the tenant ends up matching neither
37
+ environment and the operator cannot tell which half landed. Validation
38
+ failures reject the whole bundle and name the offending template; storage
39
+ errors during the write phase are still reported per template, since those can
40
+ occur after validation passes.
41
+
42
+ Import also rejects an unsupported `bundleVersion`, an empty bundle, and
43
+ duplicate names within one bundle.
44
+
45
+ New exports: `TemplateBundle`, `ImportResult`, `TEMPLATE_BUNDLE_VERSION`.
46
+
47
+ ## [2.1.0] - 2026-09-04
48
+
49
+ ### Added — sub-workflows
50
+
51
+ - **A level can delegate to a whole separate approval.** "A capital request over
52
+ 1M needs its own board approval before this purchase order can proceed" could
53
+ only be modelled by flattening the board's chain into the purchase order's —
54
+ duplicating it in every template that needed it, and losing the board approval
55
+ as a thing with its own identity, audit trail and lifecycle.
56
+
57
+ ```ts
58
+ { level: 2, name: 'Board approval', mode: 'any', approvers: [],
59
+ subWorkflow: { templateName: 'BOARD' } }
60
+ ```
61
+
62
+ When the level opens, a child instance is submitted against the named
63
+ template, carrying the parent's document data so the child's own conditions
64
+ see the same document. The parent level stays open, with no approvers of its
65
+ own, until the child finishes.
66
+
67
+ - **An approved child advances the parent; any other terminal outcome rejects
68
+ it.** Rejected, cancelled and expired all collapse into one rejection
69
+ deliberately: a parent that treated a cancelled child as "carry on" would
70
+ advance past a gate nobody cleared.
71
+
72
+ - **Children are spawned outside the parent's optimistic write.** The child's own
73
+ `submit()` reads and writes; nesting that inside the parent's compare-and-set
74
+ would turn a slow child template into spurious version conflicts on the
75
+ decision the user just made. The same applies in reverse when a child returns
76
+ its outcome.
77
+
78
+ - **Nesting is capped at five levels**, and `validateTemplate()` rejects a
79
+ template that would spawn itself, sets both `approvers` and `subWorkflow`
80
+ (whose approvers would never be asked), or names no child template. A
81
+ sub-workflow level is exempt from the "must have at least one approver" rule,
82
+ since its child decides it.
83
+
84
+ New exports: `SubWorkflowConfig`, `SubWorkflowEvent`. `ApprovalInstance` gains
85
+ `parentLevel` and `subWorkflowDepth`; levels gain `childInstanceId` and
86
+ `subWorkflowTemplate`.
87
+
10
88
  ## [2.0.0] - 2026-09-04
11
89
 
12
90
  ### BREAKING — `IStorageAdapter` requires `countInstances`
package/README.md CHANGED
@@ -468,6 +468,66 @@ The sweep is **not atomic**: it reports per-instance failures rather than rollin
468
468
  back. A partial transfer is the useful outcome — the approvals that can move
469
469
  should move, and the ones that cannot are named so a human can look at them.
470
470
 
471
+ ### Promoting templates between environments
472
+
473
+ Approval configuration is authored somewhere safe, reviewed, then promoted.
474
+ `exportTemplates()` produces a portable bundle and `importTemplates()` applies
475
+ it:
476
+
477
+ ```ts
478
+ const bundle = await sandbox.exportTemplates(['PO', 'INV']); // omit names for all
479
+ const result = await production.importTemplates(bundle, { mode: 'upsert', dryRun: true });
480
+ // { created: [], updated: ['PO', 'INV'], skipped: [], errors: [], dryRun: true }
481
+ ```
482
+
483
+ A bundle is plain JSON and carries **no** `id`, `tenantId`, `createdAt`,
484
+ `version` or `previousVersionId` — those describe one row in one database, and
485
+ importing them would either collide with the target's ids or claim a lineage the
486
+ target never had. The target assigns its own.
487
+
488
+ `mode: 'create'` (the default) skips templates that already exist; `'upsert'`
489
+ updates them, bumping the version and recording `previousVersionId` as a normal
490
+ `updateTemplate()` would.
491
+
492
+ **Every template is validated before any is written.** A half-applied bundle
493
+ leaves the tenant matching neither environment with no way to tell which half
494
+ landed, so a bundle that fails validation is rejected whole, naming the
495
+ offending template.
496
+
497
+ ### Sub-workflows
498
+
499
+ A level can delegate to a whole separate approval instead of to a list of
500
+ approvers — "a capital request over 1M needs its own board approval before this
501
+ purchase order can proceed" — without flattening the board's chain into the
502
+ purchase order's:
503
+
504
+ ```ts
505
+ levels: [
506
+ { level: 1, name: 'Manager', approvers: [{ type: 'user', userId: 'mgr' }], mode: 'any' },
507
+ { level: 2, name: 'Board approval', mode: 'any', approvers: [],
508
+ subWorkflow: { templateName: 'BOARD' } },
509
+ { level: 3, name: 'CEO', approvers: [{ type: 'user', userId: 'ceo' }], mode: 'any' },
510
+ ]
511
+ ```
512
+
513
+ When level 2 opens, a child instance is submitted against `BOARD`, carrying the
514
+ parent's document data so the child's own conditions see the same document. The
515
+ parent level stays open, with no approvers of its own, until the child finishes.
516
+
517
+ | Child outcome | Parent |
518
+ |---|---|
519
+ | `approved` | Level approved; the chain advances |
520
+ | `rejected`, `cancelled`, `expired` | Parent rejected |
521
+
522
+ Collapsing the non-approved outcomes into one rejection is deliberate: a parent
523
+ that treated a cancelled child as "carry on" would advance past a gate nobody
524
+ cleared.
525
+
526
+ Children link back via `parentInstanceId` and `parentLevel`, and the level
527
+ records `childInstanceId`. Nesting is allowed up to five levels deep, and
528
+ `validateTemplate()` rejects a template that would spawn itself. Emits
529
+ `approval:subworkflow_started` / `approval:subworkflow_completed`.
530
+
471
531
  ### Attachments
472
532
 
473
533
  Attach supporting evidence — a quote PDF, a signed contract, a screenshot of a
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-B8aRYeGI.js';
2
- import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BE0uJmg3.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Dv_MuYwS.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-DjRvHUF0.js';
2
+ import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-DUJY_Axf.js';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BdgfUrIn.js';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.js';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.js';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DWYXVuJD.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-DDV4Rf9F.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BGYrAqep.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-C4OASS6u.js';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.js';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CFJ-gXKl.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BA_m2Dg9.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -305,6 +305,32 @@ interface ApproverWorkload {
305
305
  /** Age of that oldest item. `0` when they hold nothing. */
306
306
  oldestAgeMs: number;
307
307
  }
308
+ /** Version stamp on an exported bundle, so an importer can reject a shape it does not understand. */
309
+ declare const TEMPLATE_BUNDLE_VERSION = 1;
310
+ /**
311
+ * A portable set of templates, safe to move between environments.
312
+ *
313
+ * Deliberately carries no `id`, `tenantId`, `createdAt`, `version` or
314
+ * `previousVersionId`: those describe one row in one database, and importing
315
+ * them would either collide with the target's own ids or silently claim a
316
+ * lineage the target never had.
317
+ */
318
+ interface TemplateBundle {
319
+ bundleVersion: number;
320
+ exportedAt: Date;
321
+ templates: ApprovalTemplateConfig[];
322
+ }
323
+ /** Outcome of {@link ApprovalEngine.importTemplates}. */
324
+ interface ImportResult {
325
+ created: string[];
326
+ updated: string[];
327
+ skipped: string[];
328
+ errors: Array<{
329
+ name: string;
330
+ message: string;
331
+ }>;
332
+ dryRun: boolean;
333
+ }
308
334
  interface ApprovalStatistics {
309
335
  /** Total instances matching the filter (across all statuses). */
310
336
  total: number;
@@ -470,7 +496,18 @@ declare class ApprovalEngine {
470
496
  updateTemplate(config: ApprovalTemplateConfig): Promise<string>;
471
497
  getTemplate(name: string): Promise<ApprovalTemplate>;
472
498
  listTemplates(): Promise<ApprovalTemplate[]>;
473
- submit(raw: SubmitOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
499
+ submit(raw: SubmitOptions, auditCtx?: AuditContext,
500
+ /**
501
+ * @internal Set only when the engine spawns a sub-workflow child. Passed at
502
+ * creation rather than stamped afterwards, because the child spawns its own
503
+ * children before any post-submit update could reach it — which is how a
504
+ * grandchild ended up recorded at depth 1.
505
+ */
506
+ link?: {
507
+ parentInstanceId: string;
508
+ parentLevel: number;
509
+ depth: number;
510
+ }): Promise<ApprovalInstance>;
474
511
  approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
475
512
  reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
476
513
  delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
@@ -649,6 +686,36 @@ declare class ApprovalEngine {
649
686
  * @returns One row per approver holding at least one open level.
650
687
  */
651
688
  getWorkload(filter?: Omit<InstanceFilter, 'status'>): Promise<ApproverWorkload[]>;
689
+ /**
690
+ * Export templates as a portable bundle.
691
+ *
692
+ * Approval configuration is written once and then has to travel — authored in
693
+ * a sandbox, reviewed, promoted to production. Reading `listTemplates()` and
694
+ * re-posting the rows carried each environment's own `id`, `tenantId` and
695
+ * version lineage with it, which either collided on arrival or silently
696
+ * claimed a history the target never had. This strips all of it.
697
+ *
698
+ * @param names - Templates to include; omit for all of them.
699
+ */
700
+ exportTemplates(names?: string[]): Promise<TemplateBundle>;
701
+ /**
702
+ * Import a bundle produced by {@link exportTemplates}.
703
+ *
704
+ * **Every template is validated before any is written.** A bundle that is
705
+ * half-applied is worse than one rejected outright: the tenant is left in a
706
+ * state matching neither environment, and the operator has no way to tell
707
+ * which half landed. Per-template failures during the write phase are still
708
+ * reported individually, since a storage error can occur after validation
709
+ * passes.
710
+ *
711
+ * @param bundle - The bundle to apply.
712
+ * @param opts - `mode: 'create'` (default) refuses to touch existing
713
+ * templates; `'upsert'` updates them. `dryRun` reports without writing.
714
+ */
715
+ importTemplates(bundle: TemplateBundle, opts?: {
716
+ mode?: 'create' | 'upsert';
717
+ dryRun?: boolean;
718
+ }): Promise<ImportResult>;
652
719
  getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
653
720
  shutdown(): Promise<void>;
654
721
  /**
@@ -736,6 +803,40 @@ declare class ApprovalEngine {
736
803
  * half-decided quorum level stops pestering the approvers who did their part.
737
804
  */
738
805
  private sendReminder;
806
+ /**
807
+ * Start child approvals for any open level that delegates to a sub-workflow.
808
+ *
809
+ * Run after the parent has been persisted, never inside the same optimistic
810
+ * write: the child's own `submit()` performs its own reads and writes, and
811
+ * nesting them under the parent's compare-and-set would make a slow child
812
+ * template a source of spurious version conflicts on the parent.
813
+ */
814
+ private startSubWorkflows;
815
+ /**
816
+ * Return a finished child's outcome to the parent level that is waiting on it.
817
+ *
818
+ * An approved child approves its parent level and lets the chain advance; any
819
+ * other terminal outcome — rejected, cancelled, expired — rejects the parent,
820
+ * because the approval the parent was waiting for did not happen. Collapsing
821
+ * those into one rejection is deliberate: a parent that treated a cancelled
822
+ * child as "carry on" would advance past a gate nobody cleared.
823
+ */
824
+ private propagateToParent;
825
+ /** Mark a sub-workflow level approved and advance the parent chain. */
826
+ private completeSubWorkflowLevel;
827
+ /** Reject a parent because the child approval it was waiting on did not succeed. */
828
+ private rejectFromSubWorkflow;
829
+ /**
830
+ * Work that must happen after a decision is durably recorded, not inside it.
831
+ *
832
+ * Both branches touch other instances — a newly opened level may spawn a
833
+ * child approval, and a finished instance may be a child that owes its
834
+ * outcome to a parent. Doing either inside the deciding instance's
835
+ * compare-and-set would nest writes under a version guard that knows nothing
836
+ * about them, so a slow child template would surface as a spurious conflict
837
+ * on the decision the user just made.
838
+ */
839
+ private afterDecision;
739
840
  private findNextLevel;
740
841
  private findPreviousLevel;
741
842
  private guardBulkSize;
@@ -746,4 +847,4 @@ declare class ApprovalEngine {
746
847
  private runExternalAudit;
747
848
  }
748
849
 
749
- 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 TransferResult as T, type UpdateDataOptions as U, 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 ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type OutOfOfficeProvider as l, type OverrideOptions as m, type PreviewResult as n, type ProvideInfoOptions as o, type RejectOptions as p, type RequestInfoOptions as q, type ResubmitOptions as r, type RetryPolicy as s, defaultIdGenerator as t, toComparableNumber as u, validateConditionExpression as v, weekendCalendar as w };
850
+ 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, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, 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 ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type ImportResult as l, type OutOfOfficeProvider as m, type OverrideOptions as n, type PreviewResult as o, type ProvideInfoOptions as p, type RejectOptions as q, type RequestInfoOptions as r, type ResubmitOptions as s, type RetryPolicy as t, type TemplateBundle as u, type TransferResult as v, defaultIdGenerator as w, toComparableNumber as x, validateConditionExpression as y, weekendCalendar as z };
@@ -1,14 +1,14 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-Bgd6p8XL.cjs';
2
- import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BE0uJmg3.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Dy2d0JKy.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-Bk7ybd3z.cjs';
2
+ import { l as ConditionExpression, p as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, k as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-DUJY_Axf.cjs';
3
+ import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-JJAiMyMd.cjs';
4
4
  import { z } from 'zod';
5
5
  import { L as Logger } from './Logger-BplhlU7l.cjs';
6
6
  import { C as Clock } from './Clock-3FnOczFJ.cjs';
7
- import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DGG-guxK.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-UJNdJanq.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-D0ksiFM3.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-BHVKMMv6.cjs';
9
9
  import { I as IMetricsAdapter } from './IMetricsAdapter-DWq8IFaf.cjs';
10
10
  import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
11
- import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CN6LAaKg.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CtLkwQ8I.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -305,6 +305,32 @@ interface ApproverWorkload {
305
305
  /** Age of that oldest item. `0` when they hold nothing. */
306
306
  oldestAgeMs: number;
307
307
  }
308
+ /** Version stamp on an exported bundle, so an importer can reject a shape it does not understand. */
309
+ declare const TEMPLATE_BUNDLE_VERSION = 1;
310
+ /**
311
+ * A portable set of templates, safe to move between environments.
312
+ *
313
+ * Deliberately carries no `id`, `tenantId`, `createdAt`, `version` or
314
+ * `previousVersionId`: those describe one row in one database, and importing
315
+ * them would either collide with the target's own ids or silently claim a
316
+ * lineage the target never had.
317
+ */
318
+ interface TemplateBundle {
319
+ bundleVersion: number;
320
+ exportedAt: Date;
321
+ templates: ApprovalTemplateConfig[];
322
+ }
323
+ /** Outcome of {@link ApprovalEngine.importTemplates}. */
324
+ interface ImportResult {
325
+ created: string[];
326
+ updated: string[];
327
+ skipped: string[];
328
+ errors: Array<{
329
+ name: string;
330
+ message: string;
331
+ }>;
332
+ dryRun: boolean;
333
+ }
308
334
  interface ApprovalStatistics {
309
335
  /** Total instances matching the filter (across all statuses). */
310
336
  total: number;
@@ -470,7 +496,18 @@ declare class ApprovalEngine {
470
496
  updateTemplate(config: ApprovalTemplateConfig): Promise<string>;
471
497
  getTemplate(name: string): Promise<ApprovalTemplate>;
472
498
  listTemplates(): Promise<ApprovalTemplate[]>;
473
- submit(raw: SubmitOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
499
+ submit(raw: SubmitOptions, auditCtx?: AuditContext,
500
+ /**
501
+ * @internal Set only when the engine spawns a sub-workflow child. Passed at
502
+ * creation rather than stamped afterwards, because the child spawns its own
503
+ * children before any post-submit update could reach it — which is how a
504
+ * grandchild ended up recorded at depth 1.
505
+ */
506
+ link?: {
507
+ parentInstanceId: string;
508
+ parentLevel: number;
509
+ depth: number;
510
+ }): Promise<ApprovalInstance>;
474
511
  approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
475
512
  reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
476
513
  delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
@@ -649,6 +686,36 @@ declare class ApprovalEngine {
649
686
  * @returns One row per approver holding at least one open level.
650
687
  */
651
688
  getWorkload(filter?: Omit<InstanceFilter, 'status'>): Promise<ApproverWorkload[]>;
689
+ /**
690
+ * Export templates as a portable bundle.
691
+ *
692
+ * Approval configuration is written once and then has to travel — authored in
693
+ * a sandbox, reviewed, promoted to production. Reading `listTemplates()` and
694
+ * re-posting the rows carried each environment's own `id`, `tenantId` and
695
+ * version lineage with it, which either collided on arrival or silently
696
+ * claimed a history the target never had. This strips all of it.
697
+ *
698
+ * @param names - Templates to include; omit for all of them.
699
+ */
700
+ exportTemplates(names?: string[]): Promise<TemplateBundle>;
701
+ /**
702
+ * Import a bundle produced by {@link exportTemplates}.
703
+ *
704
+ * **Every template is validated before any is written.** A bundle that is
705
+ * half-applied is worse than one rejected outright: the tenant is left in a
706
+ * state matching neither environment, and the operator has no way to tell
707
+ * which half landed. Per-template failures during the write phase are still
708
+ * reported individually, since a storage error can occur after validation
709
+ * passes.
710
+ *
711
+ * @param bundle - The bundle to apply.
712
+ * @param opts - `mode: 'create'` (default) refuses to touch existing
713
+ * templates; `'upsert'` updates them. `dryRun` reports without writing.
714
+ */
715
+ importTemplates(bundle: TemplateBundle, opts?: {
716
+ mode?: 'create' | 'upsert';
717
+ dryRun?: boolean;
718
+ }): Promise<ImportResult>;
652
719
  getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
653
720
  shutdown(): Promise<void>;
654
721
  /**
@@ -736,6 +803,40 @@ declare class ApprovalEngine {
736
803
  * half-decided quorum level stops pestering the approvers who did their part.
737
804
  */
738
805
  private sendReminder;
806
+ /**
807
+ * Start child approvals for any open level that delegates to a sub-workflow.
808
+ *
809
+ * Run after the parent has been persisted, never inside the same optimistic
810
+ * write: the child's own `submit()` performs its own reads and writes, and
811
+ * nesting them under the parent's compare-and-set would make a slow child
812
+ * template a source of spurious version conflicts on the parent.
813
+ */
814
+ private startSubWorkflows;
815
+ /**
816
+ * Return a finished child's outcome to the parent level that is waiting on it.
817
+ *
818
+ * An approved child approves its parent level and lets the chain advance; any
819
+ * other terminal outcome — rejected, cancelled, expired — rejects the parent,
820
+ * because the approval the parent was waiting for did not happen. Collapsing
821
+ * those into one rejection is deliberate: a parent that treated a cancelled
822
+ * child as "carry on" would advance past a gate nobody cleared.
823
+ */
824
+ private propagateToParent;
825
+ /** Mark a sub-workflow level approved and advance the parent chain. */
826
+ private completeSubWorkflowLevel;
827
+ /** Reject a parent because the child approval it was waiting on did not succeed. */
828
+ private rejectFromSubWorkflow;
829
+ /**
830
+ * Work that must happen after a decision is durably recorded, not inside it.
831
+ *
832
+ * Both branches touch other instances — a newly opened level may spawn a
833
+ * child approval, and a finished instance may be a child that owes its
834
+ * outcome to a parent. Doing either inside the deciding instance's
835
+ * compare-and-set would nest writes under a version guard that knows nothing
836
+ * about them, so a slow child template would surface as a spurious conflict
837
+ * on the decision the user just made.
838
+ */
839
+ private afterDecision;
739
840
  private findNextLevel;
740
841
  private findPreviousLevel;
741
842
  private guardBulkSize;
@@ -746,4 +847,4 @@ declare class ApprovalEngine {
746
847
  private runExternalAudit;
747
848
  }
748
849
 
749
- 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 TransferResult as T, type UpdateDataOptions as U, 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 ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type OutOfOfficeProvider as l, type OverrideOptions as m, type PreviewResult as n, type ProvideInfoOptions as o, type RejectOptions as p, type RequestInfoOptions as q, type ResubmitOptions as r, type RetryPolicy as s, defaultIdGenerator as t, toComparableNumber as u, validateConditionExpression as v, weekendCalendar as w };
850
+ 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, TEMPLATE_BUNDLE_VERSION as T, type UpdateDataOptions as U, 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 ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type ImportResult as l, type OutOfOfficeProvider as m, type OverrideOptions as n, type PreviewResult as o, type ProvideInfoOptions as p, type RejectOptions as q, type RequestInfoOptions as r, type ResubmitOptions as s, type RetryPolicy as t, type TemplateBundle as u, type TransferResult as v, defaultIdGenerator as w, toComparableNumber as x, validateConditionExpression as y, weekendCalendar as z };
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-BE0uJmg3.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-DUJY_Axf.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-BE0uJmg3.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-DUJY_Axf.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BE0uJmg3.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DUJY_Axf.js';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BE0uJmg3.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DUJY_Axf.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
4
  operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-BE0uJmg3.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-DUJY_Axf.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -112,6 +112,14 @@ interface AttachmentEvent extends ApprovalEvent {
112
112
  uri: string;
113
113
  level?: number;
114
114
  }
115
+ /** Emitted when a level hands off to a child approval, and when that child returns. */
116
+ interface SubWorkflowEvent extends ApprovalEvent {
117
+ level: number;
118
+ childInstanceId: string;
119
+ childTemplateName: string;
120
+ /** Set on completion: the outcome the child returned. */
121
+ outcome?: 'approved' | 'rejected' | 'cancelled' | 'expired';
122
+ }
115
123
  interface ApprovalEventMap {
116
124
  'approval:submitted': SubmittedEvent;
117
125
  'approval:approved': ApprovedEvent;
@@ -132,6 +140,8 @@ interface ApprovalEventMap {
132
140
  'approval:info_provided': InfoProvidedEvent;
133
141
  'approval:attachment_added': AttachmentEvent;
134
142
  'approval:attachment_removed': AttachmentEvent;
143
+ 'approval:subworkflow_started': SubWorkflowEvent;
144
+ 'approval:subworkflow_completed': SubWorkflowEvent;
135
145
  }
136
146
  type ApprovalEventName = keyof ApprovalEventMap;
137
147
  interface HistoryEntry extends AuditEntry {
@@ -155,4 +165,4 @@ interface INotificationAdapter {
155
165
  notify(event: NotificationEvent): Promise<void>;
156
166
  }
157
167
 
158
- export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, AttachmentEvent as d, DelegatedEvent as e, ExpiredEvent as f, InfoProvidedEvent as g, InfoRequestedEvent as h, RejectedEvent as i, ReminderEvent as j, ResubmittedEvent as k, SubmittedEvent as l };
168
+ export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, AttachmentEvent as d, DelegatedEvent as e, ExpiredEvent as f, InfoProvidedEvent as g, InfoRequestedEvent as h, RejectedEvent as i, ReminderEvent as j, ResubmittedEvent as k, SubWorkflowEvent as l, SubmittedEvent as m };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-BE0uJmg3.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-DUJY_Axf.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -112,6 +112,14 @@ interface AttachmentEvent extends ApprovalEvent {
112
112
  uri: string;
113
113
  level?: number;
114
114
  }
115
+ /** Emitted when a level hands off to a child approval, and when that child returns. */
116
+ interface SubWorkflowEvent extends ApprovalEvent {
117
+ level: number;
118
+ childInstanceId: string;
119
+ childTemplateName: string;
120
+ /** Set on completion: the outcome the child returned. */
121
+ outcome?: 'approved' | 'rejected' | 'cancelled' | 'expired';
122
+ }
115
123
  interface ApprovalEventMap {
116
124
  'approval:submitted': SubmittedEvent;
117
125
  'approval:approved': ApprovedEvent;
@@ -132,6 +140,8 @@ interface ApprovalEventMap {
132
140
  'approval:info_provided': InfoProvidedEvent;
133
141
  'approval:attachment_added': AttachmentEvent;
134
142
  'approval:attachment_removed': AttachmentEvent;
143
+ 'approval:subworkflow_started': SubWorkflowEvent;
144
+ 'approval:subworkflow_completed': SubWorkflowEvent;
135
145
  }
136
146
  type ApprovalEventName = keyof ApprovalEventMap;
137
147
  interface HistoryEntry extends AuditEntry {
@@ -155,4 +165,4 @@ interface INotificationAdapter {
155
165
  notify(event: NotificationEvent): Promise<void>;
156
166
  }
157
167
 
158
- export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, AttachmentEvent as d, DelegatedEvent as e, ExpiredEvent as f, InfoProvidedEvent as g, InfoRequestedEvent as h, RejectedEvent as i, ReminderEvent as j, ResubmittedEvent as k, SubmittedEvent as l };
168
+ export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, AttachmentEvent as d, DelegatedEvent as e, ExpiredEvent as f, InfoProvidedEvent as g, InfoRequestedEvent as h, RejectedEvent as i, ReminderEvent as j, ResubmittedEvent as k, SubWorkflowEvent as l, SubmittedEvent as m };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-BE0uJmg3.js';
1
+ import { a as ApprovalInstance } from './instance-DUJY_Axf.js';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-BE0uJmg3.cjs';
1
+ import { a as ApprovalInstance } from './instance-DUJY_Axf.cjs';
2
2
 
3
3
  declare class ApprovalError extends Error {
4
4
  readonly code: string;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BE0uJmg3.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-DUJY_Axf.cjs';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,4 +1,4 @@
1
- import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BE0uJmg3.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-DUJY_Axf.js';
2
2
 
3
3
  interface PaginationOpts {
4
4
  limit: number;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Bgd6p8XL.cjs';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Bk7ybd3z.cjs';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.cjs';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,5 +1,5 @@
1
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-B8aRYeGI.js';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DjRvHUF0.js';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.js';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Bgd6p8XL.cjs';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.cjs';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-Bk7ybd3z.cjs';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.cjs';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;
@@ -1,7 +1,7 @@
1
1
  import * as tls from 'tls';
2
2
  import * as pg from 'pg';
3
- import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-B8aRYeGI.js';
4
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.js';
3
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DjRvHUF0.js';
4
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DUJY_Axf.js';
5
5
 
6
6
  interface PostgresAdapterOptions {
7
7
  connectionString?: string;