@hexabot-ai/api 3.2.7-beta.0 → 3.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/channel.module.js +2 -0
- package/dist/channel/channel.module.js.map +1 -1
- package/dist/channel/entities/source.entity.d.ts +38 -0
- package/dist/channel/webhook.controller.d.ts +4 -1
- package/dist/channel/webhook.controller.js +19 -1
- package/dist/channel/webhook.controller.js.map +1 -1
- package/dist/chat/services/chat.service.js +4 -2
- package/dist/chat/services/chat.service.js.map +1 -1
- package/dist/chat/services/thread.service.js +6 -2
- package/dist/chat/services/thread.service.js.map +1 -1
- package/dist/mcp/tools/hexabot-mcp-tool.base.d.ts +0 -3
- package/dist/mcp/tools/hexabot-mcp-tool.base.js +0 -6
- package/dist/mcp/tools/hexabot-mcp-tool.base.js.map +1 -1
- package/dist/mcp/tools/hexabot-mcp.utils.d.ts +3 -0
- package/dist/mcp/tools/hexabot-mcp.utils.js +8 -1
- package/dist/mcp/tools/hexabot-mcp.utils.js.map +1 -1
- package/dist/mcp/tools/workflow-mcp.helper.d.ts +19 -1
- package/dist/mcp/tools/workflow-mcp.helper.js +2 -7
- package/dist/mcp/tools/workflow-mcp.helper.js.map +1 -1
- package/dist/mcp/tools/workflow-mcp.tools.d.ts +114 -0
- package/dist/mcp/tools/workflow-run-mcp.tools.d.ts +38 -0
- package/dist/mcp/tools/workflow-run-mcp.tools.js +1 -1
- package/dist/mcp/tools/workflow-run-mcp.tools.js.map +1 -1
- package/dist/mcp/tools/workflow-version-mcp.tools.d.ts +0 -22
- package/dist/mcp/tools/workflow-version-mcp.tools.js +3 -24
- package/dist/mcp/tools/workflow-version-mcp.tools.js.map +1 -1
- package/dist/static/assets/{cssMode-v8DOzA4o.js → cssMode-CVfTJuzy.js} +1 -1
- package/dist/static/assets/{freemarker2-B92dv90I.js → freemarker2-BA7GKqB7.js} +1 -1
- package/dist/static/assets/{handlebars-Dw3f_475.js → handlebars-BCwvKzIX.js} +1 -1
- package/dist/static/assets/{html-BLTdldWn.js → html-f6_JN1YY.js} +1 -1
- package/dist/static/assets/{htmlMode-tP1xHfwv.js → htmlMode-f4asMwRs.js} +1 -1
- package/dist/static/assets/index-CLS5CNoN.css +1 -0
- package/dist/static/assets/{index-1C1RUhvE.js → index-X6nbc-0C.js} +399 -392
- package/dist/static/assets/{javascript-D6zFkYDA.js → javascript-DaS6vtZ9.js} +1 -1
- package/dist/static/assets/{jsonMode-Casc9QAu.js → jsonMode-Ct41J50t.js} +1 -1
- package/dist/static/assets/{liquid-CiiGxWfs.js → liquid-4giXfRN2.js} +1 -1
- package/dist/static/assets/{lspLanguageFeatures-DDa9NCMy.js → lspLanguageFeatures-CqJlQWTJ.js} +1 -1
- package/dist/static/assets/{mdx-CkcXhfmV.js → mdx-DcO2npTr.js} +1 -1
- package/dist/static/assets/{python-CBPYdNzT.js → python-D7O1gNbd.js} +1 -1
- package/dist/static/assets/{razor-BU2J-T3U.js → razor-K5r5YQ4D.js} +1 -1
- package/dist/static/assets/{tsMode-CMXRM61t.js → tsMode-CvIEOKtw.js} +1 -1
- package/dist/static/assets/{typescript-GDvJ0GIf.js → typescript-BKjAKJn9.js} +1 -1
- package/dist/static/assets/{xml-CSlsq1iC.js → xml-BpIa2Zv1.js} +1 -1
- package/dist/static/assets/{yaml-stKXP2Du.js → yaml-BJzMbZhP.js} +1 -1
- package/dist/static/index.html +2 -2
- package/dist/static/locales/en/translation.json +75 -2
- package/dist/static/locales/fr/translation.json +72 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/user/services/passwordReset.service.js +2 -1
- package/dist/user/services/passwordReset.service.js.map +1 -1
- package/dist/workflow/controllers/workflow-version.controller.d.ts +38 -0
- package/dist/workflow/controllers/workflow.controller.d.ts +43 -6
- package/dist/workflow/controllers/workflow.controller.js +23 -13
- package/dist/workflow/controllers/workflow.controller.js.map +1 -1
- package/dist/workflow/dto/workflow.dto.d.ts +40 -1
- package/dist/workflow/dto/workflow.dto.js +10 -0
- package/dist/workflow/dto/workflow.dto.js.map +1 -1
- package/dist/workflow/entities/memory-record.entity.d.ts +38 -0
- package/dist/workflow/entities/workflow-run.entity.d.ts +38 -0
- package/dist/workflow/entities/workflow-version.entity.d.ts +38 -0
- package/dist/workflow/entities/workflow.entity.d.ts +78 -0
- package/dist/workflow/entities/workflow.entity.js +4 -0
- package/dist/workflow/entities/workflow.entity.js.map +1 -1
- package/dist/workflow/guards/webhook-trigger.guard.d.ts +17 -0
- package/dist/workflow/guards/webhook-trigger.guard.js +139 -0
- package/dist/workflow/guards/webhook-trigger.guard.js.map +1 -0
- package/dist/workflow/services/agentic.service.d.ts +14 -2
- package/dist/workflow/services/agentic.service.js +17 -4
- package/dist/workflow/services/agentic.service.js.map +1 -1
- package/dist/workflow/services/webhook-trigger.service.d.ts +27 -0
- package/dist/workflow/services/webhook-trigger.service.js +90 -0
- package/dist/workflow/services/webhook-trigger.service.js.map +1 -0
- package/dist/workflow/services/workflow-run.service.d.ts +1 -0
- package/dist/workflow/services/workflow-run.service.js +9 -0
- package/dist/workflow/services/workflow-run.service.js.map +1 -1
- package/dist/workflow/workflow.module.js +8 -0
- package/dist/workflow/workflow.module.js.map +1 -1
- package/package.json +5 -6
- package/src/channel/README.md +17 -0
- package/src/channel/channel.module.ts +4 -0
- package/src/channel/webhook.controller.ts +46 -1
- package/src/chat/services/chat.service.ts +5 -4
- package/src/chat/services/thread.service.ts +10 -2
- package/src/mcp/README.md +2 -3
- package/src/mcp/tools/hexabot-mcp-tool.base.ts +0 -10
- package/src/mcp/tools/hexabot-mcp.utils.ts +10 -0
- package/src/mcp/tools/workflow-mcp.helper.ts +3 -11
- package/src/mcp/tools/workflow-run-mcp.tools.ts +2 -2
- package/src/mcp/tools/workflow-version-mcp.tools.ts +3 -29
- package/src/user/services/passwordReset.service.ts +9 -2
- package/src/workflow/controllers/workflow.controller.ts +36 -10
- package/src/workflow/dto/workflow.dto.ts +12 -0
- package/src/workflow/entities/workflow.entity.ts +11 -1
- package/src/workflow/guards/webhook-trigger.guard.ts +193 -0
- package/src/workflow/services/agentic.service.ts +27 -3
- package/src/workflow/services/webhook-trigger.service.ts +169 -0
- package/src/workflow/services/workflow-run.service.ts +21 -0
- package/src/workflow/workflow.module.ts +8 -0
- package/dist/static/assets/index-DyBPx3f5.css +0 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
import { WebhookAuthType, WebhookTriggerConfig } from '@hexabot-ai/types';
|
|
10
|
+
import {
|
|
11
|
+
CanActivate,
|
|
12
|
+
ExecutionContext,
|
|
13
|
+
HttpStatus,
|
|
14
|
+
Injectable,
|
|
15
|
+
NotFoundException,
|
|
16
|
+
ParseUUIDPipe,
|
|
17
|
+
UnauthorizedException,
|
|
18
|
+
} from '@nestjs/common';
|
|
19
|
+
import { JwtService } from '@nestjs/jwt';
|
|
20
|
+
import { Request } from 'express';
|
|
21
|
+
|
|
22
|
+
import { LoggerService } from '@/logger/logger.service';
|
|
23
|
+
import { CredentialService } from '@/user/services/credential.service';
|
|
24
|
+
|
|
25
|
+
import { WorkflowService } from '../services/workflow.service';
|
|
26
|
+
import { WorkflowType } from '../types';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Guards the public webhook trigger endpoint. It resolves the target workflow,
|
|
30
|
+
* enforces that it is a manual workflow with an enabled webhook, and verifies
|
|
31
|
+
* the configured credentials. The handler re-resolves the workflow rather than
|
|
32
|
+
* receiving it through mutated request state.
|
|
33
|
+
*/
|
|
34
|
+
@Injectable()
|
|
35
|
+
export class WebhookTriggerGuard implements CanActivate {
|
|
36
|
+
private readonly uuidPipe = new ParseUUIDPipe({
|
|
37
|
+
version: '4',
|
|
38
|
+
errorHttpStatusCode: HttpStatus.NOT_FOUND,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly workflowService: WorkflowService,
|
|
43
|
+
private readonly credentialService: CredentialService,
|
|
44
|
+
private readonly jwtService: JwtService,
|
|
45
|
+
private readonly logger: LoggerService,
|
|
46
|
+
) {}
|
|
47
|
+
|
|
48
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
49
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
50
|
+
const id = await this.uuidPipe.transform(request.params.id, {
|
|
51
|
+
type: 'param',
|
|
52
|
+
data: 'id',
|
|
53
|
+
});
|
|
54
|
+
const workflow = await this.workflowService.findOne(id);
|
|
55
|
+
if (!workflow) {
|
|
56
|
+
throw new NotFoundException(`Workflow with ID ${id} not found`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Do not disclose whether the workflow exists when it is not exposed as a
|
|
60
|
+
// webhook (wrong type or webhook disabled).
|
|
61
|
+
if (workflow.type !== WorkflowType.manual) {
|
|
62
|
+
throw new NotFoundException(`Workflow with ID ${id} not found`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const webhook = workflow.webhookTrigger;
|
|
66
|
+
if (!webhook?.enabled) {
|
|
67
|
+
throw new NotFoundException(`Workflow with ID ${id} not found`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await this.verifyWebhookAuth(webhook, request);
|
|
71
|
+
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the secret referenced by the trigger config from the credentials
|
|
77
|
+
* store. A missing reference or a dangling credential must never
|
|
78
|
+
* authenticate: both fail closed with a generic 401, with a server-side
|
|
79
|
+
* warning so operators can diagnose broken references.
|
|
80
|
+
*/
|
|
81
|
+
private async resolveSecret(
|
|
82
|
+
credentialId: string | null | undefined,
|
|
83
|
+
): Promise<string> {
|
|
84
|
+
if (!credentialId) {
|
|
85
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const value = await this.credentialService.findOneValue(credentialId);
|
|
89
|
+
if (!value) {
|
|
90
|
+
this.logger.warn(
|
|
91
|
+
`Webhook trigger references a missing credential (${credentialId})`,
|
|
92
|
+
);
|
|
93
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates incoming webhook credentials against the workflow configuration.
|
|
101
|
+
* Throws {@link UnauthorizedException} when the credentials are missing or invalid.
|
|
102
|
+
*/
|
|
103
|
+
private async verifyWebhookAuth(
|
|
104
|
+
webhook: WebhookTriggerConfig,
|
|
105
|
+
req: Request,
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
switch (webhook.authType) {
|
|
108
|
+
case WebhookAuthType.none:
|
|
109
|
+
return;
|
|
110
|
+
|
|
111
|
+
case WebhookAuthType.basic: {
|
|
112
|
+
// A missing expected secret must never authenticate empty credentials.
|
|
113
|
+
if (!webhook.username) {
|
|
114
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
115
|
+
}
|
|
116
|
+
const expectedPassword = await this.resolveSecret(
|
|
117
|
+
webhook.passwordCredentialId,
|
|
118
|
+
);
|
|
119
|
+
const header = req.headers.authorization ?? '';
|
|
120
|
+
const match = header.match(/^Basic\s+(\S+)$/i);
|
|
121
|
+
if (!match) {
|
|
122
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
123
|
+
}
|
|
124
|
+
const decoded = Buffer.from(match[1], 'base64').toString('utf8');
|
|
125
|
+
const separator = decoded.indexOf(':');
|
|
126
|
+
const username =
|
|
127
|
+
separator === -1 ? decoded : decoded.slice(0, separator);
|
|
128
|
+
const password = separator === -1 ? '' : decoded.slice(separator + 1);
|
|
129
|
+
if (
|
|
130
|
+
!this.safeEqual(username, webhook.username) ||
|
|
131
|
+
!this.safeEqual(password, expectedPassword)
|
|
132
|
+
) {
|
|
133
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case WebhookAuthType.header: {
|
|
140
|
+
const headerName = (webhook.headerName ?? '').toLowerCase();
|
|
141
|
+
// A missing expected secret must never authenticate empty credentials.
|
|
142
|
+
if (!headerName) {
|
|
143
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
144
|
+
}
|
|
145
|
+
const expectedValue = await this.resolveSecret(
|
|
146
|
+
webhook.headerValueCredentialId,
|
|
147
|
+
);
|
|
148
|
+
const provided = req.headers[headerName];
|
|
149
|
+
const value = Array.isArray(provided) ? provided[0] : (provided ?? '');
|
|
150
|
+
if (!this.safeEqual(value, expectedValue)) {
|
|
151
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case WebhookAuthType.jwt: {
|
|
158
|
+
const secret = await this.resolveSecret(webhook.jwtSecretCredentialId);
|
|
159
|
+
const header = req.headers.authorization ?? '';
|
|
160
|
+
const match = header.match(/^Bearer\s+(\S+)$/i);
|
|
161
|
+
if (!match) {
|
|
162
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
this.jwtService.verify(match[1], {
|
|
166
|
+
secret,
|
|
167
|
+
algorithms: [webhook.jwtAlgorithm ?? 'HS256'],
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
throw new UnauthorizedException('Invalid webhook credentials');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Constant-time string comparison to avoid leaking secrets through timing.
|
|
183
|
+
*/
|
|
184
|
+
private safeEqual(a: string, b: string): boolean {
|
|
185
|
+
const bufferA = Buffer.from(a, 'utf8');
|
|
186
|
+
const bufferB = Buffer.from(b, 'utf8');
|
|
187
|
+
if (bufferA.length !== bufferB.length) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return timingSafeEqual(bufferA, bufferB);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from '@hexabot-ai/agentic';
|
|
14
14
|
import { WorkflowFull, WorkflowRunFull } from '@hexabot-ai/types';
|
|
15
15
|
import { Injectable } from '@nestjs/common';
|
|
16
|
+
import { OnEvent } from '@nestjs/event-emitter';
|
|
16
17
|
|
|
17
18
|
import { ActionService } from '@/actions/actions.service';
|
|
18
19
|
import { RuntimeBindingsService } from '@/bindings/runtime-bindings.service';
|
|
@@ -57,9 +58,14 @@ export class AgenticService implements WorkflowCallService {
|
|
|
57
58
|
/**
|
|
58
59
|
* Process an event by resuming a suspended workflow run if it exists,
|
|
59
60
|
* otherwise start a new run using the latest configured workflow.
|
|
61
|
+
*
|
|
62
|
+
* Callers that already resolved the target workflow (e.g. the webhook
|
|
63
|
+
* trigger path) can pass it through `options.workflow` to avoid a redundant
|
|
64
|
+
* lookup.
|
|
60
65
|
*/
|
|
61
66
|
async handleEvent(
|
|
62
67
|
event: TriggerEventWrapper,
|
|
68
|
+
options: { workflow?: WorkflowFull } = {},
|
|
63
69
|
): Promise<WorkflowRunFull | null> {
|
|
64
70
|
const initiator = event.getInitiator();
|
|
65
71
|
const requestedWorkflowId = event.getWorkflowId();
|
|
@@ -98,9 +104,11 @@ export class AgenticService implements WorkflowCallService {
|
|
|
98
104
|
return execution.run;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
const workflowToRun =
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
const workflowToRun =
|
|
108
|
+
options.workflow ??
|
|
109
|
+
(requestedWorkflowId
|
|
110
|
+
? await this.workflowService.findOneAndPopulate(requestedWorkflowId)
|
|
111
|
+
: await this.workflowService.pickWorkflow());
|
|
104
112
|
if (!workflowToRun) {
|
|
105
113
|
this.logger.warn('No workflow available to handle incoming event', {
|
|
106
114
|
requestedWorkflowId: requestedWorkflowId ?? null,
|
|
@@ -761,4 +769,20 @@ export class AgenticService implements WorkflowCallService {
|
|
|
761
769
|
|
|
762
770
|
return typeof error === 'string' ? error : JSON.stringify(error);
|
|
763
771
|
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Abort all active workflow runs linked to a thread when that thread is closed.
|
|
775
|
+
*/
|
|
776
|
+
@OnEvent('hook:thread:postUpdate')
|
|
777
|
+
async handleThreadClosed({
|
|
778
|
+
entity,
|
|
779
|
+
databaseEntity,
|
|
780
|
+
}: {
|
|
781
|
+
entity: { id: string; status: string };
|
|
782
|
+
databaseEntity: { id: string; status: string };
|
|
783
|
+
}): Promise<void> {
|
|
784
|
+
if (entity.status === 'closed' && databaseEntity.status !== 'closed') {
|
|
785
|
+
await this.workflowRunService.abortActiveRunsForThread(entity.id);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
764
788
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
WebhookAuthType,
|
|
9
|
+
WorkflowFull,
|
|
10
|
+
WorkflowRun,
|
|
11
|
+
WorkflowRunFull,
|
|
12
|
+
} from '@hexabot-ai/types';
|
|
13
|
+
import {
|
|
14
|
+
BadRequestException,
|
|
15
|
+
Injectable,
|
|
16
|
+
NotFoundException,
|
|
17
|
+
UnprocessableEntityException,
|
|
18
|
+
} from '@nestjs/common';
|
|
19
|
+
import { JwtService } from '@nestjs/jwt';
|
|
20
|
+
|
|
21
|
+
import { CredentialService } from '@/user/services/credential.service';
|
|
22
|
+
import { UserService } from '@/user/services/user.service';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
ManualEventWrapper,
|
|
26
|
+
TriggerEventWrapper,
|
|
27
|
+
} from '../lib/trigger-event-wrapper';
|
|
28
|
+
import { WorkflowType } from '../types';
|
|
29
|
+
|
|
30
|
+
import { AgenticService } from './agentic.service';
|
|
31
|
+
import { WorkflowService } from './workflow.service';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Outcome of a triggered workflow run, exposed to webhook callers. Execution
|
|
35
|
+
* is synchronous, so the final status and workflow output are available.
|
|
36
|
+
*/
|
|
37
|
+
export type WorkflowTriggerResult = {
|
|
38
|
+
runId: string;
|
|
39
|
+
status: WorkflowRun['status'];
|
|
40
|
+
output: Record<string, unknown> | null;
|
|
41
|
+
error: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A server-issued webhook trigger token. Tokens carry no expiry: they stay
|
|
46
|
+
* valid until the workflow's signing secret credential is rotated.
|
|
47
|
+
*/
|
|
48
|
+
export type WebhookTokenResult = {
|
|
49
|
+
token: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
@Injectable()
|
|
53
|
+
export class WebhookTriggerService {
|
|
54
|
+
constructor(
|
|
55
|
+
private readonly workflowService: WorkflowService,
|
|
56
|
+
private readonly userService: UserService,
|
|
57
|
+
private readonly agenticService: AgenticService,
|
|
58
|
+
private readonly credentialService: CredentialService,
|
|
59
|
+
private readonly jwtService: JwtService,
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Signs a webhook trigger token with the workflow's own JWT secret so
|
|
64
|
+
* callers never have to craft tokens themselves. Tokens carry no expiry;
|
|
65
|
+
* rotating the secret credential invalidates every previously issued
|
|
66
|
+
* token. The workflow must be a manual workflow with an enabled,
|
|
67
|
+
* JWT-authenticated webhook trigger.
|
|
68
|
+
*
|
|
69
|
+
* @param id - The workflow ID the token is scoped to.
|
|
70
|
+
*/
|
|
71
|
+
async generateToken(id: string): Promise<WebhookTokenResult> {
|
|
72
|
+
const workflow = await this.workflowService.findOne(id);
|
|
73
|
+
if (!workflow) {
|
|
74
|
+
throw new NotFoundException(`Workflow with ID ${id} not found`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const webhook = workflow.webhookTrigger;
|
|
78
|
+
if (
|
|
79
|
+
workflow.type !== WorkflowType.manual ||
|
|
80
|
+
!webhook?.enabled ||
|
|
81
|
+
webhook.authType !== WebhookAuthType.jwt
|
|
82
|
+
) {
|
|
83
|
+
throw new BadRequestException(
|
|
84
|
+
'Workflow must have an enabled JWT-authenticated webhook trigger',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const secret = await this.credentialService.findOneValue(
|
|
89
|
+
webhook.jwtSecretCredentialId ?? undefined,
|
|
90
|
+
);
|
|
91
|
+
if (!secret) {
|
|
92
|
+
throw new BadRequestException(
|
|
93
|
+
'The webhook trigger references a missing JWT secret credential',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const token = this.jwtService.sign(
|
|
98
|
+
{ sub: id },
|
|
99
|
+
{
|
|
100
|
+
secret,
|
|
101
|
+
algorithm: webhook.jwtAlgorithm ?? 'HS256',
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return { token };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Executes a manual workflow run from a webhook.
|
|
110
|
+
*
|
|
111
|
+
* Access control (existence, manual type, enabled webhook, credentials) is
|
|
112
|
+
* enforced upstream by {@link WebhookTriggerGuard}; this method resolves the
|
|
113
|
+
* workflow, validates the payload, and dispatches the event. The workflow
|
|
114
|
+
* runs to completion before responding, so the result carries the final run
|
|
115
|
+
* status and output.
|
|
116
|
+
*
|
|
117
|
+
* @param id - The workflow ID to execute.
|
|
118
|
+
* @param input - Optional workflow input payload.
|
|
119
|
+
*/
|
|
120
|
+
async trigger(id: string, input: unknown): Promise<WorkflowTriggerResult> {
|
|
121
|
+
const workflow = await this.workflowService.findOneAndPopulate(id);
|
|
122
|
+
if (!workflow) {
|
|
123
|
+
throw new NotFoundException(`Workflow with ID ${id} not found`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const manualInput = this.workflowService.validateManualInput(
|
|
127
|
+
input ?? {},
|
|
128
|
+
workflow.inputSchema,
|
|
129
|
+
);
|
|
130
|
+
const initiatorId = workflow.createdBy?.id ?? null;
|
|
131
|
+
const event = new ManualEventWrapper(manualInput, initiatorId);
|
|
132
|
+
const run = await this.dispatchTriggerEvent(workflow, event, initiatorId);
|
|
133
|
+
if (!run) {
|
|
134
|
+
// The run could not be created (missing initiator/definition) or the
|
|
135
|
+
// runner crashed before persisting a result.
|
|
136
|
+
throw new UnprocessableEntityException(
|
|
137
|
+
'Workflow run could not be started',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
runId: run.id,
|
|
143
|
+
status: run.status,
|
|
144
|
+
output: run.output ?? null,
|
|
145
|
+
error: run.error ?? null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves the initiator, targets the event at the given workflow, and hands
|
|
151
|
+
* it to the agentic runner. Shared by webhook triggers and authenticated
|
|
152
|
+
* manual runs so both paths dispatch identically.
|
|
153
|
+
*/
|
|
154
|
+
async dispatchTriggerEvent(
|
|
155
|
+
workflow: WorkflowFull,
|
|
156
|
+
event: TriggerEventWrapper,
|
|
157
|
+
initiatorId: string | null,
|
|
158
|
+
): Promise<WorkflowRunFull | null> {
|
|
159
|
+
const initiator = initiatorId
|
|
160
|
+
? await this.userService.findOne(initiatorId)
|
|
161
|
+
: null;
|
|
162
|
+
if (initiator) {
|
|
163
|
+
event.setInitiator(initiator);
|
|
164
|
+
}
|
|
165
|
+
event.setWorkflowId(workflow.id);
|
|
166
|
+
|
|
167
|
+
return await this.agenticService.handleEvent(event, { workflow });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -264,4 +264,25 @@ export class WorkflowRunService extends BaseOrmService<WorkflowRunOrmEntity> {
|
|
|
264
264
|
);
|
|
265
265
|
})[0];
|
|
266
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Mark all active runs linked to a thread as failed.
|
|
270
|
+
* Called when a thread is closed to abort any in-flight workflow execution.
|
|
271
|
+
*
|
|
272
|
+
* @param threadId - The ID of the closed thread.
|
|
273
|
+
*/
|
|
274
|
+
async abortActiveRunsForThread(threadId: string): Promise<void> {
|
|
275
|
+
const activeRuns = await this.find({
|
|
276
|
+
where: {
|
|
277
|
+
thread: { id: threadId },
|
|
278
|
+
status: In(ACTIVE_WORKFLOW_RUN_STATUSES),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await Promise.all(
|
|
283
|
+
activeRuns.map((run) =>
|
|
284
|
+
this.markFailed(run.id, { error: 'Thread was closed' }),
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
267
288
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { forwardRef, Module } from '@nestjs/common';
|
|
8
|
+
import { JwtModule } from '@nestjs/jwt';
|
|
8
9
|
import { SchedulerRegistry } from '@nestjs/schedule';
|
|
9
10
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
10
11
|
|
|
@@ -28,6 +29,7 @@ import { MemoryRecordOrmEntity } from './entities/memory-record.entity';
|
|
|
28
29
|
import { WorkflowRunOrmEntity } from './entities/workflow-run.entity';
|
|
29
30
|
import { WorkflowVersionOrmEntity } from './entities/workflow-version.entity';
|
|
30
31
|
import { WorkflowOrmEntity } from './entities/workflow.entity';
|
|
32
|
+
import { WebhookTriggerGuard } from './guards/webhook-trigger.guard';
|
|
31
33
|
import { McpServerRepository } from './repositories/mcp-server.repository';
|
|
32
34
|
import { MemoryDefinitionRepository } from './repositories/memory-definition.repository';
|
|
33
35
|
import { MemoryRecordRepository } from './repositories/memory-record.repository';
|
|
@@ -42,6 +44,7 @@ import { McpServerService } from './services/mcp-server.service';
|
|
|
42
44
|
import { MemoryDefinitionService } from './services/memory-definition.service';
|
|
43
45
|
import { MemoryRecordService } from './services/memory-record.service';
|
|
44
46
|
import { MemoryService } from './services/memory.service';
|
|
47
|
+
import { WebhookTriggerService } from './services/webhook-trigger.service';
|
|
45
48
|
import { WorkflowRunService } from './services/workflow-run.service';
|
|
46
49
|
import { WorkflowSchedulerService } from './services/workflow-scheduler.service';
|
|
47
50
|
import { WorkflowVersionService } from './services/workflow-version.service';
|
|
@@ -62,6 +65,7 @@ import { WORKFLOW_CALL_SERVICE } from './types';
|
|
|
62
65
|
forwardRef(() => CmsModule),
|
|
63
66
|
forwardRef(() => ChatModule),
|
|
64
67
|
UserModule,
|
|
68
|
+
JwtModule.register({}),
|
|
65
69
|
],
|
|
66
70
|
controllers: [
|
|
67
71
|
WorkflowController,
|
|
@@ -94,6 +98,8 @@ import { WORKFLOW_CALL_SERVICE } from './types';
|
|
|
94
98
|
ScheduledWorkflowContext,
|
|
95
99
|
WorkflowContextFactory,
|
|
96
100
|
AgenticService,
|
|
101
|
+
WebhookTriggerService,
|
|
102
|
+
WebhookTriggerGuard,
|
|
97
103
|
{ provide: WORKFLOW_CALL_SERVICE, useExisting: AgenticService },
|
|
98
104
|
],
|
|
99
105
|
exports: [
|
|
@@ -113,6 +119,8 @@ import { WORKFLOW_CALL_SERVICE } from './types';
|
|
|
113
119
|
McpClientPoolService,
|
|
114
120
|
ConversationalWorkflowContext,
|
|
115
121
|
AgenticService,
|
|
122
|
+
WebhookTriggerService,
|
|
123
|
+
WebhookTriggerGuard,
|
|
116
124
|
WORKFLOW_CALL_SERVICE,
|
|
117
125
|
],
|
|
118
126
|
})
|