@gadmin2n/schematics 0.0.141 → 0.0.143
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/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +5 -23
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/page-helpers.ts +4 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +36 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/agenda.seed.ts +14 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/bootstrap.ts +18 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/canvas.seed.ts +14 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +67 -404
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +16 -40
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +12 -124
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.module.ts +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -21
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.ts +3 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/codeRuntime.module.ts +1 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.spec.ts +48 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +96 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.controller.spec.ts +1 -59
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.controller.ts +8 -43
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction-runtime.dto.ts +0 -23
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-interaction.service.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-wait.handler.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/externalInteraction.module.ts +7 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.service.spec.ts +18 -123
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.service.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/http-request-runtime.controller.ts +3 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/httpRequest/httpRequest.module.ts +1 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.controller.ts +0 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +12 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.errors.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.external-decision.spec.ts +42 -209
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.ts +108 -225
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/humanApproval.module.ts +0 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.ts +27 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.dto.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/runtime-token.guard.spec.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/runtime-token.guard.ts +8 -15
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +0 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/runtime-connectivity-probe.service.spec.ts +14 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/runtime-connectivity-probe.service.ts +37 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +41 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +53 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +7 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/workflow-runtime-health.controller.ts +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.spec.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-callable.spec.ts +16 -88
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-callable.ts +23 -59
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +125 -39
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +95 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-handle-validator.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.spec.ts +23 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-http-response.ts +6 -52
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +26 -21
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/widget-catalog.service.spec.ts +0 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/widget-catalog.service.ts +0 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowRuntimeConfig/workflow-runtime-config.service.spec.ts +32 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowRuntimeConfig/workflow-runtime-config.service.ts +41 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowRuntimeConfig/workflowRuntimeConfig.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/tsconfig.build.json +10 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +16 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/PageHeader.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/inspectorActions.ts +19 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/contexts/color-mode/index.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +15 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/AddDataModal.tsx +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/AddPageModal.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/DeleteDataConfirm.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/EditDataModal.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +85 -95
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +85 -95
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +21 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/game/list.tsx +21 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/human-approval/inbox.tsx +14 -66
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.test.tsx +29 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +4 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +4 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.test.tsx +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +4 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NewNodeDefaults.test.tsx +23 -92
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.tsx +200 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +61 -136
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +96 -195
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeTypeBadge.tsx +36 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/CreateNodeInstanceModal.tsx +5 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +47 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/create.tsx +7 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/edit.tsx +8 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +35 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +1 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApproverSourceEditor.tsx +1 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalRequestConfigEditor.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ExternalWaitConfigEditor.tsx +82 -259
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HttpRequestConfigEditor.tsx +1 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +7 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +77 -71
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.test.tsx +10 -57
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ReturnResultConfigEditor.tsx +8 -273
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +110 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +4 -34
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.test.ts +3 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +2 -37
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/list.tsx +26 -2
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.controller.ts +0 -89
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.dto.ts +0 -68
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.service.spec.ts +0 -177
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approval-channel.service.ts +0 -283
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/approvalChannel/approvalChannel.module.ts +0 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/ApprovalChannelModal.test.tsx +0 -84
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/ApprovalChannelModal.tsx +0 -141
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/approval-channel.types.ts +0 -16
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/approval-channels/index.tsx +0 -210
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/ApprovalRequestTriggerConfigEditor.tsx +0 -39
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubmitApprovalResultConfigEditor.tsx +0 -104
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubmitExternalResultConfigEditor.tsx +0 -98
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/AssignRolesModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/CreatePageModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/EditPageModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/PageDetailDrawer.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/{Components → components}/PageFormModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/CreateModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/EditModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/ResourceDetailDrawer.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/{Components → components}/modal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/{Components → components}/CreateModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/{Components → components}/EditModal.tsx +0 -0
- /package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/{Components → components}/RoleDetailDrawer.tsx +0 -0
- /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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 (
|
|
59
|
+
if (
|
|
60
|
+
!approvalWorkflow?.isEnabled ||
|
|
61
|
+
approvalWorkflow.status !== 'PUBLISHED'
|
|
62
|
+
)
|
|
61
63
|
throw approvalError(
|
|
62
64
|
'APPROVAL_CONFIG_MISMATCH',
|
|
63
|
-
`Approval
|
|
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 (
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
223
|
+
if (snapshot.approvalWorkflowId !== input.approvalWorkflowId)
|
|
232
224
|
throw approvalError(
|
|
233
|
-
'
|
|
234
|
-
'
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
{
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12
|
+
constructor(private readonly runtimeConfig: WorkflowRuntimeConfigService) {}
|
|
14
13
|
|
|
15
|
-
canActivate(context: ExecutionContext): boolean {
|
|
16
|
-
const runtime = this.
|
|
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 (
|
|
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
|
+
}
|