Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions apps/api/src/app/agents/agents-webhook.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpException,
HttpStatus,
Expand Down Expand Up @@ -56,6 +57,16 @@ export class AgentsWebhookController {
);
}

@Get('/:agentId/webhook/:integrationIdentifier')
async handleWebhookVerification(
@Param('agentId') agentId: string,
@Param('integrationIdentifier') integrationIdentifier: string,
@Req() req: Request,
@Res() res: Response
) {
return this.routeWebhook(agentId, integrationIdentifier, req, res);
}

@Post('/:agentId/webhook/:integrationIdentifier')
@HttpCode(HttpStatus.OK)
async handleInboundWebhook(
Expand All @@ -64,12 +75,13 @@ export class AgentsWebhookController {
@Req() req: Request,
@Res() res: Response
) {
return this.routeWebhook(agentId, integrationIdentifier, req, res);
}

private async routeWebhook(agentId: string, integrationIdentifier: string, req: Request, res: Response) {
try {
console.log('handleInboundWebhook', agentId, integrationIdentifier);
await this.chatSdkService.handleWebhook(agentId, integrationIdentifier, req, res);
console.log('handleInboundWebhook success');
} catch (err) {
console.log(err);
if (err instanceof HttpException) {
res.status(err.getStatus()).json(err.getResponse());
} else {
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/app/agents/dtos/agent-platform.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ export enum AgentPlatformEnum {
WHATSAPP = 'whatsapp',
TEAMS = 'teams',
}

export const PLATFORMS_WITHOUT_TYPING_INDICATOR = new Set<AgentPlatformEnum>([
AgentPlatformEnum.WHATSAPP,
]);
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface PersistInboundMessageParams {
senderId: string;
senderName?: string;
content: string;
richContent?: Record<string, unknown>;
platformMessageId?: string;
environmentId: string;
organizationId: string;
Expand Down Expand Up @@ -140,6 +141,7 @@ export class AgentConversationService {
senderId: params.senderId,
senderName: params.senderName,
content: params.content,
richContent: params.richContent,
platformMessageId: params.platformMessageId,
environmentId: params.environmentId,
organizationId: params.organizationId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@novu/dal';
import type { Message, Thread } from 'chat';
import { AgentEventEnum } from '../dtos/agent-event.enum';
import { PLATFORMS_WITHOUT_TYPING_INDICATOR } from '../dtos/agent-platform.enum';
import { HandleAgentReplyCommand } from '../usecases/handle-agent-reply/handle-agent-reply.command';
import { HandleAgentReply } from '../usecases/handle-agent-reply/handle-agent-reply.usecase';
import { ResolvedAgentConfig } from './agent-config-resolver.service';
Expand Down Expand Up @@ -89,6 +90,18 @@ export class AgentInboundHandler {
? ConversationActivitySenderTypeEnum.SUBSCRIBER
: ConversationActivitySenderTypeEnum.PLATFORM_USER;

const richContent = message.attachments?.length
? {
attachments: message.attachments.map((a) => ({
type: a.type,
url: a.url,
name: a.name,
mimeType: a.mimeType,
size: a.size,
})),
}
: undefined;

await this.conversationService.persistInboundMessage({
conversationId: conversation._id,
platform: config.platform,
Expand All @@ -98,6 +111,7 @@ export class AgentInboundHandler {
senderId: participantId,
senderName: message.author.fullName,
content: message.text,
richContent,
platformMessageId: message.id,
environmentId: config.environmentId,
organizationId: config.organizationId,
Expand All @@ -121,7 +135,7 @@ export class AgentInboundHandler {
});
}

if (config.thinkingIndicatorEnabled) {
if (config.thinkingIndicatorEnabled && !PLATFORMS_WITHOUT_TYPING_INDICATOR.has(config.platform)) {
await thread.startTyping('Thinking...');
}

Expand Down
25 changes: 24 additions & 1 deletion apps/api/src/app/agents/services/bridge-executor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,20 @@ interface BridgeMessageAuthor {
isBot: boolean | 'unknown';
}

interface BridgeAttachment {
type: string;
url?: string;
name?: string;
mimeType?: string;
size?: number;
}

interface BridgeMessage {
text: string;
platformMessageId: string;
author: BridgeMessageAuthor;
timestamp: string;
attachments?: BridgeAttachment[];
}

export interface BridgeAction {
Expand Down Expand Up @@ -87,6 +96,7 @@ interface BridgeHistoryEntry {
role: ConversationActivitySenderTypeEnum;
type: ConversationActivityTypeEnum;
content: string;
richContent?: Record<string, unknown>;
senderName?: string;
signalData?: { type: string; payload?: Record<string, unknown> };
createdAt: string;
Expand Down Expand Up @@ -282,7 +292,7 @@ export class BridgeExecutorService {
}

private mapMessage(message: Message): BridgeMessage {
return {
const mapped: BridgeMessage = {
text: message.text,
platformMessageId: message.id,
author: {
Expand All @@ -293,6 +303,18 @@ export class BridgeExecutorService {
},
timestamp: message.metadata?.dateSent?.toISOString() ?? new Date().toISOString(),
};

if (message.attachments?.length) {
mapped.attachments = message.attachments.map((a) => ({
type: a.type,
url: a.url,
name: a.name,
mimeType: a.mimeType,
size: a.size,
}));
}

return mapped;
}

private mapConversation(conversation: ConversationEntity): BridgeConversation {
Expand Down Expand Up @@ -337,6 +359,7 @@ export class BridgeExecutorService {
role: activity.senderType,
type: activity.type,
content: activity.content,
richContent: activity.richContent || undefined,
senderName: activity.senderName || undefined,
signalData: activity.signalData || undefined,
createdAt: activity.createdAt,
Expand Down
36 changes: 25 additions & 11 deletions apps/api/src/app/agents/services/chat-sdk.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import { AgentInboundHandler } from './agent-inbound-handler.service';
* credentials.secretKey → appPassword
* credentials.tenantId → appTenantId
*
* WhatsApp: credentials.token → accessToken
* WhatsApp: credentials.apiToken → accessToken
* credentials.secretKey → appSecret
* credentials.apiToken → verifyToken
* credentials.token → verifyToken
* credentials.phoneNumberIdentification → phoneNumberId
*/

Expand Down Expand Up @@ -224,35 +224,49 @@ export class ChatSdkService implements OnModuleDestroy {

switch (platform) {
case AgentPlatformEnum.SLACK: {
if (!connectionAccessToken || !credentials.signingSecret) {
throw new BadRequestException('Slack agent integration requires botToken and signingSecret credentials');
}

const { createSlackAdapter } = await esmImport('@chat-adapter/slack');

return {
slack: createSlackAdapter({
botToken: connectionAccessToken!,
signingSecret: credentials.signingSecret!,
botToken: connectionAccessToken,
signingSecret: credentials.signingSecret,
}),
};
}
case AgentPlatformEnum.TEAMS: {
if (!credentials.clientId || !credentials.secretKey || !credentials.tenantId) {
throw new BadRequestException('Teams agent integration requires appId, appPassword, and appTenantId credentials');
}

const { createTeamsAdapter } = await esmImport('@chat-adapter/teams');

return {
teams: createTeamsAdapter({
appId: credentials.clientId!,
appPassword: credentials.secretKey!,
appTenantId: credentials.tenantId!,
appId: credentials.clientId,
appPassword: credentials.secretKey,
appTenantId: credentials.tenantId,
}),
};
}
case AgentPlatformEnum.WHATSAPP: {
if (!credentials.apiToken || !credentials.secretKey || !credentials.token || !credentials.phoneNumberIdentification) {
throw new BadRequestException(
'WhatsApp agent integration requires accessToken, appSecret, verifyToken, and phoneNumberId credentials'
);
}

const { createWhatsAppAdapter } = await esmImport('@chat-adapter/whatsapp');

return {
whatsapp: createWhatsAppAdapter({
accessToken: credentials.token!,
appSecret: credentials.secretKey!,
verifyToken: credentials.apiToken!,
phoneNumberId: credentials.phoneNumberIdentification!,
accessToken: credentials.apiToken,
appSecret: credentials.secretKey,
verifyToken: credentials.token,
phoneNumberId: credentials.phoneNumberIdentification,
}),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class ConversationActivityRepository extends BaseRepositoryV2<
senderType: ConversationActivitySenderTypeEnum;
senderId: string;
content: string;
richContent?: Record<string, unknown>;
platformMessageId?: string;
senderName?: string;
environmentId: string;
Expand All @@ -69,6 +70,7 @@ export class ConversationActivityRepository extends BaseRepositoryV2<
senderType: params.senderType,
senderId: params.senderId,
content: params.content,
richContent: params.richContent,
platformMessageId: params.platformMessageId,
senderName: params.senderName,
_environmentId: params.environmentId,
Expand Down
10 changes: 10 additions & 0 deletions packages/framework/src/resources/agent/agent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,20 @@ export interface AgentMessageAuthor {
isBot: boolean | 'unknown';
}

export interface AgentAttachment {
type: string;
url?: string;
name?: string;
mimeType?: string;
size?: number;
}

export interface AgentMessage {
text: string;
platformMessageId: string;
author: AgentMessageAuthor;
timestamp: string;
attachments?: AgentAttachment[];
}

export interface AgentConversation {
Expand All @@ -49,6 +58,7 @@ export interface AgentHistoryEntry {
role: string;
type: string;
content: string;
richContent?: Record<string, unknown>;
senderName?: string;
signalData?: { type: string; payload?: Record<string, unknown> };
createdAt: string;
Expand Down
1 change: 1 addition & 0 deletions packages/framework/src/resources/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { agent } from './agent.resource';
export type {
Agent,
AgentAction,
AgentAttachment,
AgentBridgeRequest,
AgentContext,
AgentConversation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type ConversationalProvider = {
export const CONVERSATIONAL_PROVIDERS: ConversationalProvider[] = [
{ providerId: ChatProviderIdEnum.Slack, displayName: 'Slack' },
{ providerId: ChatProviderIdEnum.MsTeams, displayName: 'MS Teams', comingSoon: true },
{ providerId: ChatProviderIdEnum.WhatsAppBusiness, displayName: 'WhatsApp Business', comingSoon: true },
{ providerId: ChatProviderIdEnum.WhatsAppBusiness, displayName: 'WhatsApp Business' },
{ providerId: 'telegram', displayName: 'Telegram', comingSoon: true },
{ providerId: 'google-chat', displayName: 'Google Chat', comingSoon: true },
{ providerId: 'linear', displayName: 'Linear', comingSoon: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,20 @@ export const whatsAppBusinessConfig: IConfigCredential[] = [
type: 'string',
required: true,
},
{
key: CredentialsKeyEnum.SecretKey,
displayName: 'App Secret',
description: 'Found under App Settings > Basic in your Meta app dashboard — used to verify inbound webhook signatures',
type: 'string',
required: false,
},
{
key: CredentialsKeyEnum.Token,
displayName: 'Verify Token',
description: 'A secret string you define — must match the Verify Token entered in your Meta webhook configuration',
type: 'string',
required: false,
},
Comment on lines +1293 to +1306
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

App Secret and Verify Token should not be optional if runtime treats them as required.

On Lines 1298 and 1305 these fields are optional, but downstream adapter setup dereferences credentials.secretKey! and credentials.token!. That allows invalid configs at save-time and fails later at runtime.

Suggested fix
   {
     key: CredentialsKeyEnum.SecretKey,
     displayName: 'App Secret',
     description: 'Found under App Settings > Basic in your Meta app dashboard — used to verify inbound webhook signatures',
     type: 'string',
-    required: false,
+    required: true,
   },
   {
     key: CredentialsKeyEnum.Token,
     displayName: 'Verify Token',
     description: 'A secret string you define — must match the Verify Token entered in your Meta webhook configuration',
     type: 'string',
-    required: false,
+    required: true,
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
key: CredentialsKeyEnum.SecretKey,
displayName: 'App Secret',
description: 'Found under App Settings > Basic in your Meta app dashboard — used to verify inbound webhook signatures',
type: 'string',
required: false,
},
{
key: CredentialsKeyEnum.Token,
displayName: 'Verify Token',
description: 'A secret string you define — must match the Verify Token entered in your Meta webhook configuration',
type: 'string',
required: false,
},
{
key: CredentialsKeyEnum.SecretKey,
displayName: 'App Secret',
description: 'Found under App Settings > Basic in your Meta app dashboard — used to verify inbound webhook signatures',
type: 'string',
required: true,
},
{
key: CredentialsKeyEnum.Token,
displayName: 'Verify Token',
description: 'A secret string you define — must match the Verify Token entered in your Meta webhook configuration',
type: 'string',
required: true,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/consts/providers/credentials/provider-credentials.ts`
around lines 1293 - 1306, The credential entries for
CredentialsKeyEnum.SecretKey and CredentialsKeyEnum.Token are marked required:
false but downstream code dereferences credentials.secretKey! and
credentials.token!, so update both entries to required: true (change the
required flag on the objects keyed by CredentialsKeyEnum.SecretKey and
CredentialsKeyEnum.Token) to enforce validation at save-time; additionally,
audit the adapter setup code that uses credentials.secretKey! and
credentials.token! (remove non-null assertions or add runtime checks) so runtime
dereferences are safe if inputs ever bypass schema validation.

];

export const mobishastraConfig: IConfigCredential[] = [
Expand Down
Loading