@objectstack/plugin-approvals 7.3.0 → 7.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
6
9
  var __export = (target, all) => {
7
10
  for (var name in all)
8
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,215 +20,941 @@ var __copyProps = (to, from, except, desc) => {
17
20
  };
18
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
22
 
23
+ // src/translations/en.objects.generated.ts
24
+ var enObjects;
25
+ var init_en_objects_generated = __esm({
26
+ "src/translations/en.objects.generated.ts"() {
27
+ "use strict";
28
+ enObjects = {
29
+ sys_approval_request: {
30
+ label: "Approval Request",
31
+ pluralLabel: "Approval Requests",
32
+ description: "Live approval instance tracked per submission",
33
+ fields: {
34
+ id: {
35
+ label: "Request ID"
36
+ },
37
+ organization_id: {
38
+ label: "Organization",
39
+ help: "Tenant that owns this approval request (propagated from submitter context)"
40
+ },
41
+ process_name: {
42
+ label: "Source",
43
+ help: "Origin of the request \u2014 `flow:<flowName|nodeId>` for node-driven approvals"
44
+ },
45
+ object_name: {
46
+ label: "Object"
47
+ },
48
+ record_id: {
49
+ label: "Record ID"
50
+ },
51
+ submitter_id: {
52
+ label: "Submitter"
53
+ },
54
+ submitter_comment: {
55
+ label: "Submitter Comment"
56
+ },
57
+ status: {
58
+ label: "Status",
59
+ help: "Lifecycle state of the request",
60
+ options: {
61
+ pending: "pending",
62
+ approved: "approved",
63
+ rejected: "rejected",
64
+ recalled: "recalled"
65
+ }
66
+ },
67
+ current_step: {
68
+ label: "Current Step",
69
+ help: "Machine name of the step awaiting approval"
70
+ },
71
+ current_step_index: {
72
+ label: "Current Step Index"
73
+ },
74
+ pending_approvers: {
75
+ label: "Pending Approvers",
76
+ help: "Comma-separated user ids who can act on the current step"
77
+ },
78
+ payload_json: {
79
+ label: "Snapshot",
80
+ help: "Record snapshot at submission time"
81
+ },
82
+ flow_run_id: {
83
+ label: "Flow Run",
84
+ help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
85
+ },
86
+ flow_node_id: {
87
+ label: "Flow Node",
88
+ help: "Approval node id within the flow that opened this request (ADR-0019)."
89
+ },
90
+ node_config_json: {
91
+ label: "Node Config",
92
+ help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
93
+ },
94
+ completed_at: {
95
+ label: "Completed At"
96
+ },
97
+ created_at: {
98
+ label: "Created At"
99
+ },
100
+ updated_at: {
101
+ label: "Updated At"
102
+ }
103
+ },
104
+ _views: {
105
+ my_pending: {
106
+ label: "My Pending"
107
+ },
108
+ submitted_by_me: {
109
+ label: "I Submitted"
110
+ },
111
+ completed: {
112
+ label: "Completed"
113
+ },
114
+ all_requests: {
115
+ label: "All"
116
+ }
117
+ }
118
+ },
119
+ sys_approval_action: {
120
+ label: "Approval Action",
121
+ pluralLabel: "Approval Actions",
122
+ description: "Append-only audit trail for approval actions",
123
+ fields: {
124
+ id: {
125
+ label: "Action ID"
126
+ },
127
+ organization_id: {
128
+ label: "Organization",
129
+ help: "Tenant that owns this action (mirrors the parent request)"
130
+ },
131
+ request_id: {
132
+ label: "Request"
133
+ },
134
+ step_name: {
135
+ label: "Step",
136
+ help: "Machine name of the step at the time of the action"
137
+ },
138
+ step_index: {
139
+ label: "Step Index"
140
+ },
141
+ action: {
142
+ label: "Action",
143
+ options: {
144
+ submit: "submit",
145
+ approve: "approve",
146
+ reject: "reject",
147
+ recall: "recall",
148
+ escalate: "escalate"
149
+ }
150
+ },
151
+ actor_id: {
152
+ label: "Actor"
153
+ },
154
+ comment: {
155
+ label: "Comment"
156
+ },
157
+ created_at: {
158
+ label: "Created At"
159
+ }
160
+ },
161
+ _views: {
162
+ recent: {
163
+ label: "Recent"
164
+ },
165
+ by_actor: {
166
+ label: "By Actor"
167
+ },
168
+ all_actions: {
169
+ label: "All"
170
+ }
171
+ }
172
+ }
173
+ };
174
+ }
175
+ });
176
+
177
+ // src/translations/zh-CN.objects.generated.ts
178
+ var zhCNObjects;
179
+ var init_zh_CN_objects_generated = __esm({
180
+ "src/translations/zh-CN.objects.generated.ts"() {
181
+ "use strict";
182
+ zhCNObjects = {
183
+ sys_approval_request: {
184
+ label: "\u5BA1\u6279\u8BF7\u6C42",
185
+ pluralLabel: "\u5BA1\u6279\u8BF7\u6C42",
186
+ description: "\u6309\u63D0\u4EA4\u8BB0\u5F55\u8DDF\u8E2A\u7684\u5B9E\u65F6\u5BA1\u6279\u5B9E\u4F8B",
187
+ fields: {
188
+ id: {
189
+ label: "\u8BF7\u6C42 ID"
190
+ },
191
+ organization_id: {
192
+ label: "\u7EC4\u7EC7",
193
+ help: "\u62E5\u6709\u8BE5\u5BA1\u6279\u8BF7\u6C42\u7684\u79DF\u6237\uFF08\u4ECE\u63D0\u4EA4\u65B9\u4E0A\u4E0B\u6587\u4F20\u64AD\uFF09"
194
+ },
195
+ process_name: {
196
+ label: "\u6765\u6E90",
197
+ help: "\u8BF7\u6C42\u6765\u6E90 \u2014\u2014 \u8282\u70B9\u9A71\u52A8\u7684\u5BA1\u6279\u4E3A `flow:<flowName|nodeId>`"
198
+ },
199
+ object_name: {
200
+ label: "\u5BF9\u8C61"
201
+ },
202
+ record_id: {
203
+ label: "\u8BB0\u5F55 ID"
204
+ },
205
+ submitter_id: {
206
+ label: "\u63D0\u4EA4\u4EBA"
207
+ },
208
+ submitter_comment: {
209
+ label: "\u63D0\u4EA4\u5907\u6CE8"
210
+ },
211
+ status: {
212
+ label: "\u72B6\u6001",
213
+ help: "\u8BF7\u6C42\u7684\u751F\u547D\u5468\u671F\u72B6\u6001",
214
+ options: {
215
+ pending: "\u5F85\u5904\u7406",
216
+ approved: "\u5DF2\u6279\u51C6",
217
+ rejected: "\u5DF2\u62D2\u7EDD",
218
+ recalled: "\u5DF2\u64A4\u56DE"
219
+ }
220
+ },
221
+ current_step: {
222
+ label: "\u5F53\u524D\u6B65\u9AA4",
223
+ help: "\u5F53\u524D\u7B49\u5F85\u5BA1\u6279\u7684\u6B65\u9AA4\u673A\u5668\u540D\u79F0"
224
+ },
225
+ current_step_index: {
226
+ label: "\u5F53\u524D\u6B65\u9AA4\u7D22\u5F15"
227
+ },
228
+ pending_approvers: {
229
+ label: "\u5F85\u5BA1\u6279\u4EBA",
230
+ help: "\u53EF\u5728\u5F53\u524D\u6B65\u9AA4\u6267\u884C\u64CD\u4F5C\u7684\u7528\u6237 ID\uFF0C\u9017\u53F7\u5206\u9694"
231
+ },
232
+ payload_json: {
233
+ label: "\u5FEB\u7167",
234
+ help: "\u63D0\u4EA4\u65F6\u7684\u8BB0\u5F55\u5FEB\u7167"
235
+ },
236
+ flow_run_id: {
237
+ label: "Flow Run",
238
+ help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
239
+ },
240
+ flow_node_id: {
241
+ label: "Flow Node",
242
+ help: "Approval node id within the flow that opened this request (ADR-0019)."
243
+ },
244
+ node_config_json: {
245
+ label: "Node Config",
246
+ help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
247
+ },
248
+ completed_at: {
249
+ label: "\u5B8C\u6210\u65F6\u95F4"
250
+ },
251
+ created_at: {
252
+ label: "\u521B\u5EFA\u65F6\u95F4"
253
+ },
254
+ updated_at: {
255
+ label: "\u66F4\u65B0\u65F6\u95F4"
256
+ }
257
+ },
258
+ _views: {
259
+ my_pending: {
260
+ label: "\u6211\u7684\u5F85\u529E"
261
+ },
262
+ submitted_by_me: {
263
+ label: "\u6211\u63D0\u4EA4\u7684"
264
+ },
265
+ completed: {
266
+ label: "\u5DF2\u5B8C\u6210"
267
+ },
268
+ all_requests: {
269
+ label: "\u5168\u90E8"
270
+ }
271
+ }
272
+ },
273
+ sys_approval_action: {
274
+ label: "\u5BA1\u6279\u52A8\u4F5C",
275
+ pluralLabel: "\u5BA1\u6279\u52A8\u4F5C",
276
+ description: "\u8FFD\u52A0\u5199\u5165\u7684\u5BA1\u6279\u64CD\u4F5C\u5BA1\u8BA1\u8BB0\u5F55",
277
+ fields: {
278
+ id: {
279
+ label: "\u52A8\u4F5C ID"
280
+ },
281
+ organization_id: {
282
+ label: "\u7EC4\u7EC7",
283
+ help: "\u62E5\u6709\u8BE5\u52A8\u4F5C\u7684\u79DF\u6237\uFF08\u4E0E\u7236\u8BF7\u6C42\u4FDD\u6301\u4E00\u81F4\uFF09"
284
+ },
285
+ request_id: {
286
+ label: "\u8BF7\u6C42"
287
+ },
288
+ step_name: {
289
+ label: "\u6B65\u9AA4",
290
+ help: "\u6267\u884C\u8BE5\u52A8\u4F5C\u65F6\u5BF9\u5E94\u6B65\u9AA4\u7684\u673A\u5668\u540D\u79F0"
291
+ },
292
+ step_index: {
293
+ label: "\u6B65\u9AA4\u7D22\u5F15"
294
+ },
295
+ action: {
296
+ label: "\u64CD\u4F5C",
297
+ options: {
298
+ submit: "\u63D0\u4EA4",
299
+ approve: "\u6279\u51C6",
300
+ reject: "\u62D2\u7EDD",
301
+ recall: "\u64A4\u56DE",
302
+ escalate: "\u5347\u7EA7"
303
+ }
304
+ },
305
+ actor_id: {
306
+ label: "\u6267\u884C\u4EBA"
307
+ },
308
+ comment: {
309
+ label: "\u8BC4\u8BBA"
310
+ },
311
+ created_at: {
312
+ label: "\u521B\u5EFA\u65F6\u95F4"
313
+ }
314
+ },
315
+ _views: {
316
+ recent: {
317
+ label: "\u6700\u8FD1"
318
+ },
319
+ by_actor: {
320
+ label: "\u6309\u6267\u884C\u4EBA"
321
+ },
322
+ all_actions: {
323
+ label: "\u5168\u90E8"
324
+ }
325
+ }
326
+ }
327
+ };
328
+ }
329
+ });
330
+
331
+ // src/translations/ja-JP.objects.generated.ts
332
+ var jaJPObjects;
333
+ var init_ja_JP_objects_generated = __esm({
334
+ "src/translations/ja-JP.objects.generated.ts"() {
335
+ "use strict";
336
+ jaJPObjects = {
337
+ sys_approval_request: {
338
+ label: "\u627F\u8A8D\u30EA\u30AF\u30A8\u30B9\u30C8",
339
+ pluralLabel: "\u627F\u8A8D\u30EA\u30AF\u30A8\u30B9\u30C8",
340
+ description: "\u9001\u4FE1\u3054\u3068\u306B\u8FFD\u8DE1\u3055\u308C\u308B\u30E9\u30A4\u30D6\u627F\u8A8D\u30A4\u30F3\u30B9\u30BF\u30F3\u30B9",
341
+ fields: {
342
+ id: {
343
+ label: "\u30EA\u30AF\u30A8\u30B9\u30C8 ID"
344
+ },
345
+ organization_id: {
346
+ label: "\u7D44\u7E54",
347
+ help: "\u3053\u306E\u627F\u8A8D\u30EA\u30AF\u30A8\u30B9\u30C8\u3092\u6240\u6709\u3059\u308B\u30C6\u30CA\u30F3\u30C8\uFF08\u9001\u4FE1\u8005\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u304B\u3089\u4F1D\u64AD\uFF09"
348
+ },
349
+ process_name: {
350
+ label: "\u30BD\u30FC\u30B9",
351
+ help: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u767A\u751F\u5143 \u2014 \u30CE\u30FC\u30C9\u99C6\u52D5\u306E\u627F\u8A8D\u3067\u306F `flow:<flowName|nodeId>`"
352
+ },
353
+ object_name: {
354
+ label: "\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8"
355
+ },
356
+ record_id: {
357
+ label: "\u30EC\u30B3\u30FC\u30C9 ID"
358
+ },
359
+ submitter_id: {
360
+ label: "\u9001\u4FE1\u8005"
361
+ },
362
+ submitter_comment: {
363
+ label: "\u9001\u4FE1\u8005\u30B3\u30E1\u30F3\u30C8"
364
+ },
365
+ status: {
366
+ label: "\u30B9\u30C6\u30FC\u30BF\u30B9",
367
+ help: "\u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u30E9\u30A4\u30D5\u30B5\u30A4\u30AF\u30EB\u72B6\u614B",
368
+ options: {
369
+ pending: "\u4FDD\u7559\u4E2D",
370
+ approved: "\u627F\u8A8D\u6E08\u307F",
371
+ rejected: "\u5374\u4E0B\u6E08\u307F",
372
+ recalled: "\u53D6\u308A\u6D88\u3057\u6E08\u307F"
373
+ }
374
+ },
375
+ current_step: {
376
+ label: "\u73FE\u5728\u306E\u30B9\u30C6\u30C3\u30D7",
377
+ help: "\u627F\u8A8D\u5F85\u3061\u306E\u30B9\u30C6\u30C3\u30D7\u306E\u6A5F\u68B0\u540D"
378
+ },
379
+ current_step_index: {
380
+ label: "\u73FE\u5728\u306E\u30B9\u30C6\u30C3\u30D7\u756A\u53F7"
381
+ },
382
+ pending_approvers: {
383
+ label: "\u627F\u8A8D\u5F85\u3061\u627F\u8A8D\u8005",
384
+ help: "\u73FE\u5728\u306E\u30B9\u30C6\u30C3\u30D7\u3092\u51E6\u7406\u3067\u304D\u308B\u30E6\u30FC\u30B6\u30FC ID \u306E\u30AB\u30F3\u30DE\u533A\u5207\u308A\u30EA\u30B9\u30C8"
385
+ },
386
+ payload_json: {
387
+ label: "\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8",
388
+ help: "\u9001\u4FE1\u6642\u306E\u30EC\u30B3\u30FC\u30C9\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8"
389
+ },
390
+ flow_run_id: {
391
+ label: "Flow Run",
392
+ help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
393
+ },
394
+ flow_node_id: {
395
+ label: "Flow Node",
396
+ help: "Approval node id within the flow that opened this request (ADR-0019)."
397
+ },
398
+ node_config_json: {
399
+ label: "Node Config",
400
+ help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
401
+ },
402
+ completed_at: {
403
+ label: "\u5B8C\u4E86\u65E5\u6642"
404
+ },
405
+ created_at: {
406
+ label: "\u4F5C\u6210\u65E5\u6642"
407
+ },
408
+ updated_at: {
409
+ label: "\u66F4\u65B0\u65E5\u6642"
410
+ }
411
+ },
412
+ _views: {
413
+ my_pending: {
414
+ label: "\u81EA\u5206\u306E\u4FDD\u7559\u4E2D"
415
+ },
416
+ submitted_by_me: {
417
+ label: "\u81EA\u5206\u304C\u9001\u4FE1"
418
+ },
419
+ completed: {
420
+ label: "\u5B8C\u4E86\u6E08\u307F"
421
+ },
422
+ all_requests: {
423
+ label: "\u3059\u3079\u3066"
424
+ }
425
+ }
426
+ },
427
+ sys_approval_action: {
428
+ label: "\u627F\u8A8D\u30A2\u30AF\u30B7\u30E7\u30F3",
429
+ pluralLabel: "\u627F\u8A8D\u30A2\u30AF\u30B7\u30E7\u30F3",
430
+ description: "\u627F\u8A8D\u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u8FFD\u8A18\u5C02\u7528\u76E3\u67FB\u8A3C\u8DE1",
431
+ fields: {
432
+ id: {
433
+ label: "\u30A2\u30AF\u30B7\u30E7\u30F3 ID"
434
+ },
435
+ organization_id: {
436
+ label: "\u7D44\u7E54",
437
+ help: "\u3053\u306E\u30A2\u30AF\u30B7\u30E7\u30F3\u3092\u6240\u6709\u3059\u308B\u30C6\u30CA\u30F3\u30C8\uFF08\u89AA\u30EA\u30AF\u30A8\u30B9\u30C8\u3068\u540C\u3058\uFF09"
438
+ },
439
+ request_id: {
440
+ label: "\u30EA\u30AF\u30A8\u30B9\u30C8"
441
+ },
442
+ step_name: {
443
+ label: "\u30B9\u30C6\u30C3\u30D7",
444
+ help: "\u30A2\u30AF\u30B7\u30E7\u30F3\u6642\u70B9\u306E\u30B9\u30C6\u30C3\u30D7\u306E\u6A5F\u68B0\u540D"
445
+ },
446
+ step_index: {
447
+ label: "\u30B9\u30C6\u30C3\u30D7\u756A\u53F7"
448
+ },
449
+ action: {
450
+ label: "\u30A2\u30AF\u30B7\u30E7\u30F3",
451
+ options: {
452
+ submit: "\u7533\u8ACB",
453
+ approve: "\u627F\u8A8D",
454
+ reject: "\u5374\u4E0B",
455
+ recall: "\u53D6\u6D88",
456
+ escalate: "\u30A8\u30B9\u30AB\u30EC\u30FC\u30B7\u30E7\u30F3"
457
+ }
458
+ },
459
+ actor_id: {
460
+ label: "\u64CD\u4F5C\u8005"
461
+ },
462
+ comment: {
463
+ label: "\u30B3\u30E1\u30F3\u30C8"
464
+ },
465
+ created_at: {
466
+ label: "\u4F5C\u6210\u65E5\u6642"
467
+ }
468
+ },
469
+ _views: {
470
+ recent: {
471
+ label: "\u6700\u8FD1"
472
+ },
473
+ by_actor: {
474
+ label: "\u64CD\u4F5C\u8005\u5225"
475
+ },
476
+ all_actions: {
477
+ label: "\u3059\u3079\u3066"
478
+ }
479
+ }
480
+ }
481
+ };
482
+ }
483
+ });
484
+
485
+ // src/translations/es-ES.objects.generated.ts
486
+ var esESObjects;
487
+ var init_es_ES_objects_generated = __esm({
488
+ "src/translations/es-ES.objects.generated.ts"() {
489
+ "use strict";
490
+ esESObjects = {
491
+ sys_approval_request: {
492
+ label: "Solicitud de aprobaci\xF3n",
493
+ pluralLabel: "Solicitudes de aprobaci\xF3n",
494
+ description: "Instancia activa de aprobaci\xF3n registrada por env\xEDo",
495
+ fields: {
496
+ id: {
497
+ label: "ID de solicitud"
498
+ },
499
+ organization_id: {
500
+ label: "Organizaci\xF3n",
501
+ help: "Tenant que posee esta solicitud de aprobaci\xF3n (propagado desde el contexto del solicitante)."
502
+ },
503
+ process_name: {
504
+ label: "Origen",
505
+ help: "Origen de la solicitud \u2014 `flow:<flowName|nodeId>` para aprobaciones por nodo"
506
+ },
507
+ object_name: {
508
+ label: "Objeto"
509
+ },
510
+ record_id: {
511
+ label: "ID de registro"
512
+ },
513
+ submitter_id: {
514
+ label: "Solicitante"
515
+ },
516
+ submitter_comment: {
517
+ label: "Comentario del solicitante"
518
+ },
519
+ status: {
520
+ label: "Estado",
521
+ help: "Estado del ciclo de vida de la solicitud.",
522
+ options: {
523
+ pending: "Pendiente",
524
+ approved: "Aprobada",
525
+ rejected: "Rechazada",
526
+ recalled: "Retirada"
527
+ }
528
+ },
529
+ current_step: {
530
+ label: "Paso actual",
531
+ help: "Nombre t\xE9cnico del paso pendiente de aprobaci\xF3n."
532
+ },
533
+ current_step_index: {
534
+ label: "\xCDndice del paso actual"
535
+ },
536
+ pending_approvers: {
537
+ label: "Aprobadores pendientes",
538
+ help: "ID de usuario separados por comas que pueden actuar en el paso actual."
539
+ },
540
+ payload_json: {
541
+ label: "Instant\xE1nea",
542
+ help: "Instant\xE1nea del registro en el momento del env\xEDo."
543
+ },
544
+ flow_run_id: {
545
+ label: "Flow Run",
546
+ help: "Suspended automation run id this request gates (ADR-0019). The decision resumes it."
547
+ },
548
+ flow_node_id: {
549
+ label: "Flow Node",
550
+ help: "Approval node id within the flow that opened this request (ADR-0019)."
551
+ },
552
+ node_config_json: {
553
+ label: "Node Config",
554
+ help: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019)."
555
+ },
556
+ completed_at: {
557
+ label: "Completado el"
558
+ },
559
+ created_at: {
560
+ label: "Creado el"
561
+ },
562
+ updated_at: {
563
+ label: "Actualizado el"
564
+ }
565
+ },
566
+ _views: {
567
+ my_pending: {
568
+ label: "Mis pendientes"
569
+ },
570
+ submitted_by_me: {
571
+ label: "Enviadas por m\xED"
572
+ },
573
+ completed: {
574
+ label: "Completadas"
575
+ },
576
+ all_requests: {
577
+ label: "Todas"
578
+ }
579
+ }
580
+ },
581
+ sys_approval_action: {
582
+ label: "Acci\xF3n de aprobaci\xF3n",
583
+ pluralLabel: "Acciones de aprobaci\xF3n",
584
+ description: "Registro de auditor\xEDa append-only para acciones de aprobaci\xF3n",
585
+ fields: {
586
+ id: {
587
+ label: "ID de acci\xF3n"
588
+ },
589
+ organization_id: {
590
+ label: "Organizaci\xF3n",
591
+ help: "Tenant que posee esta acci\xF3n (refleja la solicitud principal)."
592
+ },
593
+ request_id: {
594
+ label: "Solicitud"
595
+ },
596
+ step_name: {
597
+ label: "Paso",
598
+ help: "Nombre t\xE9cnico del paso en el momento de la acci\xF3n."
599
+ },
600
+ step_index: {
601
+ label: "\xCDndice del paso"
602
+ },
603
+ action: {
604
+ label: "Acci\xF3n",
605
+ options: {
606
+ submit: "Enviar",
607
+ approve: "Aprobar",
608
+ reject: "Rechazar",
609
+ recall: "Retirar",
610
+ escalate: "Escalar"
611
+ }
612
+ },
613
+ actor_id: {
614
+ label: "Actor"
615
+ },
616
+ comment: {
617
+ label: "Comentario"
618
+ },
619
+ created_at: {
620
+ label: "Creado el"
621
+ }
622
+ },
623
+ _views: {
624
+ recent: {
625
+ label: "Recientes"
626
+ },
627
+ by_actor: {
628
+ label: "Por actor"
629
+ },
630
+ all_actions: {
631
+ label: "Todas"
632
+ }
633
+ }
634
+ }
635
+ };
636
+ }
637
+ });
638
+
639
+ // src/translations/index.ts
640
+ var translations_exports = {};
641
+ __export(translations_exports, {
642
+ ApprovalsTranslations: () => ApprovalsTranslations
643
+ });
644
+ var ApprovalsTranslations;
645
+ var init_translations = __esm({
646
+ "src/translations/index.ts"() {
647
+ "use strict";
648
+ init_en_objects_generated();
649
+ init_zh_CN_objects_generated();
650
+ init_ja_JP_objects_generated();
651
+ init_es_ES_objects_generated();
652
+ ApprovalsTranslations = {
653
+ en: { objects: enObjects },
654
+ "zh-CN": { objects: zhCNObjects },
655
+ "ja-JP": { objects: jaJPObjects },
656
+ "es-ES": { objects: esESObjects }
657
+ };
658
+ }
659
+ });
660
+
20
661
  // src/index.ts
21
662
  var index_exports = {};
22
663
  __export(index_exports, {
23
664
  ApprovalService: () => ApprovalService,
24
665
  ApprovalsServicePlugin: () => ApprovalsServicePlugin,
25
- SysApprovalAction: () => import_audit2.SysApprovalAction,
26
- SysApprovalProcess: () => import_audit2.SysApprovalProcess,
27
- SysApprovalRequest: () => import_audit2.SysApprovalRequest
666
+ SysApprovalAction: () => SysApprovalAction,
667
+ SysApprovalRequest: () => SysApprovalRequest,
668
+ registerApprovalNode: () => registerApprovalNode
28
669
  });
29
670
  module.exports = __toCommonJS(index_exports);
30
- var import_audit2 = require("@objectstack/platform-objects/audit");
31
-
32
- // src/approval-service.ts
33
- var import_automation = require("@objectstack/spec/automation");
34
671
 
35
- // src/action-executor.ts
36
- var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
37
- var noopLogger = {
38
- info: () => {
39
- },
40
- warn: () => {
672
+ // src/sys-approval-request.object.ts
673
+ var import_data = require("@objectstack/spec/data");
674
+ var SysApprovalRequest = import_data.ObjectSchema.create({
675
+ name: "sys_approval_request",
676
+ label: "Approval Request",
677
+ pluralLabel: "Approval Requests",
678
+ icon: "inbox",
679
+ isSystem: true,
680
+ managedBy: "system",
681
+ description: "Live approval instance tracked per submission",
682
+ displayNameField: "id",
683
+ titleFormat: "{process_name} \xB7 {record_id}",
684
+ compactLayout: ["process_name", "object_name", "record_id", "status", "current_step", "submitter_id", "updated_at"],
685
+ // Curated built-in list views — render as segmented tabs in the console.
686
+ // Filters use {current_user_id} substitution wired by the console.
687
+ listViews: {
688
+ my_pending: {
689
+ type: "grid",
690
+ name: "my_pending",
691
+ label: "My Pending",
692
+ data: { provider: "object", object: "sys_approval_request" },
693
+ columns: ["process_name", "object_name", "record_id", "current_step", "submitter_id", "updated_at"],
694
+ filter: [
695
+ { field: "status", operator: "equals", value: "pending" },
696
+ { field: "pending_approvers", operator: "contains", value: "{current_user_id}" }
697
+ ],
698
+ sort: [{ field: "updated_at", order: "desc" }],
699
+ pagination: { pageSize: 25 },
700
+ emptyState: { title: "No pending approvals", message: "You're all caught up." }
701
+ },
702
+ submitted_by_me: {
703
+ type: "grid",
704
+ name: "submitted_by_me",
705
+ label: "I Submitted",
706
+ data: { provider: "object", object: "sys_approval_request" },
707
+ columns: ["process_name", "object_name", "record_id", "status", "current_step", "updated_at"],
708
+ filter: [{ field: "submitter_id", operator: "equals", value: "{current_user_id}" }],
709
+ sort: [{ field: "updated_at", order: "desc" }],
710
+ pagination: { pageSize: 25 }
711
+ },
712
+ completed: {
713
+ type: "grid",
714
+ name: "completed",
715
+ label: "Completed",
716
+ data: { provider: "object", object: "sys_approval_request" },
717
+ columns: ["process_name", "object_name", "record_id", "status", "submitter_id", "completed_at"],
718
+ filter: [{ field: "status", operator: "in", value: ["approved", "rejected", "recalled"] }],
719
+ sort: [{ field: "completed_at", order: "desc" }],
720
+ pagination: { pageSize: 25 }
721
+ },
722
+ all_requests: {
723
+ type: "grid",
724
+ name: "all_requests",
725
+ label: "All",
726
+ data: { provider: "object", object: "sys_approval_request" },
727
+ columns: ["process_name", "object_name", "record_id", "status", "current_step", "submitter_id", "updated_at"],
728
+ sort: [{ field: "updated_at", order: "desc" }],
729
+ pagination: { pageSize: 50 }
730
+ }
41
731
  },
42
- error: () => {
732
+ fields: {
733
+ id: import_data.Field.text({ label: "Request ID", required: true, readonly: true, group: "System" }),
734
+ organization_id: import_data.Field.lookup("sys_organization", {
735
+ label: "Organization",
736
+ required: false,
737
+ group: "System",
738
+ description: "Tenant that owns this approval request (propagated from submitter context)"
739
+ }),
740
+ process_name: import_data.Field.text({
741
+ label: "Source",
742
+ required: true,
743
+ maxLength: 100,
744
+ description: "Origin of the request \u2014 `flow:<flowName|nodeId>` for node-driven approvals",
745
+ group: "Target"
746
+ }),
747
+ object_name: import_data.Field.text({
748
+ label: "Object",
749
+ required: true,
750
+ maxLength: 100,
751
+ group: "Target"
752
+ }),
753
+ record_id: import_data.Field.text({
754
+ label: "Record ID",
755
+ required: true,
756
+ maxLength: 100,
757
+ group: "Target"
758
+ }),
759
+ submitter_id: import_data.Field.lookup("sys_user", {
760
+ label: "Submitter",
761
+ required: false,
762
+ group: "Target"
763
+ }),
764
+ submitter_comment: import_data.Field.textarea({
765
+ label: "Submitter Comment",
766
+ required: false,
767
+ group: "Target"
768
+ }),
769
+ status: import_data.Field.select(
770
+ ["pending", "approved", "rejected", "recalled"],
771
+ {
772
+ label: "Status",
773
+ required: true,
774
+ defaultValue: "pending",
775
+ description: "Lifecycle state of the request",
776
+ group: "State"
777
+ }
778
+ ),
779
+ current_step: import_data.Field.text({
780
+ label: "Current Step",
781
+ required: false,
782
+ maxLength: 100,
783
+ description: "Machine name of the step awaiting approval",
784
+ group: "State"
785
+ }),
786
+ current_step_index: import_data.Field.number({
787
+ label: "Current Step Index",
788
+ required: false,
789
+ defaultValue: 0,
790
+ group: "State"
791
+ }),
792
+ pending_approvers: import_data.Field.textarea({
793
+ label: "Pending Approvers",
794
+ required: false,
795
+ description: "Comma-separated user ids who can act on the current step",
796
+ group: "State"
797
+ }),
798
+ payload_json: import_data.Field.textarea({
799
+ label: "Snapshot",
800
+ required: false,
801
+ description: "Record snapshot at submission time",
802
+ group: "State"
803
+ }),
804
+ // ── ADR-0019: approval-as-flow-node correlation ──────────────────
805
+ // When a request is opened by an Approval *node* (rather than a standalone
806
+ // process), these tie it back to the suspended flow run so a decision can
807
+ // resume it. Null for legacy process-driven requests.
808
+ flow_run_id: import_data.Field.text({
809
+ label: "Flow Run",
810
+ required: false,
811
+ maxLength: 100,
812
+ readonly: true,
813
+ description: "Suspended automation run id this request gates (ADR-0019). The decision resumes it.",
814
+ group: "State"
815
+ }),
816
+ flow_node_id: import_data.Field.text({
817
+ label: "Flow Node",
818
+ required: false,
819
+ maxLength: 100,
820
+ readonly: true,
821
+ description: "Approval node id within the flow that opened this request (ADR-0019).",
822
+ group: "State"
823
+ }),
824
+ node_config_json: import_data.Field.textarea({
825
+ label: "Node Config",
826
+ required: false,
827
+ readonly: true,
828
+ description: "Snapshot of the Approval node config (approvers/behavior) for node-driven requests (ADR-0019).",
829
+ group: "State"
830
+ }),
831
+ completed_at: import_data.Field.datetime({
832
+ label: "Completed At",
833
+ required: false,
834
+ group: "State"
835
+ }),
836
+ created_at: import_data.Field.datetime({
837
+ label: "Created At",
838
+ required: true,
839
+ defaultValue: "NOW()",
840
+ readonly: true,
841
+ group: "System"
842
+ }),
843
+ updated_at: import_data.Field.datetime({ label: "Updated At", required: false, group: "System" })
43
844
  },
44
- debug: () => {
45
- }
46
- };
47
- var DEFAULT_WEBHOOK_TIMEOUT_MS = 5e3;
48
- async function executeActions(actions, ctx, opts) {
49
- if (!Array.isArray(actions) || actions.length === 0) return;
50
- const log = { ...noopLogger, ...opts.logger ?? {} };
51
- for (const a of actions) {
52
- try {
53
- await runOne(a, ctx, opts, log);
54
- } catch (err) {
55
- log.error?.(`[approvals] action '${a?.type ?? "<unknown>"}' failed: ${err?.message ?? err}`, {
56
- action: a,
57
- trigger: ctx.trigger,
58
- request_id: ctx.request?.id
59
- });
60
- }
61
- }
62
- }
63
- async function runOne(action, ctx, opts, log) {
64
- if (!action || typeof action !== "object") return;
65
- switch (action.type) {
66
- case "field_update":
67
- return runFieldUpdate(action, ctx, opts, log);
68
- case "inbox_notify":
69
- return runInboxNotify(action, ctx, opts, log);
70
- case "webhook":
71
- return runWebhook(action, ctx, opts, log);
72
- case "email_alert":
73
- case "script":
74
- case "connector_action":
75
- log.warn?.(`[approvals] action type '${action.type}' is not implemented yet \u2014 skipping`, {
76
- action_name: action.name,
77
- trigger: ctx.trigger
78
- });
79
- return;
80
- default:
81
- log.warn?.(`[approvals] unknown action type '${action.type}' \u2014 skipping`);
82
- }
83
- }
84
- async function runFieldUpdate(action, ctx, opts, log) {
85
- const cfg = action.config ?? {};
86
- const field = cfg.field;
87
- if (!field) {
88
- log.warn?.("[approvals] field_update missing config.field");
89
- return;
90
- }
91
- const value = resolveValueToken(cfg.value, ctx);
92
- const object = ctx.process?.object_name ?? ctx.process?.object;
93
- const recordId = ctx.request?.record_id;
94
- if (!object || !recordId) {
95
- log.warn?.("[approvals] field_update missing object/record context");
96
- return;
97
- }
98
- await opts.engine.update(
99
- object,
100
- { id: recordId, [field]: value },
101
- { context: SYSTEM_CTX }
102
- );
103
- log.debug?.(`[approvals] field_update ${object}/${recordId} set ${field}`, { value });
104
- }
105
- function resolveValueToken(raw, ctx) {
106
- if (typeof raw !== "string") return raw;
107
- switch (raw) {
108
- case "$status":
109
- return ctx.request?.status ?? null;
110
- case "$now":
111
- return (/* @__PURE__ */ new Date()).toISOString();
112
- case "$actor":
113
- return ctx.actorId ?? null;
114
- case "$comment":
115
- return ctx.comment ?? null;
116
- case "$step":
117
- return ctx.request?.current_step ?? null;
118
- case "$request_id":
119
- return ctx.request?.id ?? null;
120
- default:
121
- return raw;
122
- }
123
- }
124
- function interpolate(template, ctx) {
125
- if (typeof template !== "string") return template;
126
- return template.replace(/\{record_id\}/g, String(ctx.request?.record_id ?? "")).replace(/\{object\}/g, String(ctx.process?.object_name ?? ctx.process?.object ?? "")).replace(/\{status\}/g, String(ctx.request?.status ?? "")).replace(/\{step\}/g, String(ctx.request?.current_step ?? "")).replace(/\{actor\}/g, String(ctx.actorId ?? "")).replace(/\{comment\}/g, String(ctx.comment ?? "")).replace(/\{process\}/g, String(ctx.process?.name ?? ""));
127
- }
128
- async function runInboxNotify(action, ctx, opts, log) {
129
- const cfg = action.config ?? {};
130
- const recipients = resolveRecipients(cfg.to, ctx);
131
- if (recipients.length === 0) {
132
- log.debug?.("[approvals] inbox_notify resolved no recipients \u2014 skipping");
133
- return;
134
- }
135
- const title = interpolate(cfg.title ?? "Approval update", ctx);
136
- const body = interpolate(cfg.body ?? "", ctx);
137
- const type = String(cfg.notificationType ?? "system");
138
- const rawLink = cfg.link ? interpolate(String(cfg.link), ctx) : `/console/system/approvals?requestId=${encodeURIComponent(ctx.request?.id ?? "")}`;
139
- const url = /^https?:\/\//i.test(rawLink) ? rawLink : null;
140
- const now = (/* @__PURE__ */ new Date()).toISOString();
141
- for (const recipient of recipients) {
142
- try {
143
- await opts.engine.insert(
144
- "sys_notification",
145
- {
146
- id: `notif_${cryptoRandom()}`,
147
- recipient_id: String(recipient),
148
- type,
149
- title,
150
- body,
151
- url,
152
- is_read: false,
153
- source_object: ctx.process?.object_name ?? ctx.process?.object ?? null,
154
- source_id: ctx.request?.record_id ?? null,
155
- created_at: now,
156
- updated_at: now
157
- },
158
- { context: SYSTEM_CTX }
159
- );
160
- } catch (err) {
161
- log.warn?.(`[approvals] inbox_notify insert failed for ${recipient}: ${err?.message ?? err}`);
162
- }
163
- }
164
- }
165
- function resolveRecipients(to, ctx) {
166
- if (Array.isArray(to)) return to.map(String).filter(Boolean);
167
- if (typeof to === "string") {
168
- if (to === "submitter") return ctx.request?.submitter_id ? [String(ctx.request.submitter_id)] : [];
169
- if (to === "pending_approvers") {
170
- const list = ctx.request?.pending_approvers ?? [];
171
- if (Array.isArray(list)) return list.map(String).filter(Boolean);
172
- if (typeof list === "string") return list.split(",").map((s) => s.trim()).filter(Boolean);
173
- return [];
174
- }
175
- return [to];
176
- }
177
- return [];
178
- }
179
- function cryptoRandom() {
180
- const g = globalThis;
181
- if (g.crypto?.randomUUID) return g.crypto.randomUUID();
182
- return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
183
- }
184
- async function runWebhook(action, ctx, opts, log) {
185
- const cfg = action.config ?? {};
186
- const url = cfg.url;
187
- if (!url) {
188
- log.warn?.("[approvals] webhook missing config.url");
189
- return;
190
- }
191
- const fetchImpl = opts.fetch ?? globalThis.fetch;
192
- if (!fetchImpl) {
193
- log.warn?.("[approvals] webhook skipped \u2014 no fetch implementation available");
194
- return;
195
- }
196
- const timeoutMs = opts.webhookTimeoutMs ?? DEFAULT_WEBHOOK_TIMEOUT_MS;
197
- const headers = { "Content-Type": "application/json", ...cfg.headers ?? {} };
198
- const payload = {
199
- trigger: ctx.trigger,
200
- request: ctx.request,
201
- step: ctx.step ? { name: ctx.step.name, index: ctx.request?.current_step_index } : null,
202
- actor_id: ctx.actorId ?? null,
203
- comment: ctx.comment ?? null,
204
- process_name: ctx.process?.name,
205
- object: ctx.process?.object_name ?? ctx.process?.object,
206
- ...cfg.body && typeof cfg.body === "object" ? cfg.body : {}
207
- };
208
- const controller = globalThis.AbortController ? new globalThis.AbortController() : null;
209
- const timer = setTimeout(() => controller?.abort(), timeoutMs);
210
- try {
211
- const res = await fetchImpl(url, {
212
- method: cfg.method ?? "POST",
213
- headers,
214
- body: JSON.stringify(payload),
215
- signal: controller?.signal
216
- });
217
- if (!res.ok) {
218
- log.warn?.(`[approvals] webhook ${url} \u2192 ${res.status} ${res.statusText}`);
845
+ indexes: [
846
+ // Look up "is there a pending request for this record?" — common
847
+ // guard on submit and on edit-while-locked checks.
848
+ { fields: ["object_name", "record_id"] },
849
+ { fields: ["status", "object_name"] },
850
+ // "My approvals" inbox pending_approvers is a CSV string so this
851
+ // index only helps with status pre-filtering; the engine does a
852
+ // post-filter substring match per row.
853
+ { fields: ["status", "updated_at"] },
854
+ { fields: ["submitter_id", "status"] }
855
+ ]
856
+ });
857
+
858
+ // src/sys-approval-action.object.ts
859
+ var import_data2 = require("@objectstack/spec/data");
860
+ var SysApprovalAction = import_data2.ObjectSchema.create({
861
+ name: "sys_approval_action",
862
+ label: "Approval Action",
863
+ pluralLabel: "Approval Actions",
864
+ icon: "check-circle",
865
+ isSystem: true,
866
+ managedBy: "append-only",
867
+ description: "Append-only audit trail for approval actions",
868
+ displayNameField: "id",
869
+ titleFormat: "{action} \xB7 {step_name}",
870
+ compactLayout: ["request_id", "step_name", "action", "actor_id", "created_at"],
871
+ listViews: {
872
+ recent: {
873
+ type: "grid",
874
+ name: "recent",
875
+ label: "Recent",
876
+ data: { provider: "object", object: "sys_approval_action" },
877
+ columns: ["created_at", "request_id", "step_name", "action", "actor_id", "comment"],
878
+ sort: [{ field: "created_at", order: "desc" }],
879
+ pagination: { pageSize: 50 },
880
+ emptyState: { title: "No approval actions yet", message: "Actions are logged automatically when approvals progress." }
881
+ },
882
+ by_actor: {
883
+ type: "grid",
884
+ name: "by_actor",
885
+ label: "By Actor",
886
+ data: { provider: "object", object: "sys_approval_action" },
887
+ columns: ["actor_id", "created_at", "request_id", "step_name", "action"],
888
+ sort: [{ field: "actor_id", order: "asc" }, { field: "created_at", order: "desc" }],
889
+ grouping: { fields: [{ field: "actor_id", order: "asc", collapsed: false }] },
890
+ pagination: { pageSize: 100 }
891
+ },
892
+ all_actions: {
893
+ type: "grid",
894
+ name: "all_actions",
895
+ label: "All",
896
+ data: { provider: "object", object: "sys_approval_action" },
897
+ columns: ["created_at", "request_id", "step_name", "action", "actor_id", "comment"],
898
+ sort: [{ field: "created_at", order: "desc" }],
899
+ pagination: { pageSize: 100 }
219
900
  }
220
- } catch (err) {
221
- log.warn?.(`[approvals] webhook ${url} failed: ${err?.message ?? err}`);
222
- } finally {
223
- clearTimeout(timer);
224
- }
225
- }
901
+ },
902
+ fields: {
903
+ id: import_data2.Field.text({ label: "Action ID", required: true, readonly: true, group: "System" }),
904
+ organization_id: import_data2.Field.lookup("sys_organization", {
905
+ label: "Organization",
906
+ required: false,
907
+ group: "System",
908
+ description: "Tenant that owns this action (mirrors the parent request)"
909
+ }),
910
+ request_id: import_data2.Field.lookup("sys_approval_request", {
911
+ label: "Request",
912
+ required: true,
913
+ group: "Target"
914
+ }),
915
+ step_name: import_data2.Field.text({
916
+ label: "Step",
917
+ required: false,
918
+ maxLength: 100,
919
+ description: "Machine name of the step at the time of the action",
920
+ group: "Target"
921
+ }),
922
+ step_index: import_data2.Field.number({
923
+ label: "Step Index",
924
+ required: false,
925
+ group: "Target"
926
+ }),
927
+ action: import_data2.Field.select(
928
+ ["submit", "approve", "reject", "recall", "escalate"],
929
+ {
930
+ label: "Action",
931
+ required: true,
932
+ group: "Action"
933
+ }
934
+ ),
935
+ actor_id: import_data2.Field.lookup("sys_user", {
936
+ label: "Actor",
937
+ required: false,
938
+ group: "Action"
939
+ }),
940
+ comment: import_data2.Field.textarea({ label: "Comment", required: false, group: "Action" }),
941
+ created_at: import_data2.Field.datetime({
942
+ label: "Created At",
943
+ required: true,
944
+ defaultValue: "NOW()",
945
+ readonly: true,
946
+ group: "System"
947
+ })
948
+ },
949
+ indexes: [
950
+ { fields: ["request_id", "created_at"] },
951
+ { fields: ["request_id", "step_index", "action"] }
952
+ ]
953
+ });
226
954
 
227
955
  // src/approval-service.ts
228
- var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
956
+ var import_automation = require("@objectstack/spec/automation");
957
+ var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
229
958
  function uid(prefix) {
230
959
  const g = globalThis;
231
960
  if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
@@ -247,25 +976,11 @@ function csvSplit(raw) {
247
976
  if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
248
977
  return String(raw).split(",").map((s) => s.trim()).filter(Boolean);
249
978
  }
250
- function rowFromProcess(row) {
251
- return {
252
- id: String(row.id),
253
- name: String(row.name ?? ""),
254
- label: String(row.label ?? ""),
255
- object_name: String(row.object_name ?? ""),
256
- description: row.description ?? void 0,
257
- active: row.active !== false,
258
- definition: parseJson(row.definition_json, {}),
259
- created_at: row.created_at ?? void 0,
260
- updated_at: row.updated_at ?? void 0
261
- };
262
- }
263
979
  function rowFromRequest(row) {
264
980
  return {
265
981
  id: String(row.id),
266
982
  organization_id: row.organization_id ?? void 0,
267
983
  process_name: String(row.process_name ?? ""),
268
- process_hash: row.process_hash ?? void 0,
269
984
  object_name: String(row.object_name ?? ""),
270
985
  record_id: String(row.record_id ?? ""),
271
986
  submitter_id: row.submitter_id ?? void 0,
@@ -275,6 +990,8 @@ function rowFromRequest(row) {
275
990
  current_step_index: row.current_step_index ?? void 0,
276
991
  pending_approvers: csvSplit(row.pending_approvers),
277
992
  payload: parseJson(row.payload_json, void 0),
993
+ flow_run_id: row.flow_run_id ?? void 0,
994
+ flow_node_id: row.flow_node_id ?? void 0,
278
995
  completed_at: row.completed_at ?? void 0,
279
996
  created_at: row.created_at ?? void 0,
280
997
  updated_at: row.updated_at ?? void 0
@@ -297,23 +1014,20 @@ var ApprovalService = class {
297
1014
  this.engine = opts.engine;
298
1015
  this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
299
1016
  this.logger = opts.logger;
300
- this.fetchImpl = opts.fetch;
301
- this.webhookTimeoutMs = opts.webhookTimeoutMs;
302
- this.onRegistryChange = opts.onRegistryChange;
303
- this.metadataRepo = opts.metadataRepo;
1017
+ this.automation = opts.automation;
304
1018
  }
305
- /** Allow the plugin to attach a hook re-binding callback after construction. */
306
- setRegistryChangeHandler(handler) {
307
- this.onRegistryChange = handler;
1019
+ /** Attach (or replace) the automation surface used to resume flow runs. */
1020
+ attachAutomation(automation) {
1021
+ this.automation = automation;
308
1022
  }
309
1023
  /**
310
- * Expand the approvers on a step into user IDs by querying the graph
311
- * tables for `team:` / `department:` / `role:` / `manager:` approver
312
- * types. Falls back to a prefixed literal (`type:value`) when graph
313
- * lookups produce nothing — so existing test fixtures and approver
314
- * flows that rely on substring matching keep working.
1024
+ * Expand the approvers on an Approval node into user IDs by querying the
1025
+ * graph tables for `team:` / `department:` / `role:` / `manager:` approver
1026
+ * types. Falls back to a prefixed literal (`type:value`) when graph lookups
1027
+ * produce nothing — so existing fixtures and flows that rely on substring
1028
+ * matching keep working.
315
1029
  *
316
- * **Graph semantics (M10.17.1):**
1030
+ * **Graph semantics:**
317
1031
  * - `team` → flat members of `sys_team` (better-auth; no BFS)
318
1032
  * - `department` → recursive BFS of `sys_department.parent_department_id`
319
1033
  * → members of every descendant via `sys_department_member`
@@ -379,7 +1093,7 @@ var ApprovalService = class {
379
1093
  filter: { team_id: teamId },
380
1094
  fields: ["user_id"],
381
1095
  limit: 1e4,
382
- context: SYSTEM_CTX2
1096
+ context: SYSTEM_CTX
383
1097
  });
384
1098
  } catch {
385
1099
  rows = [];
@@ -394,7 +1108,7 @@ var ApprovalService = class {
394
1108
  filter: organizationId ? { id: departmentId, organization_id: organizationId } : { id: departmentId },
395
1109
  fields: ["id", "active"],
396
1110
  limit: 1,
397
- context: SYSTEM_CTX2
1111
+ context: SYSTEM_CTX
398
1112
  });
399
1113
  const seedRow = Array.isArray(seed) ? seed[0] : null;
400
1114
  if (!seedRow || seedRow.active === false) return [];
@@ -409,7 +1123,7 @@ var ApprovalService = class {
409
1123
  try {
410
1124
  const filter = { parent_department_id: parent, active: { $ne: false } };
411
1125
  if (organizationId) filter.organization_id = organizationId;
412
- kids = await this.engine.find("sys_department", { filter, fields: ["id"], limit: 1e3, context: SYSTEM_CTX2 });
1126
+ kids = await this.engine.find("sys_department", { filter, fields: ["id"], limit: 1e3, context: SYSTEM_CTX });
413
1127
  } catch {
414
1128
  kids = [];
415
1129
  }
@@ -427,7 +1141,7 @@ var ApprovalService = class {
427
1141
  filter: { department_id: { $in: Array.from(seen) } },
428
1142
  fields: ["user_id"],
429
1143
  limit: 1e4,
430
- context: SYSTEM_CTX2
1144
+ context: SYSTEM_CTX
431
1145
  });
432
1146
  } catch {
433
1147
  rows = [];
@@ -440,7 +1154,7 @@ var ApprovalService = class {
440
1154
  if (organizationId) filter.organization_id = organizationId;
441
1155
  let rows = [];
442
1156
  try {
443
- rows = await this.engine.find("sys_member", { filter, fields: ["user_id"], limit: 1e4, context: SYSTEM_CTX2 });
1157
+ rows = await this.engine.find("sys_member", { filter, fields: ["user_id"], limit: 1e4, context: SYSTEM_CTX });
444
1158
  } catch {
445
1159
  rows = [];
446
1160
  }
@@ -452,7 +1166,7 @@ var ApprovalService = class {
452
1166
  filter: { id: userId },
453
1167
  fields: ["id", "manager_id"],
454
1168
  limit: 1,
455
- context: SYSTEM_CTX2
1169
+ context: SYSTEM_CTX
456
1170
  });
457
1171
  const row = Array.isArray(rows) ? rows[0] : null;
458
1172
  return row?.manager_id ? String(row.manager_id) : null;
@@ -460,263 +1174,189 @@ var ApprovalService = class {
460
1174
  return null;
461
1175
  }
462
1176
  }
463
- async notifyRegistryChanged() {
464
- const cb = this.onRegistryChange ?? this.onRegistryChange;
465
- if (!cb) return;
1177
+ /** Mirror a request status onto a business-object field, if configured. */
1178
+ async mirrorStatusField(object, recordId, field, status) {
466
1179
  try {
467
- await cb();
1180
+ await this.engine.update(object, { id: recordId, [field]: status }, { context: SYSTEM_CTX });
468
1181
  } catch (err) {
469
- this.logger?.warn?.("[approvals] onRegistryChange handler failed", { error: err?.message });
1182
+ this.logger?.warn?.(`[approvals] mirrorStatusField failed: ${err?.message ?? err}`);
470
1183
  }
471
1184
  }
1185
+ // ── ADR-0019: Approval-as-flow-node ──────────────────────────
1186
+ //
1187
+ // A flow's Approval node opens a request via `openNodeRequest` (carrying its
1188
+ // own approvers/behavior config and the suspended run id), then suspends. A
1189
+ // later `decide` finalizes it and resumes the flow run down the matching
1190
+ // `approve`/`reject` edge. The record lock is enforced by a beforeUpdate hook
1191
+ // keyed on a *pending* request, so finalizing auto-releases it.
472
1192
  /**
473
- * Look up the HEAD checksum of an approval process from the metadata repo
474
- * (ADR-0009). Returns null when no repo is wired, no metadata exists for
475
- * the name, or the lookup fails callers MUST treat null as "do not pin"
476
- * and fall back to the projection table.
1193
+ * Open a pending approval request on behalf of a flow's Approval node. The
1194
+ * node config (approvers / behavior / status field) is snapshotted on the row
1195
+ * so a decision can be made without any process to resolve against.
477
1196
  */
478
- async resolveProcessHash(processName, organizationId) {
479
- if (!this.metadataRepo) return null;
480
- if (!processName) return null;
481
- const orgRef = { org: organizationId || "system", type: "approval", name: processName };
482
- try {
483
- const head = await this.metadataRepo.get(orgRef);
484
- return head?.hash ?? null;
485
- } catch (err) {
486
- this.logger?.debug?.("[approvals] metadataRepo.get failed", { name: processName, error: err?.message });
487
- return null;
488
- }
489
- }
490
- /**
491
- * Resolve the approval process for an in-flight request, honouring
492
- * ADR-0009 execution pinning when a `process_hash` is recorded.
493
- *
494
- * Resolution order:
495
- * 1. If `req.process_hash` AND `metadataRepo` are set, try
496
- * `getByHash` — return a row whose `definition` is the pinned body.
497
- * 2. Otherwise (or on lookup failure) fall back to the current
498
- * projection via `getProcess(req.process_name)`.
499
- */
500
- async loadProcessForRequest(req, context) {
501
- const hash = req.process_hash;
502
- if (hash && this.metadataRepo) {
503
- const orgId = req.organization_id ?? null;
504
- const orgRef = { org: orgId || "system", type: "approval", name: req.process_name };
505
- try {
506
- const pinned = await this.metadataRepo.getByHash(orgRef, hash);
507
- if (pinned?.body) {
508
- const current = await this.getProcess(req.process_name, context);
509
- const body = pinned.body;
510
- return {
511
- id: current?.id ?? `pinned_${hash.slice(7, 19)}`,
512
- name: req.process_name,
513
- label: body.label ?? current?.label ?? req.process_name,
514
- object_name: req.object_name,
515
- description: body.description ?? current?.description,
516
- active: current?.active ?? true,
517
- definition: body,
518
- created_at: current?.created_at,
519
- updated_at: current?.updated_at
520
- };
521
- }
522
- this.logger?.warn?.("[approvals] pinned process body not found; falling back to current", {
523
- request: req.id,
524
- process: req.process_name,
525
- hash
526
- });
527
- } catch (err) {
528
- this.logger?.warn?.("[approvals] getByHash failed; falling back to current", {
529
- request: req.id,
530
- error: err?.message
531
- });
532
- }
533
- }
534
- return this.getProcess(req.process_name, context);
535
- }
536
- /** Mirror request status onto `process.approvalStatusField` if configured. */
537
- async syncStatusField(process, request) {
538
- const field = process.definition?.approvalStatusField;
539
- if (!field) return;
540
- try {
541
- await this.engine.update(
542
- process.object_name,
543
- { id: request.record_id, [field]: request.status },
544
- { context: SYSTEM_CTX2 }
545
- );
546
- } catch (err) {
547
- this.logger?.warn?.(`[approvals] syncStatusField failed: ${err?.message ?? err}`);
548
- }
549
- }
550
- /** Convenience wrapper that funnels every action invocation through the executor. */
551
- async runActions(actions, trigger, process, request, step, actorId, comment) {
552
- if (!actions || actions.length === 0) return;
553
- await executeActions(actions, {
554
- trigger,
555
- process: { ...process, object: process.object_name },
556
- request,
557
- step,
558
- actorId: actorId ?? null,
559
- comment: comment ?? null
560
- }, {
561
- engine: this.engine,
562
- logger: this.logger,
563
- fetch: this.fetchImpl,
564
- webhookTimeoutMs: this.webhookTimeoutMs
565
- });
566
- }
567
- // ── Process definitions ──────────────────────────────────────
568
- async defineProcess(input, _context) {
569
- if (!input.name) throw new Error("VALIDATION_FAILED: name is required");
570
- if (!input.label) throw new Error("VALIDATION_FAILED: label is required");
571
- if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
572
- if (!input.definition) throw new Error("VALIDATION_FAILED: definition is required");
573
- const parsed = import_automation.ApprovalProcessSchema.safeParse(input.definition);
574
- if (!parsed.success) {
575
- const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
576
- throw new Error(`VALIDATION_FAILED: ${msg}`);
577
- }
578
- const now = this.clock.now().toISOString();
579
- const payload = {
580
- name: input.name,
581
- label: input.label,
582
- object_name: input.object,
583
- description: input.description ?? null,
584
- active: input.active !== false,
585
- definition_json: JSON.stringify(parsed.data),
586
- updated_at: now
587
- };
588
- const existing = await this.engine.find("sys_approval_process", {
589
- where: { name: input.name },
590
- limit: 1,
591
- context: SYSTEM_CTX2
592
- });
593
- if (Array.isArray(existing) && existing[0]) {
594
- const id2 = existing[0].id;
595
- await this.engine.update("sys_approval_process", { id: id2, ...payload }, { context: SYSTEM_CTX2 });
596
- const row2 = rowFromProcess({ ...existing[0], ...payload, id: id2 });
597
- await this.notifyRegistryChanged();
598
- return row2;
599
- }
600
- const id = input.id ?? uid("apv");
601
- const row = { id, ...payload, created_at: now };
602
- await this.engine.insert("sys_approval_process", row, { context: SYSTEM_CTX2 });
603
- const out = rowFromProcess(row);
604
- await this.notifyRegistryChanged();
605
- return out;
606
- }
607
- async listProcesses(filter, _context) {
608
- const f = {};
609
- if (filter?.object) f.object_name = filter.object;
610
- if (filter?.activeOnly) f.active = true;
611
- const rows = await this.engine.find("sys_approval_process", {
612
- where: f,
613
- limit: 500,
614
- orderBy: [{ field: "updated_at", direction: "desc" }],
615
- context: SYSTEM_CTX2
616
- });
617
- return Array.isArray(rows) ? rows.map(rowFromProcess) : [];
618
- }
619
- async getProcess(idOrName, _context) {
620
- if (!idOrName) return null;
621
- let rows = await this.engine.find("sys_approval_process", {
622
- where: { id: idOrName },
623
- limit: 1,
624
- context: SYSTEM_CTX2
625
- });
626
- if (!Array.isArray(rows) || !rows[0]) {
627
- rows = await this.engine.find("sys_approval_process", {
628
- where: { name: idOrName },
629
- limit: 1,
630
- context: SYSTEM_CTX2
631
- });
632
- }
633
- return Array.isArray(rows) && rows[0] ? rowFromProcess(rows[0]) : null;
634
- }
635
- async deleteProcess(idOrName, context) {
636
- if (!idOrName) throw new Error("VALIDATION_FAILED: idOrName is required");
637
- const proc = await this.getProcess(idOrName, context);
638
- if (!proc) return;
639
- await this.engine.delete("sys_approval_process", { where: { id: proc.id }, context: SYSTEM_CTX2 });
640
- await this.notifyRegistryChanged();
641
- }
642
- // ── Requests ─────────────────────────────────────────────────
643
- async submit(input, context) {
1197
+ async openNodeRequest(input, context) {
644
1198
  if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
645
1199
  if (!input.recordId) throw new Error("VALIDATION_FAILED: recordId is required");
646
- let process = null;
647
- if (input.processName) {
648
- process = await this.getProcess(input.processName, context);
649
- if (process && !process.active) {
650
- throw new Error(`NO_ACTIVE_PROCESS: process '${input.processName}' is not active`);
651
- }
652
- } else {
653
- const list = await this.listProcesses({ object: input.object, activeOnly: true }, context);
654
- process = list[0] ?? null;
655
- }
656
- if (!process) {
657
- throw new Error(`NO_ACTIVE_PROCESS: no active approval process for object '${input.object}'`);
658
- }
1200
+ if (!input.runId) throw new Error("VALIDATION_FAILED: runId is required");
659
1201
  const existing = await this.engine.find("sys_approval_request", {
660
1202
  where: { object_name: input.object, record_id: input.recordId, status: "pending" },
661
1203
  limit: 1,
662
- context: SYSTEM_CTX2
1204
+ context: SYSTEM_CTX
663
1205
  });
664
1206
  if (Array.isArray(existing) && existing[0]) {
665
1207
  throw new Error(`DUPLICATE_REQUEST: a pending approval already exists for ${input.object}/${input.recordId}`);
666
1208
  }
667
- const steps = process.definition?.steps ?? [];
668
- if (steps.length === 0) {
669
- throw new Error("VALIDATION_FAILED: process definition has no steps");
670
- }
671
- const step0 = steps[0];
672
- const ctxOrg = context?.organizationId ?? context?.tenantId ?? null;
673
- const approvers = await this.expandApprovers(step0, input.payload, ctxOrg);
1209
+ const ctxOrg = context?.organizationId ?? context?.tenantId ?? input.organizationId ?? null;
1210
+ const approvers = await this.expandApprovers({ approvers: input.config.approvers }, input.record, ctxOrg);
674
1211
  const now = this.clock.now().toISOString();
675
1212
  const id = uid("areq");
676
- const processHash = await this.resolveProcessHash(process.name, ctxOrg);
1213
+ const processName = `flow:${input.flowName ?? input.nodeId}`;
677
1214
  const row = {
678
1215
  id,
679
- process_name: process.name,
680
- process_hash: processHash,
1216
+ process_name: processName,
681
1217
  object_name: input.object,
682
1218
  record_id: input.recordId,
683
1219
  submitter_id: input.submitterId ?? context.userId ?? null,
684
- submitter_comment: input.comment ?? null,
685
1220
  status: "pending",
686
- current_step: step0.name,
1221
+ current_step: input.nodeId,
687
1222
  current_step_index: 0,
688
1223
  pending_approvers: approvers.join(","),
689
- payload_json: input.payload != null ? JSON.stringify(input.payload) : null,
1224
+ payload_json: input.record != null ? JSON.stringify(input.record) : null,
1225
+ flow_run_id: input.runId,
1226
+ flow_node_id: input.nodeId,
1227
+ node_config_json: JSON.stringify(input.config),
690
1228
  organization_id: ctxOrg,
691
1229
  created_at: now,
692
1230
  updated_at: now
693
1231
  };
694
- await this.engine.insert("sys_approval_request", row, { context: SYSTEM_CTX2 });
1232
+ await this.engine.insert("sys_approval_request", row, { context: SYSTEM_CTX });
695
1233
  await this.engine.insert("sys_approval_action", {
696
1234
  id: uid("aact"),
697
1235
  request_id: id,
698
1236
  organization_id: ctxOrg,
699
- step_name: step0.name,
1237
+ step_name: input.nodeId,
700
1238
  step_index: 0,
701
1239
  action: "submit",
702
1240
  actor_id: input.submitterId ?? context.userId ?? null,
1241
+ comment: null,
1242
+ created_at: now
1243
+ }, { context: SYSTEM_CTX });
1244
+ if (input.config.approvalStatusField) {
1245
+ await this.mirrorStatusField(input.object, input.recordId, input.config.approvalStatusField, "pending");
1246
+ }
1247
+ return rowFromRequest(row);
1248
+ }
1249
+ /**
1250
+ * Record a decision on a node-driven request. Honours the node's `unanimous`
1251
+ * behavior (holds until every approver has approved). When the request
1252
+ * finalizes, returns the suspended run id + node id so the caller (or
1253
+ * {@link ApprovalService.decide}) can resume the flow down the matching
1254
+ * branch.
1255
+ */
1256
+ async decideNode(requestId, input, context) {
1257
+ if (!requestId) throw new Error("VALIDATION_FAILED: requestId is required");
1258
+ if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
1259
+ if (input.decision !== "approve" && input.decision !== "reject") {
1260
+ throw new Error("VALIDATION_FAILED: decision must be approve|reject");
1261
+ }
1262
+ const rawRows = await this.engine.find("sys_approval_request", {
1263
+ where: { id: requestId },
1264
+ limit: 1,
1265
+ context: SYSTEM_CTX
1266
+ });
1267
+ const raw = Array.isArray(rawRows) ? rawRows[0] : null;
1268
+ if (!raw) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
1269
+ if (raw.status !== "pending") throw new Error(`INVALID_STATE: request is ${raw.status}`);
1270
+ const pendingApprovers = csvSplit(raw.pending_approvers);
1271
+ if (!context.isSystem && !pendingApprovers.includes(input.actorId)) {
1272
+ throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
1273
+ }
1274
+ const config = parseJson(raw.node_config_json, { approvers: [], behavior: "first_response" });
1275
+ const org = raw.organization_id ?? null;
1276
+ const nodeId = raw.flow_node_id ?? raw.current_step ?? null;
1277
+ const runId = raw.flow_run_id ?? null;
1278
+ const now = this.clock.now().toISOString();
1279
+ await this.engine.insert("sys_approval_action", {
1280
+ id: uid("aact"),
1281
+ request_id: requestId,
1282
+ organization_id: org,
1283
+ step_name: nodeId,
1284
+ step_index: 0,
1285
+ action: input.decision,
1286
+ actor_id: input.actorId,
703
1287
  comment: input.comment ?? null,
704
1288
  created_at: now
705
- }, { context: SYSTEM_CTX2 });
706
- const requestRow = rowFromRequest(row);
707
- await this.syncStatusField(process, requestRow);
708
- const definition = process.definition ?? {};
709
- await this.runActions(
710
- definition.onSubmit,
711
- "submit",
712
- process,
713
- requestRow,
714
- step0,
715
- input.submitterId ?? context.userId ?? null,
716
- input.comment ?? null
717
- );
718
- return requestRow;
1289
+ }, { context: SYSTEM_CTX });
1290
+ if (input.decision === "approve" && config.behavior === "unanimous") {
1291
+ const original = await this.expandApprovers(
1292
+ { approvers: config.approvers },
1293
+ parseJson(raw.payload_json, void 0),
1294
+ org
1295
+ );
1296
+ const acts = await this.engine.find("sys_approval_action", {
1297
+ where: { request_id: requestId, step_index: 0, action: "approve" },
1298
+ limit: 500,
1299
+ context: SYSTEM_CTX
1300
+ });
1301
+ const approved = new Set((acts ?? []).map((a) => String(a.actor_id ?? "")).filter(Boolean));
1302
+ const stillPending = original.filter((a) => !approved.has(a));
1303
+ if (stillPending.length > 0) {
1304
+ await this.engine.update("sys_approval_request", {
1305
+ id: requestId,
1306
+ pending_approvers: stillPending.join(","),
1307
+ updated_at: now
1308
+ }, { context: SYSTEM_CTX });
1309
+ const fresh2 = await this.getRequest(requestId, context);
1310
+ return { request: fresh2, runId, nodeId, finalized: false, decision: input.decision };
1311
+ }
1312
+ }
1313
+ const finalStatus = input.decision === "approve" ? "approved" : "rejected";
1314
+ await this.engine.update("sys_approval_request", {
1315
+ id: requestId,
1316
+ status: finalStatus,
1317
+ pending_approvers: null,
1318
+ completed_at: now,
1319
+ updated_at: now
1320
+ }, { context: SYSTEM_CTX });
1321
+ if (config.approvalStatusField) {
1322
+ await this.mirrorStatusField(raw.object_name, raw.record_id, config.approvalStatusField, finalStatus);
1323
+ }
1324
+ const fresh = await this.getRequest(requestId, context);
1325
+ return { request: fresh, runId, nodeId, finalized: true, decision: input.decision };
1326
+ }
1327
+ /**
1328
+ * Public contract entrypoint (ADR-0019). Records a decision on a node-driven
1329
+ * request via {@link ApprovalService.decideNode} and, when it finalizes,
1330
+ * resumes the owning flow run down the matching `approve` / `reject` edge.
1331
+ */
1332
+ async decide(requestId, input, context) {
1333
+ const result = await this.decideNode(requestId, input, context);
1334
+ let resumed = false;
1335
+ if (result.finalized && result.runId && typeof this.automation?.resume === "function") {
1336
+ const branchLabel = result.decision === "approve" ? import_automation.APPROVAL_BRANCH_LABELS.approve : import_automation.APPROVAL_BRANCH_LABELS.reject;
1337
+ try {
1338
+ await this.automation.resume(result.runId, {
1339
+ branchLabel,
1340
+ output: { decision: result.decision, requestId }
1341
+ });
1342
+ resumed = true;
1343
+ } catch (err) {
1344
+ this.logger?.warn?.("[approvals] resume after decision failed", {
1345
+ request: requestId,
1346
+ run: result.runId,
1347
+ error: err?.message ?? String(err)
1348
+ });
1349
+ }
1350
+ }
1351
+ return {
1352
+ request: result.request,
1353
+ finalized: result.finalized,
1354
+ decision: result.decision,
1355
+ runId: result.runId,
1356
+ resumed
1357
+ };
719
1358
  }
1359
+ // ── Read API ─────────────────────────────────────────────────
720
1360
  async listRequests(filter, context) {
721
1361
  const f = {};
722
1362
  if (filter?.object) f.object_name = filter.object;
@@ -731,7 +1371,7 @@ var ApprovalService = class {
731
1371
  where: f,
732
1372
  limit: 500,
733
1373
  orderBy: [{ field: "updated_at", direction: "desc" }],
734
- context: SYSTEM_CTX2
1374
+ context: SYSTEM_CTX
735
1375
  });
736
1376
  let list = Array.isArray(rows) ? rows.map(rowFromRequest) : [];
737
1377
  if (statusFilter) list = list.filter((r) => statusFilter.includes(r.status));
@@ -749,169 +1389,10 @@ var ApprovalService = class {
749
1389
  const rows = await this.engine.find("sys_approval_request", {
750
1390
  where,
751
1391
  limit: 1,
752
- context: SYSTEM_CTX2
1392
+ context: SYSTEM_CTX
753
1393
  });
754
1394
  return Array.isArray(rows) && rows[0] ? rowFromRequest(rows[0]) : null;
755
1395
  }
756
- async approve(requestId, input, context) {
757
- const req = await this.getRequest(requestId, context);
758
- if (!req) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
759
- if (req.status !== "pending") throw new Error(`INVALID_STATE: request is ${req.status}`);
760
- if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
761
- if (!context.isSystem && !(req.pending_approvers ?? []).includes(input.actorId)) {
762
- throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
763
- }
764
- const process = await this.loadProcessForRequest(req, context);
765
- if (!process) throw new Error(`PROCESS_NOT_FOUND: ${req.process_name}`);
766
- const steps = process.definition?.steps ?? [];
767
- const stepIndex = req.current_step_index ?? 0;
768
- const step = steps[stepIndex];
769
- if (!step) throw new Error(`INVALID_STATE: step index ${stepIndex} out of range`);
770
- const now = this.clock.now().toISOString();
771
- await this.engine.insert("sys_approval_action", {
772
- id: uid("aact"),
773
- request_id: req.id,
774
- organization_id: req.organization_id ?? null,
775
- step_name: step.name,
776
- step_index: stepIndex,
777
- action: "approve",
778
- actor_id: input.actorId,
779
- comment: input.comment ?? null,
780
- created_at: now
781
- }, { context: SYSTEM_CTX2 });
782
- if (step.behavior === "unanimous") {
783
- const original = await this.expandApprovers(step, req.payload, req.organization_id ?? null);
784
- const acts = await this.engine.find("sys_approval_action", {
785
- where: { request_id: req.id, step_index: stepIndex, action: "approve" },
786
- limit: 500,
787
- context: SYSTEM_CTX2
788
- });
789
- const approved = new Set((acts ?? []).map((a) => String(a.actor_id ?? "")).filter(Boolean));
790
- const stillPending = original.filter((a) => !approved.has(a));
791
- if (stillPending.length > 0) {
792
- await this.engine.update("sys_approval_request", {
793
- id: req.id,
794
- pending_approvers: stillPending.join(","),
795
- updated_at: now
796
- }, { context: SYSTEM_CTX2 });
797
- const fresh2 = await this.getRequest(req.id, context);
798
- return { request: fresh2, finalized: false };
799
- }
800
- }
801
- if (stepIndex + 1 >= steps.length) {
802
- await this.engine.update("sys_approval_request", {
803
- id: req.id,
804
- status: "approved",
805
- pending_approvers: null,
806
- completed_at: now,
807
- updated_at: now
808
- }, { context: SYSTEM_CTX2 });
809
- const fresh2 = await this.getRequest(req.id, context);
810
- await this.runActions(step?.onApprove, "step_approve", process, fresh2, step, input.actorId, input.comment);
811
- await this.syncStatusField(process, fresh2);
812
- await this.runActions(process.definition?.onFinalApprove, "final_approve", process, fresh2, step, input.actorId, input.comment);
813
- return { request: fresh2, finalized: true };
814
- }
815
- const nextStep = steps[stepIndex + 1];
816
- const nextApprovers = await this.expandApprovers(nextStep, req.payload, req.organization_id ?? null);
817
- await this.engine.update("sys_approval_request", {
818
- id: req.id,
819
- current_step: nextStep.name,
820
- current_step_index: stepIndex + 1,
821
- pending_approvers: nextApprovers.join(","),
822
- updated_at: now
823
- }, { context: SYSTEM_CTX2 });
824
- const fresh = await this.getRequest(req.id, context);
825
- await this.runActions(step?.onApprove, "step_approve", process, fresh, step, input.actorId, input.comment);
826
- return { request: fresh, finalized: false };
827
- }
828
- async reject(requestId, input, context) {
829
- const req = await this.getRequest(requestId, context);
830
- if (!req) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
831
- if (req.status !== "pending") throw new Error(`INVALID_STATE: request is ${req.status}`);
832
- if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
833
- if (!context.isSystem && !(req.pending_approvers ?? []).includes(input.actorId)) {
834
- throw new Error(`FORBIDDEN: actor '${input.actorId}' is not a pending approver`);
835
- }
836
- const process = await this.loadProcessForRequest(req, context);
837
- if (!process) throw new Error(`PROCESS_NOT_FOUND: ${req.process_name}`);
838
- const steps = process.definition?.steps ?? [];
839
- const stepIndex = req.current_step_index ?? 0;
840
- const step = steps[stepIndex];
841
- const now = this.clock.now().toISOString();
842
- await this.engine.insert("sys_approval_action", {
843
- id: uid("aact"),
844
- request_id: req.id,
845
- organization_id: req.organization_id ?? null,
846
- step_name: step?.name,
847
- step_index: stepIndex,
848
- action: "reject",
849
- actor_id: input.actorId,
850
- comment: input.comment ?? null,
851
- created_at: now
852
- }, { context: SYSTEM_CTX2 });
853
- if (step?.rejectionBehavior === "back_to_previous" && stepIndex > 0) {
854
- const prev = steps[stepIndex - 1];
855
- const prevApprovers = await this.expandApprovers(prev, req.payload, req.organization_id ?? null);
856
- await this.engine.update("sys_approval_request", {
857
- id: req.id,
858
- current_step: prev.name,
859
- current_step_index: stepIndex - 1,
860
- pending_approvers: prevApprovers.join(","),
861
- updated_at: now
862
- }, { context: SYSTEM_CTX2 });
863
- const fresh2 = await this.getRequest(req.id, context);
864
- await this.runActions(step?.onReject, "step_reject", process, fresh2, step, input.actorId, input.comment);
865
- return { request: fresh2, finalized: false };
866
- }
867
- await this.engine.update("sys_approval_request", {
868
- id: req.id,
869
- status: "rejected",
870
- pending_approvers: null,
871
- completed_at: now,
872
- updated_at: now
873
- }, { context: SYSTEM_CTX2 });
874
- const fresh = await this.getRequest(req.id, context);
875
- await this.runActions(step?.onReject, "step_reject", process, fresh, step, input.actorId, input.comment);
876
- await this.syncStatusField(process, fresh);
877
- await this.runActions(process.definition?.onFinalReject, "final_reject", process, fresh, step, input.actorId, input.comment);
878
- return { request: fresh, finalized: true };
879
- }
880
- async recall(requestId, input, context) {
881
- const req = await this.getRequest(requestId, context);
882
- if (!req) throw new Error(`REQUEST_NOT_FOUND: ${requestId}`);
883
- if (req.status !== "pending") throw new Error(`INVALID_STATE: request is ${req.status}`);
884
- if (!input?.actorId) throw new Error("VALIDATION_FAILED: actorId is required");
885
- if (!context.isSystem && req.submitter_id && req.submitter_id !== input.actorId) {
886
- throw new Error(`FORBIDDEN: only the submitter can recall this request`);
887
- }
888
- const now = this.clock.now().toISOString();
889
- await this.engine.insert("sys_approval_action", {
890
- id: uid("aact"),
891
- request_id: req.id,
892
- organization_id: req.organization_id ?? null,
893
- step_name: req.current_step,
894
- step_index: req.current_step_index,
895
- action: "recall",
896
- actor_id: input.actorId,
897
- comment: input.comment ?? null,
898
- created_at: now
899
- }, { context: SYSTEM_CTX2 });
900
- await this.engine.update("sys_approval_request", {
901
- id: req.id,
902
- status: "recalled",
903
- pending_approvers: null,
904
- completed_at: now,
905
- updated_at: now
906
- }, { context: SYSTEM_CTX2 });
907
- const fresh = await this.getRequest(req.id, context);
908
- const process = await this.loadProcessForRequest(req, context);
909
- if (process) {
910
- await this.syncStatusField(process, fresh);
911
- await this.runActions(process.definition?.onRecall, "recall", process, fresh, void 0, input.actorId, input.comment);
912
- }
913
- return { request: fresh, finalized: true };
914
- }
915
1396
  async listActions(requestId, context) {
916
1397
  if (!requestId) return [];
917
1398
  const req = await this.getRequest(requestId, context);
@@ -920,160 +1401,131 @@ var ApprovalService = class {
920
1401
  where: { request_id: requestId },
921
1402
  limit: 500,
922
1403
  orderBy: [{ field: "created_at", direction: "asc" }],
923
- context: SYSTEM_CTX2
1404
+ context: SYSTEM_CTX
924
1405
  });
925
1406
  return Array.isArray(rows) ? rows.map(rowFromAction) : [];
926
1407
  }
927
1408
  };
928
1409
 
929
- // src/approvals-plugin.ts
930
- var import_audit = require("@objectstack/platform-objects/audit");
931
-
932
1410
  // src/lifecycle-hooks.ts
933
- var import_formula = require("@objectstack/formula");
934
- var APPROVALS_HOOK_PACKAGE = "plugin-approvals:auto";
935
- var SYSTEM_CTX3 = { isSystem: true, roles: [], permissions: [] };
936
- function evaluateCriteria(criteria, record, logger) {
937
- if (criteria == null || criteria === "") return true;
938
- let expr;
939
- if (typeof criteria === "string") {
940
- expr = { dialect: "cel", source: criteria };
941
- } else if (typeof criteria === "object" && criteria.dialect) {
942
- expr = criteria;
943
- } else {
944
- return true;
945
- }
946
- if (!expr.source || !expr.source.trim()) return true;
947
- const r = import_formula.ExpressionEngine.evaluate(expr, { record });
948
- if (!r.ok) {
949
- logger?.warn?.("[approvals] entryCriteria evaluation failed; skipping auto-submit", {
950
- source: expr.source,
951
- error: r.error.message
952
- });
953
- return false;
1411
+ var APPROVALS_HOOK_PACKAGE = "plugin-approvals:lock";
1412
+ function parseJson2(raw, fallback) {
1413
+ if (raw == null || raw === "") return fallback;
1414
+ if (typeof raw === "string") {
1415
+ try {
1416
+ return JSON.parse(raw);
1417
+ } catch {
1418
+ return fallback;
1419
+ }
954
1420
  }
955
- return Boolean(r.value);
1421
+ return raw;
956
1422
  }
957
- async function hasPendingRequest(engine, objectName, recordId) {
1423
+ async function pendingRequestFor(engine, objectName, recordId) {
958
1424
  try {
959
1425
  const rows = await engine.find("sys_approval_request", {
960
1426
  where: { object_name: objectName, record_id: String(recordId), status: "pending" },
961
1427
  limit: 1
962
1428
  });
963
- return Array.isArray(rows) && rows.length > 0;
1429
+ return Array.isArray(rows) && rows[0] ? rows[0] : null;
964
1430
  } catch {
965
- return false;
1431
+ return null;
966
1432
  }
967
1433
  }
968
- function bindProcessHooks(engine, service, processes, logger) {
969
- const byObject = /* @__PURE__ */ new Map();
970
- for (const p of processes) {
971
- if (!p.active && !p.is_active) continue;
972
- if (!p.object_name) continue;
973
- const list = byObject.get(p.object_name) ?? [];
974
- list.push(p);
975
- byObject.set(p.object_name, list);
976
- }
977
- for (const [objectName, procs] of byObject.entries()) {
978
- engine.registerHook("afterInsert", async (ctx) => {
979
- try {
980
- const record = ctx?.result ?? ctx?.input?.data ?? {};
981
- const id = String(record?.id ?? "");
982
- if (!id) return;
983
- for (const proc of procs) {
984
- await tryAutoSubmit(engine, service, proc, objectName, id, record, ctx, logger);
985
- }
986
- } catch (err) {
987
- logger?.warn?.("[approvals] afterInsert auto-trigger failed", { error: err?.message });
988
- }
989
- }, { object: objectName, packageId: APPROVALS_HOOK_PACKAGE, priority: 200 });
990
- engine.registerHook("afterUpdate", async (ctx) => {
991
- if (ctx?.session?.isSystem) return;
992
- try {
993
- const result = ctx?.result ?? {};
994
- const id = String(ctx?.input?.id ?? result?.id ?? "");
995
- if (!id) return;
996
- const record = {
997
- ...ctx?.previous ?? {},
998
- ...result?.id ? result : {},
999
- ...ctx?.input?.data ?? {},
1000
- id
1001
- };
1002
- for (const proc of procs) {
1003
- await tryAutoSubmit(engine, service, proc, objectName, id, record, ctx, logger);
1004
- }
1005
- } catch (err) {
1006
- logger?.warn?.("[approvals] afterUpdate auto-trigger failed", { error: err?.message });
1007
- }
1008
- }, { object: objectName, packageId: APPROVALS_HOOK_PACKAGE, priority: 200 });
1009
- const lockProcs = procs.filter((p) => p.definition?.lockRecord !== false);
1010
- if (lockProcs.length === 0) continue;
1011
- engine.registerHook("beforeUpdate", async (ctx) => {
1012
- const id = String(ctx?.input?.id ?? "");
1013
- if (!id) return;
1014
- const data = ctx?.input?.data ?? {};
1015
- const changedFields = Object.keys(data).filter((k) => k !== "id" && k !== "updated_at");
1016
- if (changedFields.length === 0) return;
1017
- if (ctx?.session?.isSystem) return;
1018
- const mirrorFields = /* @__PURE__ */ new Set();
1019
- for (const p of lockProcs) {
1020
- const f = p.definition?.approvalStatusField;
1021
- if (typeof f === "string" && f) mirrorFields.add(f);
1022
- }
1023
- const onlyMirror = changedFields.every((f) => mirrorFields.has(f));
1024
- if (onlyMirror) return;
1025
- const roles = ctx?.session?.roles ?? [];
1026
- if (Array.isArray(roles) && roles.includes("admin")) return;
1027
- const pending = await hasPendingRequest(engine, objectName, id);
1028
- if (!pending) return;
1029
- const err = new Error("RECORD_LOCKED: record is locked while an approval is in progress");
1030
- err.code = "RECORD_LOCKED";
1031
- err.statusCode = 409;
1032
- throw err;
1033
- }, { object: objectName, packageId: APPROVALS_HOOK_PACKAGE, priority: 50 });
1034
- }
1035
- logger?.info?.("[approvals] lifecycle hooks bound", {
1036
- objects: Array.from(byObject.keys()),
1037
- processCount: processes.length
1038
- });
1434
+ function bindApprovalLockHook(engine, logger) {
1435
+ engine.registerHook("beforeUpdate", async (ctx) => {
1436
+ const id = String(ctx?.input?.id ?? "");
1437
+ if (!id) return;
1438
+ const object = ctx?.object ?? ctx?.objectName;
1439
+ if (!object || String(object).startsWith("sys_approval")) return;
1440
+ const data = ctx?.input?.data ?? {};
1441
+ const changedFields = Object.keys(data).filter((k) => k !== "id" && k !== "updated_at");
1442
+ if (changedFields.length === 0) return;
1443
+ if (ctx?.session?.isSystem) return;
1444
+ const roles = ctx?.session?.roles ?? [];
1445
+ if (Array.isArray(roles) && roles.includes("admin")) return;
1446
+ const pending = await pendingRequestFor(engine, object, id);
1447
+ if (!pending) return;
1448
+ const config = parseJson2(pending.node_config_json, {});
1449
+ if (config?.lockRecord === false) return;
1450
+ const mirror = config?.approvalStatusField;
1451
+ if (typeof mirror === "string" && mirror && changedFields.every((f) => f === mirror)) return;
1452
+ const err = new Error("RECORD_LOCKED: record is locked while an approval is in progress");
1453
+ err.code = "RECORD_LOCKED";
1454
+ err.statusCode = 409;
1455
+ throw err;
1456
+ }, { packageId: APPROVALS_HOOK_PACKAGE, priority: 50 });
1457
+ logger?.info?.("[approvals] record-lock hook bound");
1039
1458
  }
1040
1459
  function unbindAllHooks(engine) {
1041
1460
  return engine.unregisterHooksByPackage(APPROVALS_HOOK_PACKAGE);
1042
1461
  }
1043
- async function tryAutoSubmit(engine, service, process, objectName, recordId, record, ctx, logger) {
1044
- try {
1045
- const criteria = process.definition?.entryCriteria;
1046
- const passes = evaluateCriteria(criteria, record, logger);
1047
- if (!passes) return;
1048
- if (await hasPendingRequest(engine, objectName, recordId)) return;
1049
- const statusField = process.definition?.approvalStatusField;
1050
- if (statusField) {
1051
- const current = record?.[statusField];
1052
- if (current === "approved" || current === "rejected" || current === "recalled") return;
1462
+
1463
+ // src/approval-node.ts
1464
+ var import_automation2 = require("@objectstack/spec/automation");
1465
+ var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
1466
+ function registerApprovalNode(automation, service, logger) {
1467
+ automation.registerNodeExecutor({
1468
+ type: import_automation2.APPROVAL_NODE_TYPE,
1469
+ descriptor: (0, import_automation2.defineActionDescriptor)({
1470
+ type: import_automation2.APPROVAL_NODE_TYPE,
1471
+ version: "1.0.0",
1472
+ name: "Approval",
1473
+ description: "Route a record for human approval; suspends the flow until a decision, then continues down the approve / reject branch.",
1474
+ icon: "check-circle",
1475
+ category: "human",
1476
+ paradigms: ["flow"],
1477
+ source: "plugin",
1478
+ // Human decision: the run suspends here awaiting an external reply.
1479
+ supportsPause: true,
1480
+ isAsync: true,
1481
+ // Publish the node's config contract (ADR-0018 §configSchema) so the
1482
+ // Studio flow designer renders the Approval property form from the engine
1483
+ // rather than a hardcoded client form — the engine owns the shape.
1484
+ configSchema: (0, import_automation2.getApprovalNodeConfigJsonSchema)()
1485
+ }),
1486
+ async execute(node, variables, context) {
1487
+ const parsed = import_automation2.ApprovalNodeConfigSchema.safeParse(node.config ?? {});
1488
+ if (!parsed.success) {
1489
+ const msg = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
1490
+ return { success: false, error: `Approval node '${node.id}' has invalid config: ${msg}` };
1491
+ }
1492
+ const config = parsed.data;
1493
+ const runId = variables.get("$runId");
1494
+ const record = variables.get("$record") ?? context?.record ?? {};
1495
+ const object = context?.object ?? record?.object_name;
1496
+ const recordId = record?.id;
1497
+ if (!runId) return { success: false, error: `Approval node '${node.id}': missing $runId` };
1498
+ if (!object) return { success: false, error: `Approval node '${node.id}': no target object in context` };
1499
+ if (!recordId) return { success: false, error: `Approval node '${node.id}': no record id in $record` };
1500
+ try {
1501
+ const request = await service.openNodeRequest({
1502
+ object,
1503
+ recordId: String(recordId),
1504
+ runId: String(runId),
1505
+ nodeId: node.id,
1506
+ config,
1507
+ flowName: context?.flowName,
1508
+ submitterId: context?.userId ?? null,
1509
+ record,
1510
+ organizationId: context?.organizationId ?? context?.tenantId ?? null
1511
+ }, {
1512
+ ...SYSTEM_CTX2,
1513
+ userId: context?.userId,
1514
+ organizationId: context?.organizationId,
1515
+ tenantId: context?.tenantId
1516
+ });
1517
+ logger?.info?.("[approvals] approval node suspended run", {
1518
+ node: node.id,
1519
+ request: request.id,
1520
+ run: String(runId)
1521
+ });
1522
+ return { success: true, suspend: true, correlation: request.id };
1523
+ } catch (err) {
1524
+ return { success: false, error: `Approval node '${node.id}': ${err?.message ?? String(err)}` };
1525
+ }
1053
1526
  }
1054
- const submitterId = ctx?.session?.userId ?? null;
1055
- const submitterOrg = ctx?.session?.tenantId ?? ctx?.session?.organizationId ?? null;
1056
- await service.submit({
1057
- object: objectName,
1058
- recordId,
1059
- processName: process.name,
1060
- payload: record,
1061
- submitterId
1062
- }, { ...SYSTEM_CTX3, userId: submitterId ?? void 0, organizationId: submitterOrg ?? void 0, tenantId: submitterOrg ?? void 0 });
1063
- logger?.info?.("[approvals] auto-submitted approval", {
1064
- process: process.name,
1065
- object: objectName,
1066
- record: recordId
1067
- });
1068
- } catch (err) {
1069
- if (err?.code === "DUPLICATE_REQUEST") return;
1070
- logger?.warn?.("[approvals] auto-submit failed", {
1071
- process: process.name,
1072
- object: objectName,
1073
- record: recordId,
1074
- error: err?.message ?? String(err)
1075
- });
1076
- }
1527
+ });
1528
+ logger?.info?.("[approvals] approval node executor registered");
1077
1529
  }
1078
1530
 
1079
1531
  // src/approvals-plugin.ts
@@ -1094,8 +1546,36 @@ var ApprovalsServicePlugin = class {
1094
1546
  scope: "system",
1095
1547
  defaultDatasource: "cloud",
1096
1548
  namespace: "sys",
1097
- objects: [import_audit.SysApprovalProcess, import_audit.SysApprovalRequest, import_audit.SysApprovalAction]
1549
+ objects: [SysApprovalRequest, SysApprovalAction],
1550
+ // ADR-0029 D7 — contribute the Approvals entries into the Setup app's
1551
+ // `group_approvals` slot. This plugin owns these objects (K2.b), so it
1552
+ // ships their menu too; when the plugin isn't installed the slot is empty.
1553
+ navigationContributions: [
1554
+ {
1555
+ app: "setup",
1556
+ group: "group_approvals",
1557
+ priority: 100,
1558
+ items: [
1559
+ { id: "nav_approval_requests", type: "object", label: "Requests", objectName: "sys_approval_request", icon: "inbox", requiresObject: "sys_approval_request" },
1560
+ { id: "nav_approval_actions", type: "object", label: "Action History", objectName: "sys_approval_action", icon: "history", requiresObject: "sys_approval_action" }
1561
+ ]
1562
+ }
1563
+ ]
1098
1564
  });
1565
+ if (typeof ctx.hook === "function") {
1566
+ ctx.hook("kernel:ready", async () => {
1567
+ try {
1568
+ const i18n = ctx.getService("i18n");
1569
+ if (i18n && typeof i18n.loadTranslations === "function") {
1570
+ const { ApprovalsTranslations: ApprovalsTranslations2 } = await Promise.resolve().then(() => (init_translations(), translations_exports));
1571
+ for (const [locale, data] of Object.entries(ApprovalsTranslations2)) {
1572
+ i18n.loadTranslations(locale, data);
1573
+ }
1574
+ }
1575
+ } catch {
1576
+ }
1577
+ });
1578
+ }
1099
1579
  ctx.logger.info("ApprovalsServicePlugin: schemas registered");
1100
1580
  }
1101
1581
  async start(ctx) {
@@ -1114,47 +1594,28 @@ var ApprovalsServicePlugin = class {
1114
1594
  return;
1115
1595
  }
1116
1596
  this.engine = engine;
1117
- this.logger = ctx.logger;
1118
- let metadataRepo;
1119
- try {
1120
- const meta = ctx.getService("metadata");
1121
- metadataRepo = meta?.getRepository?.();
1122
- } catch {
1123
- }
1124
1597
  this.service = new ApprovalService({
1125
1598
  engine,
1126
- logger: ctx.logger,
1127
- metadataRepo
1599
+ logger: ctx.logger
1128
1600
  });
1129
- if (metadataRepo) {
1130
- ctx.logger.info("ApprovalsServicePlugin: execution pinning enabled (ADR-0009)");
1131
- }
1132
1601
  if (!this.options.disableAutoHooks) {
1133
- this.service.setRegistryChangeHandler(() => this.rebindHooks());
1134
- const hookOn = ctx.hook ?? ctx.on;
1135
- if (typeof hookOn === "function") {
1136
- try {
1137
- hookOn.call(ctx, "kernel:ready", async () => {
1138
- await this.rebindHooks();
1139
- });
1140
- } catch {
1141
- await this.rebindHooks();
1142
- }
1143
- } else {
1144
- await this.rebindHooks();
1602
+ try {
1603
+ unbindAllHooks(engine);
1604
+ bindApprovalLockHook(engine, ctx.logger);
1605
+ } catch (err) {
1606
+ ctx.logger.warn?.("[approvals] failed to bind record-lock hook", { error: err?.message });
1145
1607
  }
1146
1608
  }
1147
1609
  ctx.registerService("approvals", this.service);
1148
1610
  ctx.logger.info("ApprovalsServicePlugin: service registered");
1149
- }
1150
- async rebindHooks() {
1151
- if (!this.engine || !this.service) return;
1152
1611
  try {
1153
- unbindAllHooks(this.engine);
1154
- const processes = await this.service.listProcesses({ activeOnly: true }, { isSystem: true, roles: [], permissions: [] });
1155
- bindProcessHooks(this.engine, this.service, processes, this.logger);
1156
- } catch (err) {
1157
- this.logger?.warn?.("[approvals] rebindHooks failed", { error: err?.message });
1612
+ const automation = ctx.getService("automation");
1613
+ if (automation && typeof automation.registerNodeExecutor === "function") {
1614
+ this.service.attachAutomation(automation);
1615
+ registerApprovalNode(automation, this.service, ctx.logger);
1616
+ }
1617
+ } catch {
1618
+ ctx.logger.info("ApprovalsServicePlugin: no automation engine \u2014 approval node not registered");
1158
1619
  }
1159
1620
  }
1160
1621
  async stop(_ctx) {
@@ -1171,7 +1632,7 @@ var ApprovalsServicePlugin = class {
1171
1632
  ApprovalService,
1172
1633
  ApprovalsServicePlugin,
1173
1634
  SysApprovalAction,
1174
- SysApprovalProcess,
1175
- SysApprovalRequest
1635
+ SysApprovalRequest,
1636
+ registerApprovalNode
1176
1637
  });
1177
1638
  //# sourceMappingURL=index.js.map