@gadmin2n/schematics 0.0.142 → 0.0.144

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 (140) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +5 -23
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/page-helpers.ts +4 -2
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +36 -1
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/agenda.seed.ts +14 -14
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/bootstrap.ts +18 -13
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/canvas.seed.ts +14 -14
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +1 -1
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +67 -404
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +16 -40
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +12 -124
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.module.ts +3 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -21
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.ts +3 -10
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/codeRuntime.module.ts +1 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.spec.ts +48 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +96 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.controller.spec.ts +1 -59
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.controller.ts +8 -43
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.dto.ts +0 -23
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction.service.ts +2 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-wait.handler.ts +2 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/externalInteraction.module.ts +7 -3
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +2 -2
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.service.spec.ts +18 -123
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.service.ts +4 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.controller.ts +3 -10
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/httpRequest.module.ts +1 -2
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.controller.ts +0 -10
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +12 -4
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.errors.ts +1 -1
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.external-decision.spec.ts +42 -209
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.ts +108 -225
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/humanApproval.module.ts +0 -2
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.ts +27 -22
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.dto.ts +9 -1
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/runtime-token.guard.spec.ts +30 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/runtime-token.guard.ts +8 -15
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +0 -2
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/runtime-connectivity-probe.service.spec.ts +14 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/runtime-connectivity-probe.service.ts +37 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +41 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +53 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +7 -2
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/workflow-runtime-health.controller.ts +21 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.spec.ts +2 -2
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-callable.spec.ts +16 -88
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-callable.ts +23 -59
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +125 -39
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +95 -28
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-handle-validator.ts +0 -20
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.spec.ts +23 -26
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.ts +6 -52
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +2 -2
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +24 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +26 -21
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/widget-catalog.service.spec.ts +0 -1
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/widget-catalog.service.ts +0 -1
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowRuntimeConfig/workflow-runtime-config.service.spec.ts +32 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowRuntimeConfig/workflow-runtime-config.service.ts +41 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowRuntimeConfig/workflowRuntimeConfig.module.ts +10 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/server/tsconfig.build.json +10 -1
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +16 -12
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -1
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/PageHeader.tsx +1 -1
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/inspectorActions.ts +19 -19
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/contexts/color-mode/index.tsx +1 -1
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +15 -11
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +0 -6
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/AddDataModal.tsx +3 -3
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/AddPageModal.tsx +1 -1
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/DeleteDataConfirm.tsx +1 -1
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/EditDataModal.tsx +1 -1
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +1 -0
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +85 -95
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +85 -95
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +2 -2
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +21 -2
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/game/list.tsx +21 -2
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/human-approval/inbox.tsx +14 -66
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +4 -4
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +3 -3
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +3 -3
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.test.tsx +29 -0
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +4 -2
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +4 -6
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.test.tsx +3 -0
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +3 -1
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +4 -6
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NewNodeDefaults.test.tsx +23 -92
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.tsx +200 -0
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +61 -136
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +96 -195
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeTypeBadge.tsx +36 -0
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/CreateNodeInstanceModal.tsx +5 -10
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +47 -22
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/create.tsx +7 -5
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/edit.tsx +8 -6
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +35 -0
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +1 -6
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApproverSourceEditor.tsx +1 -4
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.tsx +1 -1
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalRequestConfigEditor.tsx +1 -1
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalWaitConfigEditor.tsx +82 -259
  104. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HttpRequestConfigEditor.tsx +1 -2
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +7 -6
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +77 -71
  107. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.test.tsx +10 -57
  108. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.tsx +8 -273
  109. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +54 -0
  110. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +110 -20
  111. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +4 -34
  112. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.test.ts +3 -10
  113. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +2 -37
  114. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/list.tsx +26 -2
  115. package/package.json +1 -1
  116. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.controller.ts +0 -89
  117. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.dto.ts +0 -68
  118. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.service.spec.ts +0 -177
  119. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.service.ts +0 -283
  120. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approvalChannel.module.ts +0 -10
  121. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/ApprovalChannelModal.test.tsx +0 -84
  122. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/ApprovalChannelModal.tsx +0 -141
  123. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/approval-channel.types.ts +0 -16
  124. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/index.tsx +0 -210
  125. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApprovalRequestTriggerConfigEditor.tsx +0 -39
  126. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubmitApprovalResultConfigEditor.tsx +0 -104
  127. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubmitExternalResultConfigEditor.tsx +0 -98
  128. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/AssignRolesModal.tsx +0 -0
  129. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/CreatePageModal.tsx +0 -0
  130. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/EditPageModal.tsx +0 -0
  131. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/PageDetailDrawer.tsx +0 -0
  132. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/PageFormModal.tsx +0 -0
  133. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/CreateModal.tsx +0 -0
  134. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/EditModal.tsx +0 -0
  135. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/ResourceDetailDrawer.tsx +0 -0
  136. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/modal.tsx +0 -0
  137. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/{Components → components}/CreateModal.tsx +0 -0
  138. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/{Components → components}/EditModal.tsx +0 -0
  139. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/{Components → components}/RoleDetailDrawer.tsx +0 -0
  140. /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/{Components → components}/modal.tsx +0 -0
@@ -21,8 +21,6 @@ import { HumanApprovalDomainError } from './human-approval.errors';
21
21
 
22
22
  type SignalResult = 'APPROVED' | 'REJECTED' | 'CANCELLED';
23
23
  type SignalReason = 'RULE_SATISFIED' | 'TIMEOUT' | 'WORKFLOW_CANCELLED';
24
- type DispatchState = 'DISPATCHING' | 'WAITING_APPROVAL' | 'DISPATCH_FAILED';
25
-
26
24
  @Injectable()
27
25
  export class HumanApprovalService {
28
26
  constructor(
@@ -48,23 +46,36 @@ export class HumanApprovalService {
48
46
  where: {
49
47
  workflowInstanceId_nodeId: { workflowInstanceId, nodeId: input.nodeId },
50
48
  },
49
+ include: { candidates: true },
51
50
  });
52
- if (existing) return existing;
53
51
 
54
- const channelRef = input.configSnapshot.channelRef?.trim();
55
- if (channelRef) {
56
- const channel = await this.prisma.approvalChannel.findUnique({
57
- where: { code: channelRef },
58
- select: { isEnabled: true },
52
+ const approvalWorkflowId = input.configSnapshot.approvalWorkflowId?.trim();
53
+ let approvalWorkflow: { isEnabled: boolean; status: string } | null = null;
54
+ if (approvalWorkflowId) {
55
+ approvalWorkflow = await this.prisma.workflow.findUnique({
56
+ where: { id: toBigInt(approvalWorkflowId) },
57
+ select: { isEnabled: true, status: true },
59
58
  });
60
- if (!channel?.isEnabled)
59
+ if (
60
+ !approvalWorkflow?.isEnabled ||
61
+ approvalWorkflow.status !== 'PUBLISHED'
62
+ )
61
63
  throw approvalError(
62
64
  'APPROVAL_CONFIG_MISMATCH',
63
- `Approval channel ${channelRef} was not found or is disabled`,
65
+ `Approval Workflow ${approvalWorkflowId} was not found, published, or enabled`,
64
66
  HttpStatus.BAD_REQUEST,
65
67
  );
66
68
  }
67
69
 
70
+ if (existing) {
71
+ return attachApprovalWorkflowInvocation(
72
+ existing,
73
+ existing.candidates,
74
+ approvalWorkflowId,
75
+ input,
76
+ );
77
+ }
78
+
68
79
  let materializedSubjects;
69
80
  try {
70
81
  materializedSubjects = mergeSubjectSources(
@@ -116,59 +127,40 @@ export class HumanApprovalService {
116
127
  candidates: { create: resolved.candidates },
117
128
  },
118
129
  });
119
- if (channelRef && !blocked) {
130
+ if (approvalWorkflowId && !blocked) {
131
+ if (
132
+ !this.externalInteractions ||
133
+ !input.tenantNamespace ||
134
+ !input.approvalWorkflowRunId ||
135
+ !input.callbackToken ||
136
+ !approvalWorkflow
137
+ )
138
+ throw approvalError(
139
+ 'APPROVAL_CONFIG_MISMATCH',
140
+ 'External approval callback runtime is not configured',
141
+ HttpStatus.BAD_REQUEST,
142
+ );
120
143
  const interaction =
121
- this.externalInteractions &&
122
- input.tenantNamespace &&
123
- input.temporalRunId
124
- ? await this.externalInteractions.createInteractionInTransaction(
125
- {
126
- tenantNamespace: input.tenantNamespace,
127
- handlerType: 'HUMAN_APPROVAL',
128
- handlerConfig: {
129
- approvalRequestId: String(request.id),
130
- allowedOutcomes: ['APPROVE', 'REJECT'],
131
- },
132
- workflowInstanceId: String(workflowInstanceId),
133
- workflowVersionId: String(workflowVersionId),
134
- temporalRunId: input.temporalRunId,
135
- nodeId: input.nodeId,
136
- ...(expiresAt && { expiresAt: expiresAt.toISOString() }),
137
- },
138
- tx,
139
- )
140
- : undefined;
141
- await tx.workflowEventOutbox.create({
142
- data: {
143
- eventName: `approval.requested.${channelRef}`,
144
- payload: {
145
- workflow: input.channelInputs ?? {},
146
- system: {
147
- approval: {
148
- channel: channelRef,
149
- requestId: String(request.id),
150
- candidates: resolved.candidates.map((candidate) => ({
151
- approverId: candidate.principalRef,
152
- displayName: candidate.displayName,
153
- principalType: candidate.principalType,
154
- })),
155
- expiresAt: expiresAt?.toISOString() ?? null,
156
- },
157
- caller: {
158
- workflowInstanceId: String(workflowInstanceId),
159
- workflowVersionId: String(workflowVersionId),
160
- nodeId: input.nodeId,
161
- },
162
- invocation: {
163
- id: `approval:${request.id}:${channelRef}`,
164
- },
165
- ...(interaction && {
166
- callback: { token: interaction.callbackToken },
167
- }),
168
- },
169
- } as Prisma.InputJsonValue,
170
- },
171
- });
144
+ await this.externalInteractions.createInteractionInTransaction(
145
+ {
146
+ callbackToken: input.callbackToken,
147
+ tenantNamespace: input.tenantNamespace,
148
+ handlerType: 'EXTERNAL_WAIT',
149
+ handlerConfig: { allowedOutcomes: ['RECEIVED'] },
150
+ workflowInstanceId: String(workflowInstanceId),
151
+ workflowVersionId: String(workflowVersionId),
152
+ temporalRunId: input.approvalWorkflowRunId,
153
+ nodeId: input.nodeId,
154
+ ...(expiresAt && { expiresAt: expiresAt.toISOString() }),
155
+ },
156
+ tx,
157
+ );
158
+ return attachApprovalWorkflowInvocation(
159
+ request,
160
+ resolved.candidates,
161
+ approvalWorkflowId,
162
+ { ...input, callbackToken: interaction.callbackToken },
163
+ );
172
164
  }
173
165
  return request;
174
166
  });
@@ -228,10 +220,10 @@ export class HumanApprovalService {
228
220
  if (!request) throw notFound(requestId);
229
221
 
230
222
  const snapshot = asObject(request.configSnapshot);
231
- if (snapshot.channelRef !== input.channelRef)
223
+ if (snapshot.approvalWorkflowId !== input.approvalWorkflowId)
232
224
  throw approvalError(
233
- 'APPROVAL_CHANNEL_MISMATCH',
234
- 'Callback channel does not match the channel selected by this approval request',
225
+ 'APPROVAL_WORKFLOW_MISMATCH',
226
+ 'Result Workflow does not match the Workflow selected by this approval request',
235
227
  HttpStatus.FORBIDDEN,
236
228
  );
237
229
 
@@ -246,20 +238,18 @@ export class HumanApprovalService {
246
238
  HttpStatus.FORBIDDEN,
247
239
  );
248
240
 
249
- return (
250
- await this.recordDecision(
251
- tx,
252
- request,
253
- candidate,
254
- input,
255
- `external:${input.channelRef}:${input.approverId}`,
256
- {
257
- ...(input.payload ?? {}),
258
- externalEventId: input.eventId,
259
- approvalChannel: input.channelRef,
260
- },
261
- )
262
- ).decision;
241
+ return this.recordDecision(
242
+ tx,
243
+ request,
244
+ candidate,
245
+ input,
246
+ `external:${input.approvalWorkflowId}:${input.approverId}`,
247
+ {
248
+ ...(input.payload ?? {}),
249
+ externalEventId: input.eventId,
250
+ approvalWorkflowId: input.approvalWorkflowId,
251
+ },
252
+ );
263
253
  });
264
254
  }
265
255
 
@@ -423,24 +413,9 @@ export class HumanApprovalService {
423
413
  },
424
414
  orderBy: { activeFrom: 'desc' },
425
415
  });
426
- return Promise.all(
427
- items.map(async (item) => {
428
- const channelRef = stringValue(
429
- asObject(item.request.configSnapshot).channelRef,
430
- );
431
- return {
432
- ...item,
433
- request: {
434
- ...item.request,
435
- ...(channelRef && {
436
- dispatch: await this.getDispatchStatus(
437
- item.request.id,
438
- channelRef,
439
- ),
440
- }),
441
- },
442
- };
443
- }),
416
+ return items.filter(
417
+ (item) =>
418
+ !stringValue(asObject(item.request.configSnapshot).approvalWorkflowId),
444
419
  );
445
420
  }
446
421
 
@@ -458,44 +433,8 @@ export class HumanApprovalService {
458
433
  updatedAt: true,
459
434
  },
460
435
  });
461
- const { configSnapshot, ...publicRequest } = request;
462
- const channelRef = stringValue(asObject(configSnapshot).channelRef);
463
- return {
464
- ...publicRequest,
465
- ...(channelRef && {
466
- dispatch: await this.getDispatchStatus(requestId, channelRef),
467
- }),
468
- };
469
- }
470
-
471
- async retryDispatch(requestId: bigint) {
472
- const request = await this.getRequest(requestId);
473
- if (request.status !== 'PENDING') throw closed(request.id, request.status);
474
-
475
- const channelRef = stringValue(asObject(request.configSnapshot).channelRef);
476
- if (!channelRef)
477
- throw approvalError(
478
- 'APPROVAL_CONFIG_MISMATCH',
479
- 'This approval request does not use an external channel',
480
- HttpStatus.BAD_REQUEST,
481
- );
482
-
483
- const attempt = await this.findLatestDispatch(requestId, channelRef);
484
- const dispatch = dispatchStatusFrom(attempt);
485
- if (!attempt.outbox || dispatch.state !== 'DISPATCH_FAILED')
486
- throw approvalError(
487
- 'APPROVAL_REQUEST_CLOSED',
488
- 'The approval request can only be resent after dispatch fails',
489
- HttpStatus.CONFLICT,
490
- );
491
-
492
- await this.prisma.workflowEventOutbox.create({
493
- data: {
494
- eventName: `approval.requested.${channelRef}`,
495
- payload: attempt.outbox.payload as Prisma.InputJsonValue,
496
- },
497
- });
498
- return { success: true };
436
+ const { configSnapshot: _configSnapshot, ...publicRequest } = request;
437
+ return publicRequest;
499
438
  }
500
439
 
501
440
  private async finishPending(
@@ -527,33 +466,6 @@ export class HumanApprovalService {
527
466
  return request;
528
467
  }
529
468
 
530
- private async getDispatchStatus(requestId: bigint, channelRef: string) {
531
- return dispatchStatusFrom(
532
- await this.findLatestDispatch(requestId, channelRef),
533
- );
534
- }
535
-
536
- private async findLatestDispatch(requestId: bigint, channelRef: string) {
537
- const outbox = await this.prisma.workflowEventOutbox.findFirst({
538
- where: {
539
- eventName: `approval.requested.${channelRef}`,
540
- payload: {
541
- path: ['requestId'],
542
- equals: String(requestId),
543
- },
544
- },
545
- orderBy: { createdAt: 'desc' },
546
- });
547
- const instance = outbox
548
- ? await this.prisma.workflowInstance.findFirst({
549
- where: { sourceOutboxId: outbox.id },
550
- orderBy: { createdAt: 'desc' },
551
- select: { status: true, result: true },
552
- })
553
- : null;
554
- return { outbox, instance };
555
- }
556
-
557
469
  private async resolveSubjects(
558
470
  configuredSubjects: Array<{ provider: string; refs: string[] }>,
559
471
  actor: ActorContext,
@@ -638,6 +550,40 @@ export class HumanApprovalService {
638
550
  }
639
551
  }
640
552
 
553
+ function attachApprovalWorkflowInvocation(
554
+ request: any,
555
+ candidates: any[],
556
+ approvalWorkflowId: string | undefined,
557
+ input: StartApprovalRequestDto,
558
+ ) {
559
+ if (
560
+ !approvalWorkflowId ||
561
+ request.status !== 'PENDING' ||
562
+ !input.callbackToken
563
+ )
564
+ return request;
565
+ const expiresAt = request.expiresAt
566
+ ? new Date(request.expiresAt).toISOString()
567
+ : null;
568
+ return {
569
+ ...request,
570
+ approvalWorkflowInvocation: {
571
+ workflowId: approvalWorkflowId,
572
+ workflow: {
573
+ ...(input.approvalWorkflowInputs ?? {}),
574
+ requestId: String(request.id),
575
+ callbackToken: input.callbackToken,
576
+ candidates: candidates.map((candidate) => ({
577
+ approverId: candidate.principalRef,
578
+ displayName: candidate.displayName,
579
+ principalType: candidate.principalType,
580
+ })),
581
+ expiresAt,
582
+ },
583
+ },
584
+ };
585
+ }
586
+
641
587
  function candidateKey(value: { principalType: string; principalRef: string }) {
642
588
  return `${value.principalType}\u0000${value.principalRef}`;
643
589
  }
@@ -691,69 +637,6 @@ function stringValue(value: unknown): string | null {
691
637
  return typeof value === 'string' && value.trim() ? value.trim() : null;
692
638
  }
693
639
 
694
- function dispatchStatusFrom(attempt: {
695
- outbox: {
696
- processed: boolean;
697
- lastError: string | null;
698
- } | null;
699
- instance: {
700
- status: string;
701
- result: unknown;
702
- } | null;
703
- }): {
704
- state: DispatchState;
705
- canRetry: boolean;
706
- error?: string;
707
- } {
708
- if (!attempt.outbox)
709
- return {
710
- state: 'DISPATCH_FAILED',
711
- canRetry: false,
712
- error: 'Approval dispatch event was not created',
713
- };
714
-
715
- if (!attempt.outbox.processed)
716
- return {
717
- state: 'DISPATCHING',
718
- canRetry: false,
719
- ...(attempt.outbox.lastError && { error: attempt.outbox.lastError }),
720
- };
721
-
722
- if (attempt.outbox.lastError)
723
- return {
724
- state: 'DISPATCH_FAILED',
725
- canRetry: true,
726
- error: attempt.outbox.lastError,
727
- };
728
-
729
- if (!attempt.instance)
730
- return {
731
- state: 'DISPATCH_FAILED',
732
- canRetry: true,
733
- error: 'Approval dispatch Workflow did not start',
734
- };
735
-
736
- if (attempt.instance.status === 'COMPLETED')
737
- return { state: 'WAITING_APPROVAL', canRetry: false };
738
- if (
739
- attempt.instance.status === 'FAILED' ||
740
- attempt.instance.status === 'CANCELLED'
741
- )
742
- return {
743
- state: 'DISPATCH_FAILED',
744
- canRetry: true,
745
- ...(workflowError(attempt.instance.result) && {
746
- error: workflowError(attempt.instance.result),
747
- }),
748
- };
749
- return { state: 'DISPATCHING', canRetry: false };
750
- }
751
-
752
- function workflowError(result: unknown): string | undefined {
753
- const error = asObject(result).error;
754
- return typeof error === 'string' && error.trim() ? error : undefined;
755
- }
756
-
757
640
  function mergeSubjectSources(
758
641
  ...groups: Array<Array<{ provider: string; refs: string[] }>>
759
642
  ) {
@@ -10,7 +10,6 @@ import { ApprovalSignalOutboxService } from './approval-signal-outbox.service';
10
10
  import { HumanApprovalController } from './human-approval.controller';
11
11
  import { HumanApprovalService } from './human-approval.service';
12
12
  import { HumanApprovalRuntimeController } from './runtime/human-approval-runtime.controller';
13
- import { RuntimeTokenGuard } from './runtime/runtime-token.guard';
14
13
 
15
14
  @Injectable()
16
15
  class HumanApprovalWidgetRegistrar implements OnModuleInit {
@@ -38,7 +37,6 @@ class HumanApprovalWidgetRegistrar implements OnModuleInit {
38
37
  HumanApprovalService,
39
38
  ApprovalSignalOutboxService,
40
39
  HumanApprovalWidgetRegistrar,
41
- RuntimeTokenGuard,
42
40
  HumanApprovalHandler,
43
41
  ],
44
42
  exports: [HumanApprovalService],
@@ -8,9 +8,10 @@ import {
8
8
  Post,
9
9
  UseGuards,
10
10
  } from '@nestjs/common';
11
- import { ConfigService } from '@nestjs/config';
12
11
  import { PrismaService } from 'nestjs-prisma';
13
12
  import { AllowUnauthorizedRequest } from '../../../lib/auth.guard';
13
+ import { WorkflowCallbackUrlService } from '../../externalInteraction/workflow-callback-url.service';
14
+ import { WorkflowRuntimeConfigService } from '../../workflowRuntimeConfig/workflow-runtime-config.service';
14
15
  import { SubmitExternalApprovalDecisionDto } from '../human-approval.dto';
15
16
  import { HumanApprovalService } from '../human-approval.service';
16
17
  import {
@@ -25,21 +26,14 @@ export class HumanApprovalRuntimeController {
25
26
  constructor(
26
27
  private readonly prisma: PrismaService,
27
28
  private readonly approvals: HumanApprovalService,
28
- private readonly config: ConfigService,
29
+ private readonly runtimeConfig: WorkflowRuntimeConfigService,
30
+ private readonly callbackUrls: WorkflowCallbackUrlService,
29
31
  ) {}
30
32
 
31
33
  @Post('start')
32
34
  @AllowUnauthorizedRequest()
33
35
  async start(@Body() input: StartHumanApprovalRuntimeDto) {
34
- const runtime = this.config.get<{ tenantNamespace: string }>(
35
- 'humanApprovalRuntime',
36
- );
37
- if (input.tenantNamespace !== runtime?.tenantNamespace)
38
- throw new BadRequestException({
39
- code: 'WORKFLOW_RUNTIME_TENANT_MISMATCH',
40
- message: 'Runtime tenant does not match this application',
41
- retryable: false,
42
- });
36
+ await this.runtimeConfig.assertTenant(input.tenantNamespace);
43
37
  const instanceId = parseId(input.workflowInstanceId);
44
38
  const versionId = parseId(input.workflowVersionId);
45
39
  const instance = await this.prisma.workflowInstance.findUnique({
@@ -84,19 +78,32 @@ export class HumanApprovalRuntimeController {
84
78
  workflowVersionId: input.workflowVersionId,
85
79
  tenantNamespace: input.tenantNamespace,
86
80
  temporalRunId: input.temporalRunId,
81
+ approvalWorkflowRunId: input.approvalWorkflowRunId,
82
+ callbackToken: input.callbackToken,
87
83
  nodeId: input.nodeId,
88
84
  configHash: node.source.configHash,
89
85
  configSnapshot: node.config,
90
86
  dynamicInputs: input.dynamicInputs,
91
- channelInputs: input.channelInputs,
87
+ approvalWorkflowInputs: input.approvalWorkflowInputs,
92
88
  resolvedSubjects: input.resolvedSubjects,
93
89
  },
94
90
  { userId: `runtime:${input.tenantNamespace}` },
95
91
  );
92
+ const approvalWorkflowInvocation = (request as any)
93
+ .approvalWorkflowInvocation;
96
94
  return {
97
95
  requestId: String(request.id),
98
96
  status: request.status,
99
97
  signalName: 'humanApprovalResolved',
98
+ ...(approvalWorkflowInvocation && {
99
+ approvalWorkflowInvocation: {
100
+ ...approvalWorkflowInvocation,
101
+ workflow: {
102
+ ...approvalWorkflowInvocation.workflow,
103
+ callbackUrl: await this.callbackUrls.resolveExternalCallback(),
104
+ },
105
+ },
106
+ }),
100
107
  };
101
108
  }
102
109
 
@@ -106,18 +113,16 @@ export class HumanApprovalRuntimeController {
106
113
  return this.approvals.status(parseId(requestId));
107
114
  }
108
115
 
116
+ @Post(':requestId/expire')
117
+ @AllowUnauthorizedRequest()
118
+ expire(@Param('requestId') requestId: string) {
119
+ return this.approvals.expireRequest(parseId(requestId));
120
+ }
121
+
109
122
  @Post('external-decisions')
110
123
  @AllowUnauthorizedRequest()
111
- submitApprovalResult(@Body() input: SubmitApprovalResultRuntimeDto) {
112
- const runtime = this.config.get<{ tenantNamespace: string }>(
113
- 'humanApprovalRuntime',
114
- );
115
- if (input.tenantNamespace !== runtime?.tenantNamespace)
116
- throw new BadRequestException({
117
- code: 'WORKFLOW_RUNTIME_TENANT_MISMATCH',
118
- message: 'Runtime tenant does not match this application',
119
- retryable: false,
120
- });
124
+ async submitApprovalResult(@Body() input: SubmitApprovalResultRuntimeDto) {
125
+ await this.runtimeConfig.assertTenant(input.tenantNamespace);
121
126
  const {
122
127
  requestId,
123
128
  tenantNamespace: _tenantNamespace,
@@ -48,7 +48,7 @@ export class StartHumanApprovalRuntimeDto {
48
48
 
49
49
  @IsOptional()
50
50
  @IsObject()
51
- channelInputs?: Record<string, unknown>;
51
+ approvalWorkflowInputs?: Record<string, unknown>;
52
52
 
53
53
  @IsOptional()
54
54
  @IsArray()
@@ -59,6 +59,14 @@ export class StartHumanApprovalRuntimeDto {
59
59
  @IsString()
60
60
  @IsNotEmpty()
61
61
  temporalRunId: string;
62
+
63
+ @IsString()
64
+ @IsNotEmpty()
65
+ approvalWorkflowRunId: string;
66
+
67
+ @IsString()
68
+ @IsNotEmpty()
69
+ callbackToken: string;
62
70
  }
63
71
 
64
72
  export class SubmitApprovalResultRuntimeDto extends SubmitExternalApprovalDecisionDto {
@@ -0,0 +1,30 @@
1
+ import { RuntimeTokenGuard } from './runtime-token.guard';
2
+
3
+ function context(token: string) {
4
+ return {
5
+ switchToHttp: () => ({
6
+ getRequest: () => ({
7
+ headers: { 'x-workflow-runtime-token': token },
8
+ }),
9
+ }),
10
+ } as any;
11
+ }
12
+
13
+ describe('RuntimeTokenGuard', () => {
14
+ it('authenticates with the token stored during service registration', async () => {
15
+ const guard = new RuntimeTokenGuard({
16
+ getRequired: jest.fn().mockResolvedValue({
17
+ runtimeToken: 'registered-runtime-token',
18
+ }),
19
+ } as any);
20
+
21
+ await expect(
22
+ guard.canActivate(context('registered-runtime-token')),
23
+ ).resolves.toBe(true);
24
+ await expect(
25
+ guard.canActivate(context('old-environment-token')),
26
+ ).rejects.toMatchObject({
27
+ response: { code: 'WORKFLOW_RUNTIME_UNAUTHORIZED' },
28
+ });
29
+ });
30
+ });
@@ -2,31 +2,24 @@ import {
2
2
  CanActivate,
3
3
  ExecutionContext,
4
4
  Injectable,
5
- ServiceUnavailableException,
6
5
  UnauthorizedException,
7
6
  } from '@nestjs/common';
8
- import { ConfigService } from '@nestjs/config';
9
7
  import { timingSafeEqual } from 'crypto';
8
+ import { WorkflowRuntimeConfigService } from '../../workflowRuntimeConfig/workflow-runtime-config.service';
10
9
 
11
10
  @Injectable()
12
11
  export class RuntimeTokenGuard implements CanActivate {
13
- constructor(private readonly config: ConfigService) {}
12
+ constructor(private readonly runtimeConfig: WorkflowRuntimeConfigService) {}
14
13
 
15
- canActivate(context: ExecutionContext): boolean {
16
- const runtime = this.config.get<{
17
- enabled: boolean;
18
- token: string;
19
- }>('humanApprovalRuntime');
20
- if (!runtime?.enabled)
21
- throw new ServiceUnavailableException({
22
- code: 'WORKFLOW_RUNTIME_DISABLED',
23
- message: 'Workflow runtime is disabled',
24
- retryable: false,
25
- });
14
+ async canActivate(context: ExecutionContext): Promise<boolean> {
15
+ const runtime = await this.runtimeConfig.getRequired();
26
16
  const supplied = context.switchToHttp().getRequest().headers[
27
17
  'x-workflow-runtime-token'
28
18
  ];
29
- if (typeof supplied !== 'string' || !safeEqual(supplied, runtime.token))
19
+ if (
20
+ typeof supplied !== 'string' ||
21
+ !safeEqual(supplied, runtime.runtimeToken)
22
+ )
30
23
  throw new UnauthorizedException({
31
24
  code: 'WORKFLOW_RUNTIME_UNAUTHORIZED',
32
25
  message: 'Invalid workflow runtime token',
@@ -1,5 +1,4 @@
1
1
  import generatedModules from '../generated/modules.index';
2
- import { ApprovalChannelModule } from './approvalChannel/approvalChannel.module';
3
2
  import { CanvasModule } from './canvas/canvas.module';
4
3
  import { CodeRuntimeModule } from './codeRuntime/codeRuntime.module';
5
4
  import { ExternalInteractionModule } from './externalInteraction/externalInteraction.module';
@@ -12,7 +11,6 @@ import { WorkflowModule } from './workflow/workflow.module';
12
11
 
13
12
  export default [
14
13
  ...generatedModules,
15
- ApprovalChannelModule,
16
14
  CanvasModule,
17
15
  CodeRuntimeModule,
18
16
  ExternalInteractionModule,
@@ -0,0 +1,14 @@
1
+ import { RuntimeConnectivityProbeService } from './runtime-connectivity-probe.service';
2
+
3
+ describe('RuntimeConnectivityProbeService', () => {
4
+ it('creates a short-lived signed probe without using the runtime token', () => {
5
+ const service = new RuntimeConnectivityProbeService({
6
+ get: jest.fn().mockReturnValue('integration-secret'),
7
+ } as any);
8
+
9
+ const probe = service.create();
10
+
11
+ expect(service.verify(probe)).toBe(true);
12
+ expect(service.verify(`${probe}tampered`)).toBe(false);
13
+ });
14
+ });
@@ -0,0 +1,37 @@
1
+ import { Injectable, ServiceUnavailableException } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
4
+
5
+ @Injectable()
6
+ export class RuntimeConnectivityProbeService {
7
+ constructor(private readonly config: ConfigService) {}
8
+
9
+ create(): string {
10
+ const expiresAt = Date.now() + 30_000;
11
+ const payload = `${expiresAt}.${randomUUID()}`;
12
+ return `${payload}.${this.sign(payload)}`;
13
+ }
14
+
15
+ verify(token: string): boolean {
16
+ const separator = token.lastIndexOf('.');
17
+ if (separator < 1) return false;
18
+ const payload = token.slice(0, separator);
19
+ const signature = token.slice(separator + 1);
20
+ const expiresAt = Number(payload.slice(0, payload.indexOf('.')));
21
+ if (!Number.isFinite(expiresAt) || expiresAt < Date.now()) return false;
22
+ const expected = Buffer.from(this.sign(payload));
23
+ const supplied = Buffer.from(signature);
24
+ return (
25
+ expected.length === supplied.length && timingSafeEqual(expected, supplied)
26
+ );
27
+ }
28
+
29
+ private sign(payload: string): string {
30
+ const secret = this.config.get<string>('integration.secretKey')?.trim();
31
+ if (!secret)
32
+ throw new ServiceUnavailableException(
33
+ 'INTEGRATION_SECRET_KEY is required for connectivity testing',
34
+ );
35
+ return createHmac('sha256', secret).update(payload).digest('base64url');
36
+ }
37
+ }