hierarchical-approval 1.5.0 → 1.7.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 (61) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +34 -0
  3. package/dist/{ApprovalEngine-SI-IRUMY.d.cts → ApprovalEngine-D16llfJv.d.cts} +46 -8
  4. package/dist/{ApprovalEngine-CyZJDbjP.d.ts → ApprovalEngine-gQdFy2CO.d.ts} +46 -8
  5. package/dist/{IAuditAdapter-wfOcN3Zf.d.ts → IAuditAdapter-DDV4Rf9F.d.ts} +1 -1
  6. package/dist/{IAuditAdapter-Cr0dLoVP.d.cts → IAuditAdapter-UJNdJanq.d.cts} +1 -1
  7. package/dist/{IAuthorizationPolicy-BHybAsq5.d.cts → IAuthorizationPolicy-CFJ-gXKl.d.ts} +2 -2
  8. package/dist/{IAuthorizationPolicy-CdvoZsGC.d.ts → IAuthorizationPolicy-CN6LAaKg.d.cts} +2 -2
  9. package/dist/{IMetricsAdapter-JZVK3QUq.d.cts → IMetricsAdapter-DWq8IFaf.d.cts} +1 -1
  10. package/dist/{IMetricsAdapter-JZVK3QUq.d.ts → IMetricsAdapter-DWq8IFaf.d.ts} +1 -1
  11. package/dist/{INotificationAdapter-Cn-LNPtD.d.cts → INotificationAdapter-Dv_MuYwS.d.ts} +12 -2
  12. package/dist/{INotificationAdapter-Cyo0cvXU.d.ts → INotificationAdapter-Dy2d0JKy.d.cts} +12 -2
  13. package/dist/{IOperationMiddleware-DrJy9UtL.d.cts → IOperationMiddleware-DGG-guxK.d.cts} +1 -1
  14. package/dist/{IOperationMiddleware-rx7XAPrW.d.ts → IOperationMiddleware-DWYXVuJD.d.ts} +1 -1
  15. package/dist/{IStorageAdapter-Dz8jrIAK.d.cts → IStorageAdapter-CRSfnH8U.d.cts} +1 -1
  16. package/dist/{IStorageAdapter-CXRyeUOK.d.ts → IStorageAdapter-DMa5VNNz.d.ts} +1 -1
  17. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  18. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  19. package/dist/adapters/PostgresAdapter.cjs +43 -9
  20. package/dist/adapters/PostgresAdapter.cjs.map +1 -1
  21. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  22. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  23. package/dist/adapters/PostgresAdapter.js +43 -9
  24. package/dist/adapters/PostgresAdapter.js.map +1 -1
  25. package/dist/index.cjs +187 -0
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +9 -9
  28. package/dist/index.d.ts +9 -9
  29. package/dist/index.js +187 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/{instance-SA0BU-Gb.d.cts → instance-BE0uJmg3.d.cts} +27 -2
  32. package/dist/{instance-SA0BU-Gb.d.ts → instance-BE0uJmg3.d.ts} +27 -2
  33. package/dist/nestjs.cjs +187 -0
  34. package/dist/nestjs.cjs.map +1 -1
  35. package/dist/nestjs.d.cts +8 -8
  36. package/dist/nestjs.d.ts +8 -8
  37. package/dist/nestjs.js +187 -0
  38. package/dist/nestjs.js.map +1 -1
  39. package/dist/plugins/audit.d.cts +2 -2
  40. package/dist/plugins/audit.d.ts +2 -2
  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.d.cts +2 -2
  48. package/dist/plugins/notify.d.ts +2 -2
  49. package/dist/plugins/resilience.d.cts +3 -3
  50. package/dist/plugins/resilience.d.ts +3 -3
  51. package/dist/plugins/tracing.d.cts +2 -2
  52. package/dist/plugins/tracing.d.ts +2 -2
  53. package/dist/plugins/webhook.d.cts +2 -2
  54. package/dist/plugins/webhook.d.ts +2 -2
  55. package/dist/testing.cjs +187 -0
  56. package/dist/testing.cjs.map +1 -1
  57. package/dist/testing.d.cts +8 -8
  58. package/dist/testing.d.ts +8 -8
  59. package/dist/testing.js +187 -0
  60. package/dist/testing.js.map +1 -1
  61. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,87 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  _Nothing yet._
9
9
 
10
+ ## [1.7.0] - 2026-09-04
11
+
12
+ ### Added — attachment references
13
+
14
+ - **`addAttachment()` / `removeAttachment()` attach supporting evidence to an
15
+ approval** — a quote PDF, a signed contract, a screenshot of a system of
16
+ record. Approvers had nowhere to put the document their decision rested on, so
17
+ it lived in an email thread the audit trail never saw.
18
+
19
+ ```ts
20
+ await engine.addAttachment(id, {
21
+ actorId: 'buyer-1',
22
+ name: 'quote.pdf',
23
+ uri: 's3://procurement/quotes/q-1.pdf',
24
+ contentType: 'application/pdf',
25
+ sizeBytes: 48_120,
26
+ });
27
+ ```
28
+
29
+ - **References only, never bytes.** Approval documents belong in the object
30
+ store or DMS the organisation already runs, which handles retention, virus
31
+ scanning and access control far better than an approval table could; copying
32
+ them here would make the audit database the largest and least governed copy of
33
+ them. Removing detaches the reference and never deletes from the underlying
34
+ store.
35
+
36
+ - **The audit trail keeps what was removed** — name and URI are recorded on the
37
+ `attachment_removed` entry, so the record still shows an approver saw evidence
38
+ that is no longer listed. Dropping that would let the trail imply a decision
39
+ was made on less than it was.
40
+
41
+ - Persisted across all four PostgreSQL sites (schema, `migrate()`,
42
+ insert, update, and read-back), with `addedAt` revived as a `Date`. An
43
+ instance with no attachments reads back as `undefined` rather than `[]`,
44
+ keeping the round-trip shape identical to `MemoryAdapter`.
45
+
46
+ New exports: `Attachment`, `AttachmentEvent`, `AddAttachmentOptions`,
47
+ `RemoveAttachmentOptions`. `ApprovalInstance` gains `attachments`.
48
+
49
+ ## [1.6.0] - 2026-09-04
50
+
51
+ ### Fixed — PostgresAdapter silently dropped most instance updates
52
+
53
+ **Anyone running the PostgreSQL adapter should upgrade and run `migrate()`.**
54
+ `MemoryAdapter` stores whole objects and was always correct, so this reproduced
55
+ only against a real database — which is exactly where it mattered.
56
+
57
+ `updateInstance` wrote just six columns (`status`, `current_level`, `version`,
58
+ `levels`, `sla_breached_at`, `updated_at`). Every other field an operation
59
+ mutates was computed, logged, emitted as an event, and then thrown away on
60
+ write:
61
+
62
+ - **`updateData()` (0.9.0) did not change the stored document.** The engine
63
+ recomputed the chain and emitted `approval:data_updated`, but `data` was never
64
+ written, so the next read returned the old values — and any later
65
+ re-evaluation ran against them.
66
+ - **`requestInfo()` (1.5.0) lost the hold entirely.** `info_request` had no
67
+ column at all, so a held instance came back unheld: `provideInfo()` then threw
68
+ "No clarification request is open", and the scheduler escalated and expired an
69
+ approval that was supposed to be paused.
70
+ - **`provideInfo()`'s deadline give-back never landed** — `expires_at` and
71
+ `sla_deadline_at` were not written, so the time an instance spent on hold was
72
+ silently forfeited.
73
+ - `metadata`, `deadline_action` and `template_snapshot` were likewise never
74
+ updated after insert.
75
+
76
+ `updateInstance` now writes every mutable column, `saveInstance` and
77
+ `rowToInstance` carry `info_request` (reviving `askedAt` as a `Date`), and
78
+ `migrate()` adds the column to existing deployments via
79
+ `ADD COLUMN IF NOT EXISTS`. Optimistic concurrency is unchanged — the update
80
+ still guards on `version`.
81
+
82
+ Regression tests assert the generated SQL and parameters for each field, so a
83
+ future column cannot be added to the type and forgotten in the writer.
84
+
85
+ ### Note on the roadmap
86
+
87
+ Attachment references were the planned 1.6.0. They are deferred: shipping a new
88
+ feature ahead of a data-loss fix in an already-published release would have been
89
+ the wrong order.
90
+
10
91
  ## [1.5.0] - 2026-09-04
11
92
 
12
93
  ### Added — request for information
package/README.md CHANGED
@@ -399,6 +399,40 @@ engine.registerConditionOperator('between', (actual, expected) => {
399
399
  });
400
400
  ```
401
401
 
402
+ ### Attachments
403
+
404
+ Attach supporting evidence — a quote PDF, a signed contract, a screenshot of a
405
+ system of record:
406
+
407
+ ```ts
408
+ const withQuote = await engine.addAttachment(instance.id, {
409
+ actorId: 'buyer-1',
410
+ name: 'quote.pdf',
411
+ uri: 's3://procurement/quotes/q-1.pdf', // a reference, not bytes
412
+ contentType: 'application/pdf',
413
+ sizeBytes: 48_120,
414
+ });
415
+
416
+ await engine.removeAttachment(instance.id, {
417
+ actorId: 'buyer-1',
418
+ attachmentId: withQuote.attachments[0].id,
419
+ reason: 'superseded by revised quote',
420
+ });
421
+ ```
422
+
423
+ **The engine stores references, never bytes.** Approval documents belong in the
424
+ object store or DMS you already run, which handles retention, virus scanning and
425
+ access control far better than an approval table could — copying them here would
426
+ make the audit database the largest and least governed copy of them.
427
+
428
+ Removing detaches the reference; it never deletes from the underlying store,
429
+ which is the DMS's call. The audit entry keeps the name and URI of what was
430
+ removed, so the trail still shows an approver saw evidence that is no longer
431
+ listed.
432
+
433
+ Emits `approval:attachment_added` / `approval:attachment_removed`. Allowed on
434
+ any non-terminal instance, subject to your authorization policy.
435
+
402
436
  ### Asking the submitter a question
403
437
 
404
438
  Approvers routinely need one fact before they can decide. `requestInfo()` puts
@@ -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-Dz8jrIAK.cjs';
2
- import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-SA0BU-Gb.cjs';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Cn-LNPtD.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-CRSfnH8U.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';
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-DrJy9UtL.cjs';
8
- import { I as IAuditAdapter } from './IAuditAdapter-Cr0dLoVP.cjs';
9
- import { I as IMetricsAdapter } from './IMetricsAdapter-JZVK3QUq.cjs';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DGG-guxK.cjs';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-UJNdJanq.cjs';
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-BHybAsq5.cjs';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CN6LAaKg.cjs';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -78,6 +78,18 @@ declare const ProvideInfoOptionsSchema: z.ZodObject<{
78
78
  respondedBy: z.ZodString;
79
79
  response: z.ZodString;
80
80
  }, z.core.$strip>;
81
+ declare const AddAttachmentOptionsSchema: z.ZodObject<{
82
+ actorId: z.ZodString;
83
+ name: z.ZodString;
84
+ uri: z.ZodString;
85
+ contentType: z.ZodOptional<z.ZodString>;
86
+ sizeBytes: z.ZodOptional<z.ZodNumber>;
87
+ }, z.core.$strip>;
88
+ declare const RemoveAttachmentOptionsSchema: z.ZodObject<{
89
+ actorId: z.ZodString;
90
+ attachmentId: z.ZodString;
91
+ reason: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>;
81
93
  declare const UpdateDataOptionsSchema: z.ZodObject<{
82
94
  updatedBy: z.ZodString;
83
95
  data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -101,6 +113,8 @@ type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
101
113
  type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
102
114
  type RequestInfoOptions = z.infer<typeof RequestInfoOptionsSchema>;
103
115
  type ProvideInfoOptions = z.infer<typeof ProvideInfoOptionsSchema>;
116
+ type AddAttachmentOptions = z.infer<typeof AddAttachmentOptionsSchema>;
117
+ type RemoveAttachmentOptions = z.infer<typeof RemoveAttachmentOptionsSchema>;
104
118
 
105
119
  /**
106
120
  * Computes deadline dates from a number of days. The default engine behaviour
@@ -132,7 +146,7 @@ interface WeekendCalendarOptions {
132
146
  */
133
147
  declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
134
148
 
135
- type IdGeneratorPrefix = 'inst' | 'tpl';
149
+ type IdGeneratorPrefix = 'inst' | 'tpl' | 'att';
136
150
  type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
137
151
  declare const defaultIdGenerator: IdGeneratorFn;
138
152
 
@@ -490,6 +504,30 @@ declare class ApprovalEngine {
490
504
  * by being held.
491
505
  */
492
506
  private extendDeadlinesBy;
507
+ /**
508
+ * Attach supporting evidence to an approval — a quote PDF, a signed contract,
509
+ * a screenshot of a system of record.
510
+ *
511
+ * Stores a **reference**, never bytes. Approval documents belong in the object
512
+ * store or DMS the organisation already runs, which handles retention, virus
513
+ * scanning and access control far better than an approval table could;
514
+ * copying them here would make the audit database the largest and least
515
+ * governed copy of them.
516
+ *
517
+ * Allowed on any non-terminal instance, by anyone the authorization policy
518
+ * permits: the submitter adding a missing quote and an approver adding the
519
+ * note that justifies their decision are both ordinary.
520
+ */
521
+ addAttachment(instanceId: string, raw: AddAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
522
+ /**
523
+ * Detach a reference from an approval.
524
+ *
525
+ * The audit entry keeps the name and URI of what was removed, so the record
526
+ * still shows an approver saw evidence that is no longer listed — dropping
527
+ * that would let the trail imply a decision was made on less than it was.
528
+ * Nothing is deleted from the underlying store; that is the DMS's call.
529
+ */
530
+ removeAttachment(instanceId: string, raw: RemoveAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
493
531
  /** Add a comment to an instance without approving or rejecting. */
494
532
  addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
495
533
  /** Resubmit a rejected instance, creating a new linked instance from level 1. */
@@ -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-CXRyeUOK.js';
2
- import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-SA0BU-Gb.js';
3
- import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-Cyo0cvXU.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-DMa5VNNz.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';
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-rx7XAPrW.js';
8
- import { I as IAuditAdapter } from './IAuditAdapter-wfOcN3Zf.js';
9
- import { I as IMetricsAdapter } from './IMetricsAdapter-JZVK3QUq.js';
7
+ import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-DWYXVuJD.js';
8
+ import { I as IAuditAdapter } from './IAuditAdapter-DDV4Rf9F.js';
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-CdvoZsGC.js';
11
+ import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-CFJ-gXKl.js';
12
12
 
13
13
  declare const SubmitOptionsSchema: z.ZodObject<{
14
14
  templateName: z.ZodString;
@@ -78,6 +78,18 @@ declare const ProvideInfoOptionsSchema: z.ZodObject<{
78
78
  respondedBy: z.ZodString;
79
79
  response: z.ZodString;
80
80
  }, z.core.$strip>;
81
+ declare const AddAttachmentOptionsSchema: z.ZodObject<{
82
+ actorId: z.ZodString;
83
+ name: z.ZodString;
84
+ uri: z.ZodString;
85
+ contentType: z.ZodOptional<z.ZodString>;
86
+ sizeBytes: z.ZodOptional<z.ZodNumber>;
87
+ }, z.core.$strip>;
88
+ declare const RemoveAttachmentOptionsSchema: z.ZodObject<{
89
+ actorId: z.ZodString;
90
+ attachmentId: z.ZodString;
91
+ reason: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>;
81
93
  declare const UpdateDataOptionsSchema: z.ZodObject<{
82
94
  updatedBy: z.ZodString;
83
95
  data: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -101,6 +113,8 @@ type OverrideOptions = z.infer<typeof OverrideOptionsSchema>;
101
113
  type UpdateDataOptions = z.infer<typeof UpdateDataOptionsSchema>;
102
114
  type RequestInfoOptions = z.infer<typeof RequestInfoOptionsSchema>;
103
115
  type ProvideInfoOptions = z.infer<typeof ProvideInfoOptionsSchema>;
116
+ type AddAttachmentOptions = z.infer<typeof AddAttachmentOptionsSchema>;
117
+ type RemoveAttachmentOptions = z.infer<typeof RemoveAttachmentOptionsSchema>;
104
118
 
105
119
  /**
106
120
  * Computes deadline dates from a number of days. The default engine behaviour
@@ -132,7 +146,7 @@ interface WeekendCalendarOptions {
132
146
  */
133
147
  declare function weekendCalendar(options?: WeekendCalendarOptions): BusinessCalendar;
134
148
 
135
- type IdGeneratorPrefix = 'inst' | 'tpl';
149
+ type IdGeneratorPrefix = 'inst' | 'tpl' | 'att';
136
150
  type IdGeneratorFn = (prefix: IdGeneratorPrefix) => string;
137
151
  declare const defaultIdGenerator: IdGeneratorFn;
138
152
 
@@ -490,6 +504,30 @@ declare class ApprovalEngine {
490
504
  * by being held.
491
505
  */
492
506
  private extendDeadlinesBy;
507
+ /**
508
+ * Attach supporting evidence to an approval — a quote PDF, a signed contract,
509
+ * a screenshot of a system of record.
510
+ *
511
+ * Stores a **reference**, never bytes. Approval documents belong in the object
512
+ * store or DMS the organisation already runs, which handles retention, virus
513
+ * scanning and access control far better than an approval table could;
514
+ * copying them here would make the audit database the largest and least
515
+ * governed copy of them.
516
+ *
517
+ * Allowed on any non-terminal instance, by anyone the authorization policy
518
+ * permits: the submitter adding a missing quote and an approver adding the
519
+ * note that justifies their decision are both ordinary.
520
+ */
521
+ addAttachment(instanceId: string, raw: AddAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
522
+ /**
523
+ * Detach a reference from an approval.
524
+ *
525
+ * The audit entry keeps the name and URI of what was removed, so the record
526
+ * still shows an approver saw evidence that is no longer listed — dropping
527
+ * that would let the trail imply a decision was made on less than it was.
528
+ * Nothing is deleted from the underlying store; that is the DMS's call.
529
+ */
530
+ removeAttachment(instanceId: string, raw: RemoveAttachmentOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
493
531
  /** Add a comment to an instance without approving or rejecting. */
494
532
  addComment(instanceId: string, raw: AddCommentOptions, auditCtx?: AuditContext): Promise<void>;
495
533
  /** Resubmit a rejected instance, creating a new linked instance from level 1. */
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-SA0BU-Gb.js';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BE0uJmg3.js';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { b as AuditEntry, a as ApprovalInstance } from './instance-SA0BU-Gb.cjs';
1
+ import { b as AuditEntry, a as ApprovalInstance } from './instance-BE0uJmg3.cjs';
2
2
 
3
3
  interface IAuditAdapter {
4
4
  /**
@@ -1,7 +1,7 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-SA0BU-Gb.cjs';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BE0uJmg3.js';
2
2
 
3
3
  interface AuthorizationContext {
4
- operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo';
4
+ operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
5
5
  actorId: string;
6
6
  instance: Readonly<ApprovalInstance>;
7
7
  level?: Readonly<ApprovalLevelInstance>;
@@ -1,7 +1,7 @@
1
- import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-SA0BU-Gb.js';
1
+ import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BE0uJmg3.cjs';
2
2
 
3
3
  interface AuthorizationContext {
4
- operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo';
4
+ operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData' | 'requestInfo' | 'provideInfo' | 'addAttachment' | 'removeAttachment';
5
5
  actorId: string;
6
6
  instance: Readonly<ApprovalInstance>;
7
7
  level?: Readonly<ApprovalLevelInstance>;
@@ -1,4 +1,4 @@
1
- type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
1
+ type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.attachment_added' | 'approval.attachment_removed' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
2
2
  interface IMetricsAdapter {
3
3
  /** Increment a counter. Synchronous — never awaited. */
4
4
  increment(metric: MetricName, labels?: Record<string, string>): void;
@@ -1,4 +1,4 @@
1
- type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
1
+ type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.info_requested' | 'approval.info_provided' | 'approval.attachment_added' | 'approval.attachment_removed' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
2
2
  interface IMetricsAdapter {
3
3
  /** Increment a counter. Synchronous — never awaited. */
4
4
  increment(metric: MetricName, labels?: Record<string, string>): void;
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-SA0BU-Gb.cjs';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BE0uJmg3.js';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -104,6 +104,14 @@ interface InfoProvidedEvent extends ApprovalEvent {
104
104
  /** The approvers waiting again now that the question is answered. */
105
105
  recipients: string[];
106
106
  }
107
+ /** Emitted when supporting evidence is attached to or removed from an approval. */
108
+ interface AttachmentEvent extends ApprovalEvent {
109
+ actorId: string;
110
+ attachmentId: string;
111
+ name: string;
112
+ uri: string;
113
+ level?: number;
114
+ }
107
115
  interface ApprovalEventMap {
108
116
  'approval:submitted': SubmittedEvent;
109
117
  'approval:approved': ApprovedEvent;
@@ -122,6 +130,8 @@ interface ApprovalEventMap {
122
130
  'approval:reminder': ReminderEvent;
123
131
  'approval:info_requested': InfoRequestedEvent;
124
132
  'approval:info_provided': InfoProvidedEvent;
133
+ 'approval:attachment_added': AttachmentEvent;
134
+ 'approval:attachment_removed': AttachmentEvent;
125
135
  }
126
136
  type ApprovalEventName = keyof ApprovalEventMap;
127
137
  interface HistoryEntry extends AuditEntry {
@@ -145,4 +155,4 @@ interface INotificationAdapter {
145
155
  notify(event: NotificationEvent): Promise<void>;
146
156
  }
147
157
 
148
- 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, DelegatedEvent as d, ExpiredEvent as e, InfoProvidedEvent as f, InfoRequestedEvent as g, RejectedEvent as h, ReminderEvent as i, ResubmittedEvent as j, SubmittedEvent as k };
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 };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance, b as AuditEntry } from './instance-SA0BU-Gb.js';
1
+ import { a as ApprovalInstance, b as AuditEntry } from './instance-BE0uJmg3.cjs';
2
2
 
3
3
  interface ApprovalEvent {
4
4
  instanceId: string;
@@ -104,6 +104,14 @@ interface InfoProvidedEvent extends ApprovalEvent {
104
104
  /** The approvers waiting again now that the question is answered. */
105
105
  recipients: string[];
106
106
  }
107
+ /** Emitted when supporting evidence is attached to or removed from an approval. */
108
+ interface AttachmentEvent extends ApprovalEvent {
109
+ actorId: string;
110
+ attachmentId: string;
111
+ name: string;
112
+ uri: string;
113
+ level?: number;
114
+ }
107
115
  interface ApprovalEventMap {
108
116
  'approval:submitted': SubmittedEvent;
109
117
  'approval:approved': ApprovedEvent;
@@ -122,6 +130,8 @@ interface ApprovalEventMap {
122
130
  'approval:reminder': ReminderEvent;
123
131
  'approval:info_requested': InfoRequestedEvent;
124
132
  'approval:info_provided': InfoProvidedEvent;
133
+ 'approval:attachment_added': AttachmentEvent;
134
+ 'approval:attachment_removed': AttachmentEvent;
125
135
  }
126
136
  type ApprovalEventName = keyof ApprovalEventMap;
127
137
  interface HistoryEntry extends AuditEntry {
@@ -145,4 +155,4 @@ interface INotificationAdapter {
145
155
  notify(event: NotificationEvent): Promise<void>;
146
156
  }
147
157
 
148
- 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, DelegatedEvent as d, ExpiredEvent as e, InfoProvidedEvent as f, InfoRequestedEvent as g, RejectedEvent as h, ReminderEvent as i, ResubmittedEvent as j, SubmittedEvent as k };
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 };
@@ -1,4 +1,4 @@
1
- import { a as ApprovalInstance } from './instance-SA0BU-Gb.cjs';
1
+ import { a as ApprovalInstance } from './instance-BE0uJmg3.cjs';
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-SA0BU-Gb.js';
1
+ import { a as ApprovalInstance } from './instance-BE0uJmg3.js';
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-SA0BU-Gb.cjs';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BE0uJmg3.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-SA0BU-Gb.js';
1
+ import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BE0uJmg3.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-Dz8jrIAK.cjs';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-SA0BU-Gb.cjs';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-CRSfnH8U.cjs';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.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-CXRyeUOK.js';
2
- import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-SA0BU-Gb.js';
1
+ import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DMa5VNNz.js';
2
+ import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BE0uJmg3.js';
3
3
 
4
4
  declare class MemoryAdapter implements IStorageAdapter {
5
5
  private templates;
@@ -115,6 +115,8 @@ var PostgresAdapter = class {
115
115
  sla_deadline_at TIMESTAMPTZ,
116
116
  sla_breached_at TIMESTAMPTZ,
117
117
  template_snapshot JSONB,
118
+ info_request JSONB,
119
+ attachments JSONB NOT NULL DEFAULT '[]',
118
120
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
119
121
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
120
122
  PRIMARY KEY (tenant_id, id),
@@ -157,6 +159,8 @@ var PostgresAdapter = class {
157
159
  ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS sla_deadline_at TIMESTAMPTZ;
158
160
  ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS sla_breached_at TIMESTAMPTZ;
159
161
  ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS template_snapshot JSONB;
162
+ ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS info_request JSONB;
163
+ ALTER TABLE IF EXISTS ${this.p}_instances ADD COLUMN IF NOT EXISTS attachments JSONB NOT NULL DEFAULT '[]';
160
164
  `);
161
165
  }
162
166
  /** Enable PostgreSQL Row Level Security for the instances and audit_log tables. */
@@ -216,9 +220,9 @@ var PostgresAdapter = class {
216
220
  status, current_level, version, idempotency_key,
217
221
  data, metadata, levels,
218
222
  parent_instance_id, expires_at, deadline_action,
219
- sla_deadline_at, sla_breached_at, template_snapshot,
223
+ sla_deadline_at, sla_breached_at, template_snapshot, info_request, attachments,
220
224
  created_at, updated_at)
221
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
225
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24)
222
226
  ON CONFLICT (tenant_id, id) DO NOTHING`,
223
227
  [
224
228
  instance.id,
@@ -241,6 +245,8 @@ var PostgresAdapter = class {
241
245
  instance.slaDeadlineAt?.toISOString() ?? null,
242
246
  instance.slaBreachedAt?.toISOString() ?? null,
243
247
  instance.templateSnapshot ? JSON.stringify(instance.templateSnapshot) : null,
248
+ instance.infoRequest ? JSON.stringify(instance.infoRequest) : null,
249
+ JSON.stringify(instance.attachments ?? []),
244
250
  instance.createdAt.toISOString(),
245
251
  instance.updatedAt.toISOString()
246
252
  ]
@@ -249,13 +255,25 @@ var PostgresAdapter = class {
249
255
  async updateInstance(instance, expectedVersion) {
250
256
  const pool = await this.getPool();
251
257
  const result = await pool.query(
258
+ // Every column an engine operation can mutate must be written here.
259
+ // Listing only a subset silently dropped writes: updateData() changed
260
+ // `data`, requestInfo() set `info_request`, and provideInfo() shifted the
261
+ // deadline columns — none of which reached the database.
252
262
  `UPDATE ${this.p}_instances SET
253
- status = $4,
254
- current_level = $5,
255
- version = $6 + 1,
256
- levels = $7,
257
- sla_breached_at = $8,
258
- updated_at = $9
263
+ status = $4,
264
+ current_level = $5,
265
+ version = $6 + 1,
266
+ levels = $7,
267
+ sla_breached_at = $8,
268
+ updated_at = $9,
269
+ data = $10,
270
+ metadata = $11,
271
+ expires_at = $12,
272
+ deadline_action = $13,
273
+ sla_deadline_at = $14,
274
+ template_snapshot = $15,
275
+ info_request = $16,
276
+ attachments = $17
259
277
  WHERE tenant_id = $1 AND id = $2 AND version = $3
260
278
  RETURNING id`,
261
279
  [
@@ -267,7 +285,15 @@ var PostgresAdapter = class {
267
285
  expectedVersion,
268
286
  JSON.stringify(instance.levels),
269
287
  instance.slaBreachedAt?.toISOString() ?? null,
270
- instance.updatedAt.toISOString()
288
+ instance.updatedAt.toISOString(),
289
+ JSON.stringify(instance.data ?? {}),
290
+ JSON.stringify(instance.metadata ?? {}),
291
+ instance.expiresAt?.toISOString() ?? null,
292
+ instance.deadlineAction ?? null,
293
+ instance.slaDeadlineAt?.toISOString() ?? null,
294
+ instance.templateSnapshot ? JSON.stringify(instance.templateSnapshot) : null,
295
+ instance.infoRequest ? JSON.stringify(instance.infoRequest) : null,
296
+ JSON.stringify(instance.attachments ?? [])
271
297
  ]
272
298
  );
273
299
  if (result.rowCount === 0) throw new ApprovalConflictError(instance.id);
@@ -516,6 +542,14 @@ var PostgresAdapter = class {
516
542
  slaDeadlineAt: row["sla_deadline_at"] ? new Date(row["sla_deadline_at"]) : void 0,
517
543
  slaBreachedAt: row["sla_breached_at"] ? new Date(row["sla_breached_at"]) : void 0,
518
544
  templateSnapshot: row["template_snapshot"] ?? void 0,
545
+ // Absent attachments map to undefined, not [], so a Postgres round trip
546
+ // returns the same shape MemoryAdapter does for an instance that never
547
+ // had any — the parity the round-trip test pins down.
548
+ attachments: (row["attachments"] ?? []).length ? row["attachments"].map((a) => ({ ...a, addedAt: new Date(a.addedAt) })) : void 0,
549
+ infoRequest: row["info_request"] ? {
550
+ ...row["info_request"],
551
+ askedAt: new Date(row["info_request"].askedAt)
552
+ } : void 0,
519
553
  createdAt: new Date(row["created_at"]),
520
554
  updatedAt: new Date(row["updated_at"])
521
555
  };