From a3a14fbc80c206d6903bd7af8e4a2b734715aafa Mon Sep 17 00:00:00 2001 From: Adam Chmara Date: Mon, 31 Aug 2026 10:47:37 +0200 Subject: [PATCH 1/7] fix(dashboard): pin Web Chat connect copy to --runtime ai-sdk fixes NV-8722 (#12487) --- .../src/hooks/use-web-chat-prompt.ts | 16 +++++--------- .../src/utils/web-chat-connect-prompt.spec.ts | 7 ++++++ .../src/utils/web-chat-connect-prompt.ts | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/apps/dashboard/src/hooks/use-web-chat-prompt.ts b/apps/dashboard/src/hooks/use-web-chat-prompt.ts index 8d3d7e1d107..859bd1ba44c 100644 --- a/apps/dashboard/src/hooks/use-web-chat-prompt.ts +++ b/apps/dashboard/src/hooks/use-web-chat-prompt.ts @@ -1,4 +1,8 @@ -import { buildWebChatPrompt, isNovuConnectBridgeRuntime, type NovuConnectBridgeRuntime } from '@novu/shared'; +import { + buildWebChatPrompt, + resolveWebChatConnectRuntime as resolveConnectBridgeRuntime, + type NovuConnectBridgeRuntime, +} from '@novu/shared'; import { useMemo } from 'react'; import type { AgentResponse } from '@/api/agents'; import type { ConnectorId } from '@/components/agents/connectors/connector-options'; @@ -8,15 +12,7 @@ export function resolveWebChatConnectRuntime( agent: AgentResponse, connectorId?: ConnectorId ): NovuConnectBridgeRuntime | undefined { - if (agent.runtime === 'managed') { - return undefined; - } - - if (isNovuConnectBridgeRuntime(connectorId)) { - return connectorId; - } - - return undefined; + return resolveConnectBridgeRuntime(agent.runtime, connectorId); } export function useWebChatPrompt(agent: AgentResponse, connectorId?: ConnectorId): string { diff --git a/packages/shared/src/utils/web-chat-connect-prompt.spec.ts b/packages/shared/src/utils/web-chat-connect-prompt.spec.ts index 76b064e4225..0ed12e18688 100644 --- a/packages/shared/src/utils/web-chat-connect-prompt.spec.ts +++ b/packages/shared/src/utils/web-chat-connect-prompt.spec.ts @@ -4,6 +4,7 @@ import { buildWebChatPrompt, buildWebChatTuiCommand, buildWebChatTuiCommandForDisplay, + resolveWebChatConnectRuntime, } from './web-chat-connect-prompt'; import { NOVU_STAGING_API_URL } from './novu-connect-cli'; @@ -59,6 +60,12 @@ describe('web-chat-connect-prompt', () => { expect(prompt).toContain('--region staging'); }); + it('defaults missing connector to ai-sdk for self-hosted Web Chat copy', () => { + expect(resolveWebChatConnectRuntime('self-hosted')).toBe('ai-sdk'); + expect(resolveWebChatConnectRuntime('self-hosted', 'langchain')).toBe('langchain'); + expect(resolveWebChatConnectRuntime('managed')).toBeUndefined(); + }); + it('pins runtime and agent on the TUI command for a dashboard-created bridge agent', () => { expect( buildWebChatTuiCommand({ diff --git a/packages/shared/src/utils/web-chat-connect-prompt.ts b/packages/shared/src/utils/web-chat-connect-prompt.ts index 1d5a5e1da59..add92722487 100644 --- a/packages/shared/src/utils/web-chat-connect-prompt.ts +++ b/packages/shared/src/utils/web-chat-connect-prompt.ts @@ -18,6 +18,28 @@ export function isNovuConnectBridgeRuntime(value: string | null | undefined): va return value === 'ai-sdk' || value === 'langchain' || value === 'custom-code'; } +/** Same default as the dashboard handler scaffold copy when the connector is unknown. */ +export const DEFAULT_NOVU_CONNECT_BRIDGE_RUNTIME: NovuConnectBridgeRuntime = 'ai-sdk'; + +/** + * CLI `--runtime` for Web Chat copy. Cloud agents store `managed` | `self-hosted`, + * not the bridge flavor, so missing connector defaults to AI SDK. + */ +export function resolveWebChatConnectRuntime( + agentRuntime: string | null | undefined, + connectorId?: string | null +): NovuConnectBridgeRuntime | undefined { + if (agentRuntime === 'managed') { + return undefined; + } + + if (isNovuConnectBridgeRuntime(connectorId)) { + return connectorId; + } + + return DEFAULT_NOVU_CONNECT_BRIDGE_RUNTIME; +} + function bridgeRuntimeLabel(runtime: NovuConnectBridgeRuntime): string { switch (runtime) { case 'ai-sdk': From 7c5cf1fcae306f062b8d849ea7d79cb63ecae04e Mon Sep 17 00:00:00 2001 From: "cursor[bot]" <206951365+cursor[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:47:48 +0200 Subject: [PATCH 2/7] chore(root): remove 1 unused file and 5 unused exports (#12504) Co-authored-by: Cursor Agent Co-authored-by: Adam Chmara --- .../claude-credentials-fields.tsx | 140 ------------------ .../agents/create-agent-fields/index.ts | 1 - .../support-drawer-constants.ts | 3 - .../chat/preview/shells/ms-teams-shell.tsx | 2 +- .../chat/preview/shells/shell-registry.tsx | 7 - .../steps/chat/preview/shells/slack-shell.tsx | 2 +- 6 files changed, 2 insertions(+), 153 deletions(-) delete mode 100644 apps/dashboard/src/components/agents/create-agent-fields/claude-credentials-fields.tsx diff --git a/apps/dashboard/src/components/agents/create-agent-fields/claude-credentials-fields.tsx b/apps/dashboard/src/components/agents/create-agent-fields/claude-credentials-fields.tsx deleted file mode 100644 index a255d45a85c..00000000000 --- a/apps/dashboard/src/components/agents/create-agent-fields/claude-credentials-fields.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useId, useState } from 'react'; -import { RiArrowRightUpLine, RiEyeLine, RiEyeOffLine, RiInformation2Line } from 'react-icons/ri'; -import { Input } from '@/components/primitives/input'; -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/primitives/tooltip'; -import { - ANTHROPIC_API_KEY_HREF, - CLAUDE_WORKSPACE_HREF, - type CreateAgentFormErrors, - DEFAULT_CLAUDE_WORKSPACE_ID, -} from './types'; - -type ClaudeCredentialsFieldsProps = { - apiKey: string; - workspaceId: string; - errors: CreateAgentFormErrors; - disabled?: boolean; - onApiKeyChange: (next: string) => void; - onWorkspaceIdChange: (next: string) => void; -}; - -export function ClaudeCredentialsFields({ - apiKey, - workspaceId, - errors, - disabled, - onApiKeyChange, - onWorkspaceIdChange, -}: ClaudeCredentialsFieldsProps) { - const formId = useId(); - const apiKeyId = `${formId}-api-key`; - const workspaceIdInputId = `${formId}-workspace-id`; - const [showSecret, setShowSecret] = useState(false); - - return ( -
-
-
- - - - - - - - - Your Anthropic API key is encrypted and stored securely. It is used to provision the agent on Claude - Platform. - - - -
- onApiKeyChange(e.target.value)} - placeholder="Paste the Anthropic API key here..." - hasError={Boolean(errors.apiKey)} - disabled={disabled} - aria-invalid={errors.apiKey ? true : undefined} - aria-describedby={errors.apiKey ? `${apiKeyId}-error` : undefined} - className="font-mono" - inlineTrailingNode={ - - } - /> - {errors.apiKey ? ( - - ) : null} -
- -
-
- - - - - - - - - The Anthropic workspace your API key is scoped to. Leave empty for the Default Workspace. For custom - workspaces, paste the `wrkspc_…` id from the Claude Console (Settings → Workspaces). Used for the - in-product "Open in Claude" deep link. - - - -
- onWorkspaceIdChange(e.target.value)} - placeholder={DEFAULT_CLAUDE_WORKSPACE_ID} - className="font-mono" - disabled={disabled} - /> -
-
- ); -} diff --git a/apps/dashboard/src/components/agents/create-agent-fields/index.ts b/apps/dashboard/src/components/agents/create-agent-fields/index.ts index 651b9e8105b..3ae0217c21f 100644 --- a/apps/dashboard/src/components/agents/create-agent-fields/index.ts +++ b/apps/dashboard/src/components/agents/create-agent-fields/index.ts @@ -1,6 +1,5 @@ export * from './agent-form-validation'; export * from './aws-claude-credentials-fields'; -export * from './claude-credentials-fields'; export * from './configure-credentials-section'; export * from './existing-agent-fields'; export * from './managed-integration-credentials'; diff --git a/apps/dashboard/src/components/header-navigation/support-drawer-constants.ts b/apps/dashboard/src/components/header-navigation/support-drawer-constants.ts index 3cd51c7c757..0a343f122e0 100644 --- a/apps/dashboard/src/components/header-navigation/support-drawer-constants.ts +++ b/apps/dashboard/src/components/header-navigation/support-drawer-constants.ts @@ -357,9 +357,6 @@ const CONTEXTUAL_GETTING_STARTED: Partial conversations: AGENT_GETTING_STARTED, }; -/** Default getting-started links shown outside agent surfaces. */ -export const GETTING_STARTED = DEFAULT_GETTING_STARTED; - export function useContextualGettingStarted(): SuggestionItem[] { const location = useLocation(); diff --git a/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/ms-teams-shell.tsx b/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/ms-teams-shell.tsx index ba19df5521d..a1b2d798fb5 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/ms-teams-shell.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/ms-teams-shell.tsx @@ -17,7 +17,7 @@ const TEAMS_FONT = * the message rendered inside its own bordered content card. The full preview also shows the Teams * compose bar. Colors follow Teams' desktop message list (`#242424` body, `#616161` meta). */ -export function MsTeamsPreviewFrame({ children, variant = 'default' }: MsTeamsPreviewFrameProps) { +function MsTeamsPreviewFrame({ children, variant = 'default' }: MsTeamsPreviewFrameProps) { return (
{ - return getChatPreviewSkin(providerId).Shell; -} - -export function getChatContentSkeleton(providerId: string): ComponentType { - return getChatPreviewSkin(providerId).ContentSkeleton; -} diff --git a/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/slack-shell.tsx b/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/slack-shell.tsx index ba685a83bcc..f5174f670d4 100644 --- a/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/slack-shell.tsx +++ b/apps/dashboard/src/components/workflow-editor/steps/chat/preview/shells/slack-shell.tsx @@ -12,7 +12,7 @@ type SlackPreviewFrameProps = { * Slack message chrome: app icon/sender line (Block Kit Builder) plus the Figma Message Box * composer (`10415:19564`) used in the full preview. */ -export function SlackPreviewFrame({ children, variant = 'default' }: SlackPreviewFrameProps) { +function SlackPreviewFrame({ children, variant = 'default' }: SlackPreviewFrameProps) { return (
Date: Mon, 31 Aug 2026 12:27:54 +0300 Subject: [PATCH 3/7] fix(providers): Block configurable endpoint SSRF fixes NV-8726 (#12503) Co-authored-by: Cursor Agent --- ...e-outbound-integration-credentials.spec.ts | 61 ++++++++++++++++ ...lidate-outbound-integration-credentials.ts | 55 +++++++++++++- .../factories/push/handlers/appio.handler.ts | 4 +- packages/providers/src/index.ts | 1 + .../src/lib/email/braze/braze.provider.ts | 11 ++- .../email/infobip/infobip.provider.spec.ts | 4 +- .../src/lib/email/infobip/infobip.provider.ts | 5 +- .../src/lib/email/mailgun/mailgun.provider.ts | 9 ++- .../src/lib/push/appio/appio.provider.spec.ts | 2 +- .../src/lib/push/appio/appio.provider.ts | 72 +++++++++++-------- .../src/lib/sms/kannel/kannel.provider.ts | 30 ++++++-- .../sms/mobishastra/mobishastra.provider.ts | 37 ++++++++-- .../sms/sms-central/sms-central.provider.ts | 19 ++++- .../src/utils/safe-provider-url.spec.ts | 54 ++++++++++++++ .../providers/src/utils/safe-provider-url.ts | 58 +++++++++++++++ 15 files changed, 369 insertions(+), 53 deletions(-) create mode 100644 apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.spec.ts create mode 100644 packages/providers/src/utils/safe-provider-url.spec.ts create mode 100644 packages/providers/src/utils/safe-provider-url.ts diff --git a/apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.spec.ts b/apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.spec.ts new file mode 100644 index 00000000000..d4b54a3313c --- /dev/null +++ b/apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.spec.ts @@ -0,0 +1,61 @@ +import { EmailProviderIdEnum, PushProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; +import { expect } from 'chai'; +import { validateOutboundIntegrationCredentials } from './validate-outbound-integration-credentials'; + +const ORIGINAL_CI_EE_TEST = process.env.CI_EE_TEST; +const ORIGINAL_SELF_HOSTED = process.env.IS_SELF_HOSTED; + +function restoreEnv(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + + return; + } + + process.env[name] = value; +} + +describe('validateOutboundIntegrationCredentials', () => { + beforeEach(() => { + process.env.CI_EE_TEST = 'true'; + process.env.IS_SELF_HOSTED = 'false'; + }); + + afterEach(() => { + restoreEnv('CI_EE_TEST', ORIGINAL_CI_EE_TEST); + restoreEnv('IS_SELF_HOSTED', ORIGINAL_SELF_HOSTED); + }); + + const privateTargetCases = [ + [EmailProviderIdEnum.Infobip, { baseUrl: 'http://127.0.0.1:3000' }], + [EmailProviderIdEnum.Braze, { apiURL: 'http://127.0.0.1:3000' }], + [EmailProviderIdEnum.Mailgun, { baseUrl: 'http://127.0.0.1:3000' }], + [SmsProviderIdEnum.SmsCentral, { baseUrl: 'http://127.0.0.1:3000' }], + [SmsProviderIdEnum.Mobishastra, { baseUrl: 'http://127.0.0.1:3000' }], + [SmsProviderIdEnum.Kannel, { host: '127.0.0.1', port: '3000' }], + [PushProviderIdEnum.AppIO, { AppIOBaseUrl: 'http://127.0.0.1:3000' }], + ] as const; + + for (const [providerId, credentials] of privateTargetCases) { + it(`blocks a private ${providerId} target on Cloud`, async () => { + let error: Error | undefined; + + try { + await validateOutboundIntegrationCredentials(providerId, credentials); + } catch (caughtError) { + error = caughtError as Error; + } + + expect(error?.message).to.match(/blocked|not allowed/i); + }); + } + + it('preserves private Kannel targets for self-hosted deployments', async () => { + process.env.IS_SELF_HOSTED = 'true'; + + await validateOutboundIntegrationCredentials(SmsProviderIdEnum.Kannel, { + host: '10.0.0.1', + port: '13013', + }); + }); +}); diff --git a/apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.ts b/apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.ts index 6929f57e257..01cba4bc6d6 100644 --- a/apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.ts +++ b/apps/api/src/app/integrations/utils/validate-outbound-integration-credentials.ts @@ -1,5 +1,12 @@ import { BadRequestException } from '@nestjs/common'; -import { assertAllowedSinchSmsRegion, EmailProviderIdEnum, ICredentials, SmsProviderIdEnum } from '@novu/shared'; +import { resolveSafeInfobipBaseUrl, resolveSafeProviderUrl } from '@novu/providers'; +import { + assertAllowedSinchSmsRegion, + EmailProviderIdEnum, + ICredentials, + PushProviderIdEnum, + SmsProviderIdEnum, +} from '@novu/shared'; type ValidateSmtpOutboundTargetModule = typeof import('@novu/shared/dist/cjs/utils/validate-smtp-outbound-target'); @@ -27,6 +34,52 @@ export async function validateOutboundIntegrationCredentials( if (providerId === SmsProviderIdEnum.Sinch) { assertAllowedSinchSmsRegion(credentials.region); } + + if (providerId === EmailProviderIdEnum.Infobip) { + resolveSafeInfobipBaseUrl(credentials.baseUrl); + } + + if (providerId === EmailProviderIdEnum.Braze) { + resolveSafeProviderUrl(credentials.apiURL, { + blockedPrefix: 'Braze API URL blocked', + isHostnameAllowed: (hostname) => /^rest\.[a-z0-9-]+\.braze\.(com|eu)$/.test(hostname), + requireHttps: true, + }); + } + + if (providerId === EmailProviderIdEnum.Mailgun) { + resolveSafeProviderUrl(credentials.baseUrl || 'https://api.mailgun.net', { + allowedHostnames: ['api.mailgun.net', 'api.eu.mailgun.net'], + blockedPrefix: 'Mailgun base URL blocked', + requireHttps: true, + }); + } + + if (providerId === SmsProviderIdEnum.SmsCentral) { + resolveSafeProviderUrl(credentials.baseUrl || 'https://my.smscentral.com.au/api/v3.2', { + blockedPrefix: 'SMS Central base URL blocked', + }); + } + + if (providerId === SmsProviderIdEnum.Mobishastra) { + resolveSafeProviderUrl(credentials.baseUrl, { + blockedPrefix: 'Mobishastra base URL blocked', + }); + } + + if (providerId === SmsProviderIdEnum.Kannel) { + resolveSafeProviderUrl(`http://${credentials.host}:${credentials.port}/cgi-bin`, { + blockedPrefix: 'Kannel host blocked', + }); + } + + if (providerId === PushProviderIdEnum.AppIO) { + resolveSafeProviderUrl(credentials.AppIOBaseUrl || 'https://api.io.italia.it/api/v1', { + allowedHostnames: ['api.io.italia.it'], + blockedPrefix: 'AppIO base URL blocked', + requireHttps: true, + }); + } } catch (error) { if (error instanceof Error) { throw new BadRequestException(error.message); diff --git a/libs/application-generic/src/factories/push/handlers/appio.handler.ts b/libs/application-generic/src/factories/push/handlers/appio.handler.ts index 04f9ea43f6c..9b3ac7a0a9d 100644 --- a/libs/application-generic/src/factories/push/handlers/appio.handler.ts +++ b/libs/application-generic/src/factories/push/handlers/appio.handler.ts @@ -1,5 +1,5 @@ -import { ChannelTypeEnum, ICredentials, PushProviderIdEnum } from '@novu/shared'; import { AppioPushProvider } from '@novu/providers'; +import { ChannelTypeEnum, ICredentials, PushProviderIdEnum } from '@novu/shared'; import { BasePushHandler } from './base.handler'; export class AppIOHandler extends BasePushHandler { @@ -8,7 +8,7 @@ export class AppIOHandler extends BasePushHandler { } buildProvider(credentials: ICredentials) { - const config: { AppIOBaseUrl?: string } = { AppIOBaseUrl: credentials.apiKey }; + const config: { AppIOBaseUrl?: string } = { AppIOBaseUrl: credentials.AppIOBaseUrl }; this.provider = new AppioPushProvider(config); } diff --git a/packages/providers/src/index.ts b/packages/providers/src/index.ts index b7a415547b3..be0278c108a 100644 --- a/packages/providers/src/index.ts +++ b/packages/providers/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/index'; export { resolveSafeInfobipBaseUrl } from './utils/safe-infobip-base-url'; +export { resolveSafeProviderUrl } from './utils/safe-provider-url'; diff --git a/packages/providers/src/lib/email/braze/braze.provider.ts b/packages/providers/src/lib/email/braze/braze.provider.ts index 8f4c19814d2..38ddd4737d7 100644 --- a/packages/providers/src/lib/email/braze/braze.provider.ts +++ b/packages/providers/src/lib/email/braze/braze.provider.ts @@ -9,8 +9,11 @@ import { } from '@novu/stateless'; import { Braze, MessagesSendObject, UsersExportIdsObject, UsersExportIdsResponse } from 'braze-api'; import { BaseProvider, CasingEnum } from '../../../base.provider'; +import { resolveSafeProviderUrl } from '../../../utils/safe-provider-url'; import { WithPassthrough } from '../../../utils/types'; +const BRAZE_REST_HOSTNAME_PATTERN = /^rest\.[a-z0-9-]+\.braze\.(com|eu)$/; + export class BrazeEmailProvider extends BaseProvider implements IEmailProvider { id = EmailProviderIdEnum.Braze; protected casing: CasingEnum = CasingEnum.SNAKE_CASE; @@ -25,7 +28,13 @@ export class BrazeEmailProvider extends BaseProvider implements IEmailProvider { } ) { super(); - this.braze = new Braze(this.config.apiURL, this.config.apiKey); + const apiUrl = resolveSafeProviderUrl(this.config.apiURL, { + blockedPrefix: 'Braze API URL blocked', + isHostnameAllowed: (hostname) => BRAZE_REST_HOSTNAME_PATTERN.test(hostname), + requireHttps: true, + }); + + this.braze = new Braze(apiUrl, this.config.apiKey); } async sendMessage( diff --git a/packages/providers/src/lib/email/infobip/infobip.provider.spec.ts b/packages/providers/src/lib/email/infobip/infobip.provider.spec.ts index d1d6f26f4d5..d8366d7ecf5 100644 --- a/packages/providers/src/lib/email/infobip/infobip.provider.spec.ts +++ b/packages/providers/src/lib/email/infobip/infobip.provider.spec.ts @@ -3,7 +3,7 @@ import { InfobipEmailProvider } from './infobip.provider'; test('should trigger infobip library correctly - E-mail', async () => { const provider = new InfobipEmailProvider({ - baseUrl: 'localhost', + baseUrl: 'https://test.api.infobip.com', apiKey: '', }); @@ -43,7 +43,7 @@ test('should trigger infobip library correctly - E-mail', async () => { test('should trigger infobip library correctly - E-mail with _passthrough', async () => { const provider = new InfobipEmailProvider({ - baseUrl: 'localhost', + baseUrl: 'https://test.api.infobip.com', apiKey: '', }); diff --git a/packages/providers/src/lib/email/infobip/infobip.provider.ts b/packages/providers/src/lib/email/infobip/infobip.provider.ts index 7e118c52d83..92d2f25c666 100644 --- a/packages/providers/src/lib/email/infobip/infobip.provider.ts +++ b/packages/providers/src/lib/email/infobip/infobip.provider.ts @@ -9,6 +9,7 @@ import { ISendMessageSuccessResponse, } from '@novu/stateless'; import { BaseProvider, CasingEnum } from '../../../base.provider'; +import { resolveSafeInfobipBaseUrl } from '../../../utils/safe-infobip-base-url'; import { WithPassthrough } from '../../../utils/types'; export class InfobipEmailProvider extends BaseProvider implements IEmailProvider { @@ -26,8 +27,10 @@ export class InfobipEmailProvider extends BaseProvider implements IEmailProvider } ) { super(); + const baseUrl = resolveSafeInfobipBaseUrl(this.config.baseUrl); + this.infobipClient = new Infobip({ - baseUrl: this.config.baseUrl, + baseUrl, apiKey: this.config.apiKey, authType: AuthType.ApiKey, }); diff --git a/packages/providers/src/lib/email/mailgun/mailgun.provider.ts b/packages/providers/src/lib/email/mailgun/mailgun.provider.ts index 173e35323d9..85fe740f55e 100644 --- a/packages/providers/src/lib/email/mailgun/mailgun.provider.ts +++ b/packages/providers/src/lib/email/mailgun/mailgun.provider.ts @@ -16,6 +16,7 @@ import Mailgun from 'mailgun.js'; import { IMailgunClient } from 'mailgun.js/interfaces/IMailgunClient'; import { MailgunMessageData } from 'mailgun.js/interfaces/Messages'; import { BaseProvider, CasingEnum } from '../../../base.provider'; +import { resolveSafeProviderUrl } from '../../../utils/safe-provider-url'; import { WithPassthrough } from '../../../utils/types'; enum WebhooksIds { @@ -66,13 +67,19 @@ export class MailgunEmailProvider extends BaseProvider implements IEmailProvider } ) { super(); + const baseUrl = resolveSafeProviderUrl(config.baseUrl || 'https://api.mailgun.net', { + allowedHostnames: ['api.mailgun.net', 'api.eu.mailgun.net'], + blockedPrefix: 'Mailgun base URL blocked', + requireHttps: true, + }); const mailgun = new Mailgun(formData); this.mailgunClient = mailgun.client({ username: config.username, key: config.apiKey, - url: config.baseUrl || 'https://api.mailgun.net', + url: baseUrl, }); + this.config.baseUrl = baseUrl; } async sendMessage( diff --git a/packages/providers/src/lib/push/appio/appio.provider.spec.ts b/packages/providers/src/lib/push/appio/appio.provider.spec.ts index ba9f0434e6d..ac9ed499fde 100644 --- a/packages/providers/src/lib/push/appio/appio.provider.spec.ts +++ b/packages/providers/src/lib/push/appio/appio.provider.spec.ts @@ -55,7 +55,7 @@ describe('AppioPushProvider.sendMessage', () => { it('should send message and return id and date', async () => { mockAxios.post .mockImplementationOnce(() => Promise.resolve({ status: 200, data: { sender_allowed: true } })) - .mockImplementationOnce(() => Promise.resolve({ data: { id: 'msg-id-123' } })); + .mockImplementationOnce(() => Promise.resolve({ status: 201, data: { id: 'msg-id-123' } })); const res = await provider.sendMessage( { diff --git a/packages/providers/src/lib/push/appio/appio.provider.ts b/packages/providers/src/lib/push/appio/appio.provider.ts index e4f0d223063..a29a5eacc24 100644 --- a/packages/providers/src/lib/push/appio/appio.provider.ts +++ b/packages/providers/src/lib/push/appio/appio.provider.ts @@ -1,7 +1,9 @@ -import { PushProviderIdEnum } from '@novu/shared'; +import { isOutboundSsrfProtectionEnabled, PushProviderIdEnum } from '@novu/shared'; +import { safeOutboundJsonRequest } from '@novu/shared/utils/safe-outbound-http'; import { ChannelTypeEnum, IPushOptions, IPushProvider, ISendMessageSuccessResponse } from '@novu/stateless'; import axios from 'axios'; import { BaseProvider, CasingEnum } from '../../../base.provider'; +import { resolveSafeProviderUrl } from '../../../utils/safe-provider-url'; export class AppioPushProvider extends BaseProvider implements IPushProvider { id = PushProviderIdEnum.AppIO; @@ -28,54 +30,64 @@ export class AppioPushProvider extends BaseProvider implements IPushProvider { } const apiKey = bridgeProviderData?.apiKey as string; - const baseUrl = this.config?.AppIOBaseUrl || 'https://api.io.italia.it/api/v1'; + const baseUrl = resolveSafeProviderUrl(this.config?.AppIOBaseUrl || 'https://api.io.italia.it/api/v1', { + allowedHostnames: ['api.io.italia.it'], + blockedPrefix: 'AppIO base URL blocked', + requireHttps: true, + }); if (!apiKey) { throw new Error('Missing App IO API key (must be passed via bridgeProviderData.apiKey)'); } - const profileRes = await this.axiosInstance.post( - `${baseUrl}/profiles`, - { fiscal_code: fiscalCode }, - { - headers: { - 'Ocp-Apim-Subscription-Key': apiKey, - 'Content-Type': 'application/json', - }, - } - ); + const headers = { + 'Ocp-Apim-Subscription-Key': apiKey, + 'Content-Type': 'application/json', + }; + const profileRes = isOutboundSsrfProtectionEnabled() + ? await safeOutboundJsonRequest<{ sender_allowed?: boolean }>({ + url: `${baseUrl}/profiles`, + method: 'POST', + body: { fiscal_code: fiscalCode }, + headers, + }) + : await this.axiosInstance.post(`${baseUrl}/profiles`, { fiscal_code: fiscalCode }, { headers }); if (!profileRes) { throw new Error('Invalid response from App IO profile API'); } - if (profileRes.status !== 200 || profileRes.data?.sender_allowed !== true) { + const profileStatus = 'statusCode' in profileRes ? profileRes.statusCode : profileRes.status; + const profileData = 'body' in profileRes ? profileRes.body : profileRes.data; + + if (profileStatus !== 200 || profileData?.sender_allowed !== true) { throw new Error('Recipient is not allowed or not found in App IO'); } - const messageRes = await this.axiosInstance.post( - `${baseUrl}/messages`, - { - fiscal_code: fiscalCode, - content: { - subject: title, - markdown: content, - }, + const messageBody = { + fiscal_code: fiscalCode, + content: { + subject: title, + markdown: content, }, - { - headers: { - 'Ocp-Apim-Subscription-Key': apiKey, - 'Content-Type': 'application/json', - }, - } - ); + }; + const messageRes = isOutboundSsrfProtectionEnabled() + ? await safeOutboundJsonRequest<{ id?: string }>({ + url: `${baseUrl}/messages`, + method: 'POST', + body: messageBody, + headers, + }) + : await this.axiosInstance.post(`${baseUrl}/messages`, messageBody, { headers }); + const messageStatus = 'statusCode' in messageRes ? messageRes.statusCode : messageRes.status; + const messageData = 'body' in messageRes ? messageRes.body : messageRes.data; - if (!messageRes || !messageRes.data) { + if (messageStatus < 200 || messageStatus >= 300 || !messageData) { throw new Error('Invalid response from App IO message API'); } return { - id: messageRes.data.id || '', + id: messageData.id || '', date: new Date().toISOString(), }; } diff --git a/packages/providers/src/lib/sms/kannel/kannel.provider.ts b/packages/providers/src/lib/sms/kannel/kannel.provider.ts index 81e61512842..64b9ddc3533 100644 --- a/packages/providers/src/lib/sms/kannel/kannel.provider.ts +++ b/packages/providers/src/lib/sms/kannel/kannel.provider.ts @@ -1,7 +1,9 @@ -import { SmsProviderIdEnum } from '@novu/shared'; +import { isOutboundSsrfProtectionEnabled, SmsProviderIdEnum } from '@novu/shared'; +import { safeOutboundRequest } from '@novu/shared/utils/safe-outbound-http'; import { ChannelTypeEnum, ISendMessageSuccessResponse, ISmsOptions, ISmsProvider } from '@novu/stateless'; import axios, { AxiosInstance } from 'axios'; import { BaseProvider, CasingEnum } from '../../../base.provider'; +import { resolveSafeProviderUrl } from '../../../utils/safe-provider-url'; import { WithPassthrough } from '../../../utils/types'; export class KannelSmsProvider extends BaseProvider implements ISmsProvider { @@ -38,9 +40,29 @@ export class KannelSmsProvider extends BaseProvider implements ISmsProvider { text: options.content, }).body; - const result = await this.axiosInstance.get(url, { - params: queryParameters, - }); + if (isOutboundSsrfProtectionEnabled()) { + const safeUrl = new URL( + resolveSafeProviderUrl(url, { + blockedPrefix: 'Kannel host blocked', + }) + ); + + for (const [key, value] of Object.entries(queryParameters)) { + if (value !== undefined && value !== null) { + safeUrl.searchParams.set(key, String(value)); + } + } + + const response = await safeOutboundRequest({ url: safeUrl, method: 'GET' }); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`Kannel request failed with status ${response.statusCode}`); + } + } else { + await this.axiosInstance.get(url, { + params: queryParameters, + }); + } return { id: options.id, diff --git a/packages/providers/src/lib/sms/mobishastra/mobishastra.provider.ts b/packages/providers/src/lib/sms/mobishastra/mobishastra.provider.ts index 8824c16bb24..e79e89bb8c0 100644 --- a/packages/providers/src/lib/sms/mobishastra/mobishastra.provider.ts +++ b/packages/providers/src/lib/sms/mobishastra/mobishastra.provider.ts @@ -1,7 +1,9 @@ -import { SmsProviderIdEnum } from '@novu/shared'; +import { isOutboundSsrfProtectionEnabled, SmsProviderIdEnum } from '@novu/shared'; +import { safeOutboundJsonRequest } from '@novu/shared/utils/safe-outbound-http'; import { ChannelTypeEnum, ISendMessageSuccessResponse, ISmsOptions, ISmsProvider } from '@novu/stateless'; import axios, { AxiosInstance } from 'axios'; import { BaseProvider, CasingEnum } from '../../../base.provider'; +import { resolveSafeProviderUrl } from '../../../utils/safe-provider-url'; import { WithPassthrough } from '../../../utils/types'; export class MobishastraProvider extends BaseProvider implements ISmsProvider { @@ -40,13 +42,34 @@ export class MobishastraProvider extends BaseProvider implements ISmsProvider { user: this.config.username, pwd: this.config.password, }); - const response = await this.axiosInstance.request({ - method: 'POST', - data: JSON.stringify([transformedData.body]), - headers: transformedData.headers, - }); + const requestBody = JSON.stringify([transformedData.body]); + let responseData: Record | undefined; + + if (isOutboundSsrfProtectionEnabled()) { + const url = resolveSafeProviderUrl(this.config.baseUrl, { + blockedPrefix: 'Mobishastra base URL blocked', + }); + const response = await safeOutboundJsonRequest[]>({ + url, + method: 'POST', + body: requestBody, + headers: transformedData.headers, + }); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`Mobishastra request failed with status ${response.statusCode}`); + } + + responseData = response.body?.[0]; + } else { + const response = await this.axiosInstance.request({ + method: 'POST', + data: requestBody, + headers: transformedData.headers, + }); + responseData = response.data?.[0]; + } - const responseData = response.data?.[0]; const messageId = responseData?.msg_id?.trim(); if (!messageId) { diff --git a/packages/providers/src/lib/sms/sms-central/sms-central.provider.ts b/packages/providers/src/lib/sms/sms-central/sms-central.provider.ts index 677ef1df29f..746f474b737 100644 --- a/packages/providers/src/lib/sms/sms-central/sms-central.provider.ts +++ b/packages/providers/src/lib/sms/sms-central/sms-central.provider.ts @@ -1,7 +1,9 @@ -import { SmsProviderIdEnum } from '@novu/shared'; +import { isOutboundSsrfProtectionEnabled, SmsProviderIdEnum } from '@novu/shared'; +import { safeOutboundJsonRequest } from '@novu/shared/utils/safe-outbound-http'; import { ChannelTypeEnum, ISendMessageSuccessResponse, ISmsOptions, ISmsProvider } from '@novu/stateless'; import axios from 'axios'; import { BaseProvider, CasingEnum } from '../../../base.provider'; +import { resolveSafeProviderUrl } from '../../../utils/safe-provider-url'; import { WithPassthrough } from '../../../utils/types'; export class SmsCentralSmsProvider extends BaseProvider implements ISmsProvider { @@ -34,8 +36,19 @@ export class SmsCentralSmsProvider extends BaseProvider implements ISmsProvider MESSAGE_TEXT: options.content, }).body; - const url = this.config.baseUrl || this.DEFAULT_BASE_URL; - await axios.create().post(url, data); + const url = resolveSafeProviderUrl(this.config.baseUrl || this.DEFAULT_BASE_URL, { + blockedPrefix: 'SMS Central base URL blocked', + }); + + if (isOutboundSsrfProtectionEnabled()) { + const response = await safeOutboundJsonRequest({ url, method: 'POST', body: data }); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`SMS Central request failed with status ${response.statusCode}`); + } + } else { + await axios.create().post(url, data); + } return { id: options.id, diff --git a/packages/providers/src/utils/safe-provider-url.spec.ts b/packages/providers/src/utils/safe-provider-url.spec.ts new file mode 100644 index 00000000000..241ec8df9ef --- /dev/null +++ b/packages/providers/src/utils/safe-provider-url.spec.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import { resolveSafeProviderUrl } from './safe-provider-url'; + +const ORIGINAL_ENTERPRISE = process.env.NOVU_ENTERPRISE; +const ORIGINAL_SELF_HOSTED = process.env.IS_SELF_HOSTED; + +function restoreEnv(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + + return; + } + + process.env[name] = value; +} + +describe('resolveSafeProviderUrl', () => { + beforeEach(() => { + process.env.NOVU_ENTERPRISE = 'true'; + process.env.IS_SELF_HOSTED = 'false'; + }); + + afterEach(() => { + restoreEnv('NOVU_ENTERPRISE', ORIGINAL_ENTERPRISE); + restoreEnv('IS_SELF_HOSTED', ORIGINAL_SELF_HOSTED); + }); + + test.each(['http://127.0.0.1:3000', 'http://10.0.0.1', 'http://169.254.169.254/latest/meta-data'])( + 'blocks private target %s on Cloud', + (url) => { + expect(() => resolveSafeProviderUrl(url, { blockedPrefix: 'Provider URL blocked' })).toThrow( + 'Provider URL blocked' + ); + } + ); + + test('allows a public provider URL on Cloud', () => { + expect( + resolveSafeProviderUrl('https://api.mailgun.net', { + allowedHostnames: ['api.mailgun.net'], + blockedPrefix: 'Provider URL blocked', + requireHttps: true, + }) + ).toBe('https://api.mailgun.net'); + }); + + test('preserves private targets for self-hosted deployments', () => { + process.env.IS_SELF_HOSTED = 'true'; + + expect(resolveSafeProviderUrl('http://10.0.0.1', { blockedPrefix: 'Provider URL blocked' })).toBe( + 'http://10.0.0.1' + ); + }); +}); diff --git a/packages/providers/src/utils/safe-provider-url.ts b/packages/providers/src/utils/safe-provider-url.ts new file mode 100644 index 00000000000..6f9a1b92201 --- /dev/null +++ b/packages/providers/src/utils/safe-provider-url.ts @@ -0,0 +1,58 @@ +import { isOutboundSsrfProtectionEnabled } from '@novu/shared'; +import { + assertSafeOutboundUrl, + normalizeOutboundHttpUrl, + SsrfBlockedError, +} from '@novu/shared/utils/ssrf-url-validation'; + +interface ResolveSafeProviderUrlOptions { + allowedHostnames?: string[]; + blockedPrefix: string; + isHostnameAllowed?: (hostname: string) => boolean; + requireHttps?: boolean; +} + +export class ProviderUrlBlockedError extends Error { + constructor(message: string) { + super(message); + this.name = 'ProviderUrlBlockedError'; + } +} + +export function resolveSafeProviderUrl(rawUrl: string | undefined, options: ResolveSafeProviderUrlOptions): string { + if (!isOutboundSsrfProtectionEnabled()) { + return rawUrl ?? ''; + } + + const normalizedUrl = normalizeOutboundHttpUrl(rawUrl ?? ''); + + if (!normalizedUrl) { + throw new ProviderUrlBlockedError(`${options.blockedPrefix}: Invalid URL format.`); + } + + let parsed: URL; + + try { + parsed = assertSafeOutboundUrl(normalizedUrl); + } catch (error) { + if (error instanceof SsrfBlockedError) { + throw new ProviderUrlBlockedError(`${options.blockedPrefix}: ${error.message}`); + } + + throw error; + } + + if (options.requireHttps && parsed.protocol !== 'https:') { + throw new ProviderUrlBlockedError(`${options.blockedPrefix}: Only HTTPS URLs are allowed.`); + } + + const hostname = parsed.hostname.toLowerCase(); + const isExactHostnameAllowed = options.allowedHostnames?.includes(hostname) ?? true; + const isCustomHostnameAllowed = options.isHostnameAllowed?.(hostname) ?? true; + + if (!isExactHostnameAllowed || !isCustomHostnameAllowed) { + throw new ProviderUrlBlockedError(`${options.blockedPrefix}: Hostname is not an allowed provider endpoint.`); + } + + return normalizedUrl; +} From 1a0b5186c39d7aa40d35a23e2ca23010eeb3303e Mon Sep 17 00:00:00 2001 From: Dima Grossman Date: Mon, 31 Aug 2026 14:48:32 +0300 Subject: [PATCH 4/7] docs(docs): document remaining webhook connectors fixes DOC-437 (#12506) Co-authored-by: Cursor Agent --- docs/platform/developer/webhooks.mdx | 4 +- .../developer/webhooks/connectors.mdx | 473 +++++++++++++++++- docs/platform/developer/webhooks/webhooks.mdx | 11 +- 3 files changed, 467 insertions(+), 21 deletions(-) diff --git a/docs/platform/developer/webhooks.mdx b/docs/platform/developer/webhooks.mdx index ff3ee0f93b3..77a654adb80 100644 --- a/docs/platform/developer/webhooks.mdx +++ b/docs/platform/developer/webhooks.mdx @@ -22,11 +22,11 @@ To start listening to messages, you must register your endpoint in the Novu dash 1. Go to the **[Webhooks](https://dashboard.novu.co/webhooks)** page in the Novu dashboard. 2. Select the **Endpoints** tab. 3. Click **Add Endpoint**. -4. Select a webhook integration from the list. Choose **Webhook** to receive events over HTTP, or select a [connector](/platform/developer/webhooks/connectors) to deliver events directly to a data warehouse or AWS messaging service. +4. Select a webhook integration from the list. Choose **Webhook** to receive events over HTTP, **Polling Endpoint** or **FIFO Endpoint** for alternative HTTP delivery, or select a [connector](/platform/developer/webhooks/connectors) to deliver events directly to a warehouse, object store, queue, or analytics tool. ![Webhook integrations list](/images/developer-tools/webhook-integrations.png) 5. Configure the endpoint: - For **Webhook** endpoints, enter the HTTPS URL where you want to receive the payload. You can use tools like [Webhook.site](https://webhook.site/) or [RequestBin](https://requestbin.com/) to generate a temporary URL for testing. - - For connector endpoints (such as ClickHouse, Snowflake, Redshift, SQS, or SNS), enter the connector-specific configuration, table schema (for warehouse connectors), and transformation code. See the [webhook connectors](/platform/developer/webhooks/connectors) guide for details. + - For connector endpoints (such as BigQuery, S3, SQS, or Segment), enter the connector-specific configuration, table schema (for warehouse connectors), and transformation code. See the [webhook connectors](/platform/developer/webhooks/connectors) guide for details. 6. In the **Description** field, enter a description to identify this webhook. 7. Next, select the specific event types you want this endpoint to subscribe to. ![Add webhook](/images/developer-tools/add-webhook.png) diff --git a/docs/platform/developer/webhooks/connectors.mdx b/docs/platform/developer/webhooks/connectors.mdx index 1ca4ca664ad..9316d6ef6ae 100644 --- a/docs/platform/developer/webhooks/connectors.mdx +++ b/docs/platform/developer/webhooks/connectors.mdx @@ -1,9 +1,9 @@ --- title: 'Webhook connectors' -description: "Send Novu webhook events directly to data warehouses, analytics databases, and AWS messaging services using webhook connectors for downstream processing." +description: "Send Novu webhook events directly to data warehouses, object storage, messaging services, Segment, and OpenTelemetry collectors." --- -In addition to standard HTTP webhook endpoints, Novu supports webhook connectors that deliver events directly to third-party services. Use them to route Novu events to data warehouses, analytics databases, and AWS messaging services without building a custom receiver. +In addition to standard HTTP webhook endpoints, Novu supports webhook connectors that deliver events directly to third-party services. Use them to route Novu events to data warehouses, object storage, messaging queues, analytics tools, and observability collectors without building a custom receiver. Novu webhook delivery is powered by [Svix](https://docs.svix.com/introduction). Connector endpoints use Svix [transformations](https://docs.svix.com/transformations) to shape each event before it is written to your destination. @@ -40,11 +40,19 @@ Novu webhook delivery is powered by [Svix](https://docs.svix.com/introduction). - For **data warehouse connectors** (ClickHouse, Snowflake, Redshift): define the **table schema** and write a **transformation** that maps Novu webhook payloads to rows matching that schema. See [Data warehouse connectors](#data-warehouse-connectors). + For **data warehouse connectors** (ClickHouse, Snowflake, Redshift, BigQuery, Postgres): define the **table schema** and write a **transformation** that maps Novu webhook payloads to rows matching that schema. See [Data warehouse connectors](#data-warehouse-connectors). + + + + For **object storage connectors** (Amazon S3, Azure Blob Storage, Google Cloud Storage): review the **transformation** that controls object key and file format. See [Object storage connectors](#object-storage-connectors). - For **message connectors** (Amazon SQS, Amazon SNS): review and customize the **transformation** that shapes the message body sent to the queue or topic. See [Message connectors](#message-connectors). + For **message connectors** (Amazon SQS, Amazon SNS, Amazon EventBridge, RabbitMQ, Google Cloud PubSub): review and customize the **transformation** that shapes the message body. See [Message connectors](#message-connectors). + + + + For **Segment**, **OpenTelemetry Collector**, **Polling Endpoint**, or **FIFO Endpoint**, follow the connector-specific sections below. @@ -56,7 +64,7 @@ Novu webhook delivery is powered by [Svix](https://docs.svix.com/introduction). - Open the endpoint, go to the **Testing** tab, and **Send Example** for each subscribed event type. Confirm delivery succeeds in **Logs**, then verify the row or message appears in your destination. + Open the endpoint, go to the **Testing** tab, and **Send Example** for each subscribed event type. Confirm delivery succeeds in **Logs**, then verify the row, object, or message appears in your destination. @@ -64,17 +72,27 @@ After the endpoint is created, Novu delivers matching events to your configured ## Available connectors -Novu supports the following webhook connectors: +Novu supports the following webhook connectors. The list matches the **Add Endpoint** integrations grid in the dashboard: | Connector | Type | Description | | --- | --- | --- | +| [Polling Endpoint](#polling-endpoint) | HTTP | Receive events by polling an endpoint. | +| [FIFO Endpoint](#fifo-endpoint) | HTTP | Receive events over HTTP in FIFO order. | +| [Amazon S3](#amazon-s3) | Object storage | Store events in an Amazon S3 bucket. | +| [Azure Blob Storage](#azure-blob-storage) | Object storage | Store events in an Azure Blob Storage container. | +| [Google Cloud Storage](#google-cloud-storage) | Object storage | Store events in a Google Cloud Storage bucket. | +| [Google Cloud PubSub](#google-cloud-pubsub) | Message | Send events to a Google Cloud Pub/Sub topic. | +| [OpenTelemetry Collector](#opentelemetry-collector) | Observability | Stream events to an OpenTelemetry Collector. | +| [Amazon SQS](#amazon-sqs) | Message | Send events to an Amazon SQS queue. | +| [Amazon SNS](#amazon-sns) | Message | Send events to an Amazon SNS topic. | +| [Amazon EventBridge](#amazon-eventbridge) | Message | Send events to an AWS EventBridge event bus. | +| [Postgres](#postgres) | Data warehouse | Insert events as rows into a Postgres table. | +| [Google BigQuery](#google-bigquery) | Data warehouse | Store events in a Google Cloud BigQuery table. | | [ClickHouse](#clickhouse) | Data warehouse | Store events in a ClickHouse table. | | [Snowflake](#snowflake) | Data warehouse | Store events in a Snowflake table. | +| [RabbitMQ](#rabbitmq) | Message | Send events to a RabbitMQ exchange. | | [Amazon Redshift](#amazon-redshift) | Data warehouse | Store events in an Amazon Redshift table. | -| [Amazon SQS](#amazon-sqs) | Message | Send events to an Amazon SQS queue. | -| [Amazon SNS](#amazon-sns) | Message | Send events to an Amazon SNS topic. | - -Additional warehouse connectors (such as BigQuery) may appear in the dashboard. They follow the same **table schema + transformation** pattern described below. +| [Segment](#segment) | Analytics | Send events to Segment. | ## Transformations @@ -102,7 +120,7 @@ The handler must return the `webhook` object after modifying `webhook.payload` ( ## Data warehouse connectors -Data warehouse connectors (ClickHouse, Snowflake, Redshift, and similar) insert one row per delivered webhook. Credentials and table location alone are not enough - you must also configure: +Data warehouse connectors (ClickHouse, Snowflake, Redshift, BigQuery, Postgres) insert one row per delivered webhook. Credentials and table location alone are not enough - you must also configure: 1. **Table schema** - Column names and types that match your destination table. 2. **Transformation** - JavaScript that maps each webhook `eventType` and `payload` into a row compatible with that schema. @@ -158,6 +176,7 @@ function handler(webhook) { break; // Add a case for each subscribed event type } + return webhook; } ``` @@ -166,7 +185,7 @@ If deliveries succeed in Novu but no rows appear in your warehouse, the transfor ## Message connectors -Message connectors (Amazon SQS and Amazon SNS) publish each webhook as a queue message or topic notification. Use the transformation to shape the JSON body your consumers receive. +Message connectors (Amazon SQS, Amazon SNS, Amazon EventBridge, RabbitMQ, Google Cloud PubSub) publish each webhook as a queue message, topic notification, or bus event. Use the transformation to shape the JSON body your consumers receive. The default template typically forwards the webhook payload. Customize it if downstream workers expect a specific structure (for example, flattening nested fields or adding metadata). @@ -175,13 +194,47 @@ function handler(webhook) { webhook.payload = { eventType: webhook.eventType, data: webhook.payload, - receivedAt: new Date().toISOString(), }; + + return webhook; +} +``` + +After creating the endpoint, send a test event and confirm the message appears in your SQS queue, SNS topic, EventBridge bus, RabbitMQ exchange, or Pub/Sub topic. + +## Object storage connectors + +Object storage connectors (Amazon S3, Azure Blob Storage, Google Cloud Storage) write webhook batches as objects in your bucket or container. Use the transformation to control: + +- **Object key** - the file name prefix. A timestamp is suffixed so each batch is unique. +- **Format** - `jsonl` (default), `json`, or `raw` (exact file contents as a string). + +```js +function handler(webhook) { + webhook.payload = { + config: { + format: "jsonl", + key: "novu-events", + }, + data: [ + { + eventType: webhook.eventType, + payload: webhook.payload, + }, + ], + }; + return webhook; } ``` -After creating the endpoint, send a test event and confirm the message appears in your SQS queue or SNS topic subscription. +Start from the dashboard template for your storage provider. After creating the endpoint, send a test event and confirm a new object appears in the bucket or container. + +## Analytics connectors + +The [Segment](#segment) connector forwards Novu webhook events to Segment. Provide a Segment **Write Key** in the dashboard, then map each subscribed [event type](/platform/developer/webhooks/event-types) to a Segment Track (or Identify) payload in the transformation. + +This is outbound delivery from Novu to Segment. To send Segment events *into* Novu as workflow triggers, see the [Segment destination function guide](/guides/analytics/segment). ## ClickHouse @@ -390,6 +443,398 @@ The Amazon SNS connector publishes Novu webhook events to an Amazon SNS topic. U +## Google BigQuery + +The Google BigQuery connector stores Novu webhook events in a BigQuery table. Use it when you want notification data available in BigQuery for analytics without a custom ingestion pipeline. + +Without a custom transformation, each webhook is inserted using two columns: `id` and `payload`. Create the table before enabling the endpoint. If you define a table schema and transformation in Novu, the object keys in the transformed row must match your column names. + +```sql +CREATE TABLE `my-gcp-project.my_dataset.events` ( + id STRING, + payload STRING +); +``` + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Project ID | Yes | The GCP project that owns the dataset. | +| Dataset ID | Yes | The BigQuery dataset that contains the table. | +| Table ID | Yes | The table that receives the rows. | +| Credentials | Yes | Google Cloud service account credentials JSON, provided as a string. The service account needs permission to insert rows into the table. | +| Table schema | Yes | Column definitions that match your BigQuery table. | +| Transformation | Yes | JavaScript that maps webhook payloads to rows matching the table schema. | + +### Setup + + + + Create the table before enabling the endpoint. The default mapping uses `id` and `payload` columns. + + + + Grant insert permission on the table and download the JSON key. + + + + In the Novu dashboard, select **BigQuery** when adding a webhook endpoint. + + + + Enter credentials, define the table schema, and review the transformation. + + + + Select event types, create the endpoint, and verify rows with **Send Example**. + + + +## Postgres + +The Postgres connector inserts Novu webhook events as rows in a Postgres table. Use it when you want a durable operational store of notification events in your own database. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Connection settings | Yes | Host, port, database, username, and password shown in the dashboard form. | +| Table name | Yes | The table where events are stored. | +| Table schema | Yes | Column definitions that match your Postgres table. | +| Transformation | Yes | JavaScript that maps webhook payloads to rows matching the table schema. | + +### Setup + + + + Define columns for the webhook event fields you want to store. + + + + Grant `INSERT` on the table to the user Novu connects with. + + + + In the Novu dashboard, select **Postgres** when adding a webhook endpoint. + + + + Enter credentials, define the table schema, and review the transformation. + + + + Select event types, create the endpoint, and verify rows with **Send Example**. + + + +## Amazon EventBridge + +The Amazon EventBridge connector publishes Novu webhook events to an EventBridge event bus. Each webhook is sent as a separate entry. The event `source` is set by the delivery system; `detail-type` comes from the **Detail type** field; `detail` is the transformed message body. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Event bus name | Yes | The name or ARN of the event bus that receives the events. | +| Detail type | No | Free-form string (max 128 characters) used as `detail-type`. Defaults to `application/json`. | +| Region | Yes | The AWS region where the event bus is located. | +| Access key ID | Yes | The AWS access key ID with permission to put events on the bus. | +| Secret access key | Yes | The AWS secret access key for the IAM user. | +| Transformation | Yes | JavaScript that shapes the `detail` body of each EventBridge event. | + +### Setup + + + + Create a bus (or use the default bus) and rules that match the source and detail type. + + + + Grant `events:PutEvents` on the bus. + + + + In the Novu dashboard, select **EventBridge** when adding a webhook endpoint. + + + + Enter your connection credentials and review the transformation before saving. + + + + Select event types, create the endpoint, and verify events with **Send Example**. + + + +## RabbitMQ + +The RabbitMQ connector publishes Novu webhook events to a RabbitMQ exchange using a routing key. Each webhook is published as a separate message. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| URI | Yes | The AMQP connection URI (for example, `amqp://user:password@rabbitmq.example.com:5672/my-vhost`). | +| Routing key | Yes | The routing key each message is published with. | +| Transformation | Yes | JavaScript that shapes the message body published to RabbitMQ. | + +### Setup + + + + Bind a queue to the routing key you will use. + + + + In the Novu dashboard, select **RabbitMQ** when adding a webhook endpoint. + + + + Enter the AMQP URI and routing key, then review the transformation. + + + + Select event types, create the endpoint, and verify messages with **Send Example**. + + + +## Amazon S3 + +The Amazon S3 connector stores Novu webhook batches as objects in an S3 bucket. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Bucket | Yes | The name of the S3 bucket. | +| Region | Yes | The AWS region where the bucket is located. | +| Access key ID | Yes | The AWS access key ID with permission to put objects in the bucket. | +| Secret access key | Yes | The AWS secret access key for the IAM user. | +| Transformation | Yes | JavaScript that sets the object key, format (`jsonl`, `json`, or `raw`), and contents. | + +### Setup + + + + Use this bucket as the destination for Novu webhook event objects. + + + + Grant `s3:PutObject` on the bucket. + + + + In the Novu dashboard, select **Amazon S3** when adding a webhook endpoint. + + + + Enter your connection credentials and review the transformation before saving. + + + + Select event types, create the endpoint, and verify objects with **Send Example**. + + + +## Azure Blob Storage + +The Azure Blob Storage connector stores Novu webhook batches as blobs in a storage container. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Container | Yes | The Azure Blob Storage container name. | +| Account | Yes | The Azure storage account name. | +| Access key | Yes | The storage account access key. | +| Transformation | Yes | JavaScript that sets the blob key, format, and contents. | + +### Setup + + + + Use this container as the destination for Novu webhook event blobs. + + + + In the Novu dashboard, select **Azure Blob Storage** when adding a webhook endpoint. + + + + Enter your connection credentials and review the transformation before saving. + + + + Select event types, create the endpoint, and verify blobs with **Send Example**. + + + +## Google Cloud Storage + +The Google Cloud Storage connector stores Novu webhook batches as objects in a GCS bucket. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Bucket | Yes | The GCS bucket name. | +| Credentials | Yes | Google Cloud service account credentials JSON, provided as a string. The service account needs permission to write objects to the bucket. | +| Transformation | Yes | JavaScript that sets the object key, format, and contents. | + +### Setup + + + + Create a service account with object-create permission on the bucket. + + + + In the Novu dashboard, select **Google Cloud Storage** when adding a webhook endpoint. + + + + Enter credentials and review the transformation before saving. + + + + Select event types, create the endpoint, and verify objects with **Send Example**. + + + +## Google Cloud PubSub + +The Google Cloud PubSub connector publishes Novu webhook events to a Pub/Sub topic. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Project ID | Yes | The GCP project that owns the topic. | +| Topic | Yes | The Pub/Sub topic that receives events. | +| Credentials | Yes | Google Cloud service account credentials JSON, provided as a string. The service account needs permission to publish to the topic. | +| Transformation | Yes | JavaScript that shapes the message body published to the topic. | + +### Setup + + + + Create a subscription as well if you want to inspect messages. + + + + Grant `pubsub.topics.publish` permission. + + + + In the Novu dashboard, select **Google Cloud Pub/Sub** when adding a webhook endpoint. + + + + Enter credentials and review the transformation before saving. + + + + Select event types, create the endpoint, and verify messages with **Send Example**. + + + +## Segment + +The Segment connector sends Novu webhook events to Segment's HTTP API using your **Write Key**. Use the transformation to format each event for [Segment Track](https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#track) (or another Segment API you need). + +```js +function handler(webhook) { + webhook.url = "https://api.segment.io/v1/track"; + webhook.payload = { + userId: webhook.payload.subscriberId, + event: webhook.eventType, + properties: webhook.payload, + }; + + return webhook; +} +``` + +Adjust `userId` and `properties` to match the [event types](/platform/developer/webhooks/event-types) you subscribe to. Workflow events may not include `subscriberId`. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| Write key | Yes | The Segment source write key. | +| Transformation | Yes | JavaScript that maps Novu payloads to Segment API requests. | + +### Setup + + + + Copy the source write key. + + + + In the Novu dashboard, select **Segment** when adding a webhook endpoint. + + + + Enter the write key and review the transformation. + + + + Select event types, create the endpoint, and verify events in the Segment debugger. + + + +## OpenTelemetry Collector + +The OpenTelemetry Collector connector streams Novu webhook events as OpenTelemetry spans to a collector over OTLP. Use it to view notification activity in Datadog, Grafana, and other observability platforms that ingest OTLP traces. + +The collector URL is typically your provider's `OTEL_EXPORTER_OTLP_ENDPOINT` with `/v1/traces` appended. You can add custom headers (for example, an API key) the same way as on a standard webhook endpoint. + +The dashboard ships a default transformation that maps each webhook to a span. Override `startTime` and `endTime` from payload timestamps when you have them, and set `traceIdKey` if you need multiple events grouped into one trace. + +### Configuration + +| Field | Required | Description | +| --- | --- | --- | +| URL | Yes | The OTLP HTTP traces endpoint (for example, `https://otlp.example.com/v1/traces`). | +| Headers | No | Custom headers required by your observability vendor. | +| Transformation | Yes | JavaScript that maps webhooks to OpenTelemetry spans. | + +### Setup + + + + Copy the URL and any required headers from your observability vendor. + + + + In the Novu dashboard, select **OpenTelemetry Collector** when adding a webhook endpoint. + + + + Enter the URL, optional headers, and review the transformation. + + + + Select event types, create the endpoint, and verify spans in your observability platform. + + + +## Polling Endpoint + +A polling endpoint lets you pull Novu webhook events on a schedule instead of exposing a public HTTP receiver. Use it for local testing, private networks, or batching events at the end of a day. + +After you create the endpoint, the dashboard provides a unique polling URL and API key. Poll with a stable **consumer ID** so each worker tracks its own offset. After processing a batch, commit the last message offset so the next poll continues from there. + +Polling endpoints can run alongside push-based webhook and connector endpoints. + +## FIFO Endpoint + +A FIFO endpoint delivers events over HTTP in strict first-in, first-out order. Regular webhook endpoints deliver independently and only guarantee order on a best-effort basis. + +Strict ordering limits throughput: each call waits for a successful acknowledgement of the previous batch. FIFO endpoints therefore deliver webhooks in configurable batch sizes. Use a FIFO endpoint when consumers must process events in the order they were produced (for example, sequential status transitions). + ## Monitoring and troubleshooting Connector endpoints support the same delivery monitoring, retry, and recovery features as standard webhook endpoints. From the endpoint details page in the Novu dashboard, you can: diff --git a/docs/platform/developer/webhooks/webhooks.mdx b/docs/platform/developer/webhooks/webhooks.mdx index 83a128ab2ec..42606d493d4 100644 --- a/docs/platform/developer/webhooks/webhooks.mdx +++ b/docs/platform/developer/webhooks/webhooks.mdx @@ -72,11 +72,12 @@ To start listening to messages, you will need to configure your endpoints. 1. Go to the [Webhooks](https://dashboard.novu.co/webhooks) page in the Novu dashboard. 2. Click **Add Endpoint**. -3. Enter the URL of your endpoint. -4. Add description for this webhook endpoint. -5. Select the event types you want to listen to. -6. Optional: add advanced configuration for your endpoint. -7. Click **Create**. +3. Select an integration. Choose **Webhook** for an HTTPS URL, **Polling Endpoint** or **FIFO Endpoint** for alternative HTTP delivery, or a [connector](/platform/developer/webhooks/connectors) such as BigQuery, S3, or Segment. +4. For a **Webhook** endpoint, enter the URL. For a connector, fill in the destination-specific settings, table schema (for warehouses), and transformation. +5. Add a description for this webhook endpoint. +6. Select the event types you want to listen to. +7. Optional: add advanced configuration for your endpoint. +8. Click **Create**. If your endpoint isn't quite ready to start receiving events, you can use a service like [Webhook.site](https://webhook.site/) or [RequestBin](https://requestbin.com/) to have a unique URL generated for you. From 50b6e869ecc762c6d30a66101284af25852a4bbb Mon Sep 17 00:00:00 2001 From: Pawan Jain Date: Mon, 31 Aug 2026 17:32:14 +0530 Subject: [PATCH 5/7] fix(docs): update broken inbox docs link and ee licence directory info (#12501) --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ec2b1b9f9e4..cdcbc6c4a75 100644 --- a/README.md +++ b/README.md @@ -121,12 +121,12 @@ npx novu@latest connect ## Embeddable Inbox component -Using the Novu API and admin panel, you can easily add a real-time notification center to your web app without building it yourself. You can use our [React](https://docs.novu.co/inbox/react/get-started?utm_source=github&utm_medium=readme&utm_campaign=react-starter-link), or build your own via our API and SDK. React native, Vue, and Angular are coming soon. +Using the Novu API and admin panel, you can easily add a real-time notification center to your web app without building it yourself. You can use our [React](https://docs.novu.co/platform/quickstart/react?utm_source=github&utm_medium=readme&utm_campaign=react-starter-link), or build your own via our API and SDK. React native, Vue, and Angular are coming soon.
Novu's Embeddable Inbox components -Read more about how to add a [notification center Inbox](https://docs.novu.co/inbox/react/get-started?utm_source=github&utm_medium=readme&utm_campaign=read-more-react-link) to your app. +Read more about how to add a [notification center Inbox](https://docs.novu.co/platform/quickstart/react?utm_source=github&utm_medium=readme&utm_campaign=read-more-react-link) to your app.
@@ -251,7 +251,7 @@ Expand a channel below to browse supported providers. | Provider | | --- | -| [Novu Inbox](https://docs.novu.co/inbox/react/get-started?utm_source=github&utm_medium=repository&utm_campaign=inbox-channel-link) | +| [Novu Inbox](https://docs.novu.co/platform/quickstart/react?utm_source=github&utm_medium=repository&utm_campaign=inbox-channel-link) | @@ -275,9 +275,7 @@ Novu is a commercial open source company, which means some parts of this open so The following modules and folders are licensed under the enterprise license: -- `enterprise` folder at the root of the project and all of their subfolders and modules -- `apps/web/src/ee` folder and all of their subfolders and modules -- `apps/dashboard/src/ee` folder and all of their subfolders and modules +- `enterprise` folder at the root of the project and all of its subfolders and modules ## 💪 Thanks to all of our contributors From d50414c42ef2622e399ea87d6a7c8b35012bcaf4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 14:49:43 +0200 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=9A=80=20Release=20v3.19.1=20-=20@nov?= =?UTF-8?q?u/js,@novu/react,@novu/nextjs=20(#12507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ChmaraX Co-authored-by: Adam Chmara --- packages/js/CHANGELOG.md | 28 ++++++++++++++++++++++++++++ packages/js/package.json | 2 +- packages/nextjs/CHANGELOG.md | 4 ++++ packages/nextjs/package.json | 2 +- packages/react/CHANGELOG.md | 21 +++++++++++++++++++++ packages/react/package.json | 2 +- 6 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/js/CHANGELOG.md b/packages/js/CHANGELOG.md index 74519390290..fca2f55da58 100644 --- a/packages/js/CHANGELOG.md +++ b/packages/js/CHANGELOG.md @@ -1,3 +1,31 @@ +## v3.19.1 (2026-08-31) + +### 🚀 Features + +- **js,react:** lazy-load Agent Chat so Inbox bundles stay isolated fixes NV-8698 ([#12466](https://github.com/novuhq/novu/pull/12466)) +- **api-service,js:** persist and deliver custom agent data end-to-end fixes NV-8646 ([#12444](https://github.com/novuhq/novu/pull/12444)) +- **react,js:** rebuild useAgentChat on conversation runtime fixes NV-8641 ([#12438](https://github.com/novuhq/novu/pull/12438)) +- **js:** improve agent-chat public types and runtime validation fixes NV-8644 ([#12420](https://github.com/novuhq/novu/pull/12420)) +- **js:** add agent conversation runtime with immutable snapshots fixes NV-8640 ([#12415](https://github.com/novuhq/novu/pull/12415)) +- **api-service,js:** expose Agent Chat tool trust actions fixes NV-8594 ([#12365](https://github.com/novuhq/novu/pull/12365)) +- **api-service,js:** deliver Agent Chat card parts and sendAction for button clicks fixes NV-8600 ([#12353](https://github.com/novuhq/novu/pull/12353)) +- **dashboard:** add in-dashboard Agent Chat tester for onboarding fixes NV-8590 ([#12338](https://github.com/novuhq/novu/pull/12338)) +- **api-service,js:** emit MCP connection events for agent chat fixes NV-8578 ([#12321](https://github.com/novuhq/novu/pull/12321)) +- **api-service,js,react:** optional agentHash HMAC gate for web chat fixes NV-8442 ([#12303](https://github.com/novuhq/novu/pull/12303)) +- **js,react,api:** wire typing indicator through agent chat fixes NV-8569 ([#12297](https://github.com/novuhq/novu/pull/12297)) +- **js,react:** add agent-chat hook callbacks via a store change descriptor fixes NV-8445 ([#12293](https://github.com/novuhq/novu/pull/12293)) +- **js,react,api:** wire agent-chat live WS, status, fetchMore, approvals fixes NV-8445 ([#12292](https://github.com/novuhq/novu/pull/12292)) + +### 🩹 Fixes + +- **js,react:** correct agent-chat pagination state and run-error exposure fixes NV-8638 ([#12414](https://github.com/novuhq/novu/pull/12414)) +- **agent-chat:** await ingress processing and fix Agent Chat runtime bugs fixes NV-8593 ([#12392](https://github.com/novuhq/novu/pull/12392)) +- **api-service,js:** sync 402 on web chat accept when plan limits block fixes NV-8575 ([#12307](https://github.com/novuhq/novu/pull/12307)) + +### ❤️ Thank You + +- Adam Chmara @ChmaraX + ## v3.19.0 (2026-08-07) ### 🚀 Features diff --git a/packages/js/package.json b/packages/js/package.json index 319c6647935..cfcb471fd74 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@novu/js", - "version": "3.19.0", + "version": "3.19.1", "repository": { "type": "git", "url": "https://github.com/novuhq/novu", diff --git a/packages/nextjs/CHANGELOG.md b/packages/nextjs/CHANGELOG.md index 4cdac0c1b2d..4fd3b09cd18 100644 --- a/packages/nextjs/CHANGELOG.md +++ b/packages/nextjs/CHANGELOG.md @@ -1,3 +1,7 @@ +## v3.19.1 (2026-08-31) + +This was a version bump only for @novu/nextjs to align it with other projects, there were no code changes. + ## v3.19.0 (2026-08-07) This was a version bump only for @novu/nextjs to align it with other projects, there were no code changes. diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 48afaf9c4ad..71c9c98d51c 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@novu/nextjs", - "version": "3.19.0", + "version": "3.19.1", "repository": { "type": "git", "url": "https://github.com/novuhq/novu", diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 909fe06dab9..89a4ad43cdc 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,3 +1,24 @@ +## v3.19.1 (2026-08-31) + +### 🚀 Features + +- **js,react:** lazy-load Agent Chat so Inbox bundles stay isolated fixes NV-8698 ([#12466](https://github.com/novuhq/novu/pull/12466)) +- **react,js:** rebuild useAgentChat on conversation runtime fixes NV-8641 ([#12438](https://github.com/novuhq/novu/pull/12438)) +- **api-service,js:** deliver Agent Chat card parts and sendAction for button clicks fixes NV-8600 ([#12353](https://github.com/novuhq/novu/pull/12353)) +- **api-service,js:** emit MCP connection events for agent chat fixes NV-8578 ([#12321](https://github.com/novuhq/novu/pull/12321)) +- **js,react,api:** wire typing indicator through agent chat fixes NV-8569 ([#12297](https://github.com/novuhq/novu/pull/12297)) +- **js,react,api:** wire agent-chat live WS, status, fetchMore, approvals fixes NV-8445 ([#12292](https://github.com/novuhq/novu/pull/12292)) + +### 🩹 Fixes + +- **react:** prevent Web Chat Next.js hydration mismatch fixes NV-8709 ([#12480](https://github.com/novuhq/novu/pull/12480)) +- **js,react:** harden agent-chat reconnect recovery and surface failures fixes NV-8639 ([#12417](https://github.com/novuhq/novu/pull/12417)) +- **js,react:** correct agent-chat pagination state and run-error exposure fixes NV-8638 ([#12414](https://github.com/novuhq/novu/pull/12414)) + +### ❤️ Thank You + +- Adam Chmara @ChmaraX + ## v3.19.0 (2026-08-07) ### 🚀 Features diff --git a/packages/react/package.json b/packages/react/package.json index 5e99b6b370d..eccbcb1d117 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@novu/react", - "version": "3.19.0", + "version": "3.19.1", "repository": { "type": "git", "url": "https://github.com/novuhq/novu", From b5a70483f857861adc2a798f4c8035d226ecd67f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:29:17 +0200 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=9A=80=20Release=20v2.13.1=20-=20@nov?= =?UTF-8?q?u/framework=20(#12508)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ChmaraX --- apps/api/package.json | 2 +- apps/dashboard/package.json | 2 +- apps/inbound-mail/package.json | 2 +- apps/webhook/package.json | 2 +- apps/worker/package.json | 2 +- apps/ws/package.json | 2 +- enterprise/packages/ai/package.json | 2 +- enterprise/packages/api/package.json | 2 +- enterprise/packages/auth/package.json | 2 +- enterprise/packages/billing/package.json | 2 +- enterprise/packages/translation/package.json | 2 +- libs/application-generic/package.json | 2 +- libs/notifications/package.json | 2 +- packages/framework/CHANGELOG.md | 41 ++++++++++++++++++++ packages/framework/package.json | 2 +- packages/novu/package.json | 2 +- 16 files changed, 56 insertions(+), 15 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index ae609f96a0f..67faa74147f 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@novu/api-service", - "version": "3.19.0", + "version": "3.19.1", "description": "description", "author": "", "private": "true", diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index cb921ade6ba..8695da33670 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,7 +1,7 @@ { "name": "@novu/dashboard", "private": true, - "version": "3.19.2", + "version": "3.19.3", "type": "module", "portless": { "name": "dashboard.novu" diff --git a/apps/inbound-mail/package.json b/apps/inbound-mail/package.json index 1856db40665..410478f146b 100644 --- a/apps/inbound-mail/package.json +++ b/apps/inbound-mail/package.json @@ -1,6 +1,6 @@ { "name": "@novu/inbound-mail", - "version": "3.19.0", + "version": "3.19.1", "description": "", "author": "", "private": true, diff --git a/apps/webhook/package.json b/apps/webhook/package.json index c6e1faa8505..9b8fa2052f1 100644 --- a/apps/webhook/package.json +++ b/apps/webhook/package.json @@ -1,6 +1,6 @@ { "name": "@novu/webhook", - "version": "3.18.1", + "version": "3.18.2", "description": "", "author": "", "private": true, diff --git a/apps/worker/package.json b/apps/worker/package.json index fc64fd8e8a9..efce5b20f06 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -1,6 +1,6 @@ { "name": "@novu/worker", - "version": "3.19.0", + "version": "3.19.1", "description": "description", "author": "", "private": "true", diff --git a/apps/ws/package.json b/apps/ws/package.json index 39216493a17..4d740b365b3 100644 --- a/apps/ws/package.json +++ b/apps/ws/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ws", - "version": "3.19.0", + "version": "3.19.1", "description": "", "author": "", "private": true, diff --git a/enterprise/packages/ai/package.json b/enterprise/packages/ai/package.json index 9d58bfd58b1..cea84710495 100644 --- a/enterprise/packages/ai/package.json +++ b/enterprise/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-ai", - "version": "2.0.2", + "version": "2.0.3", "private": true, "main": "dist/index.js", "scripts": { diff --git a/enterprise/packages/api/package.json b/enterprise/packages/api/package.json index 62e9ebe10fc..b36f7c522cb 100644 --- a/enterprise/packages/api/package.json +++ b/enterprise/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-api", - "version": "2.0.2", + "version": "2.0.3", "private": true, "main": "dist/index.js", "scripts": { diff --git a/enterprise/packages/auth/package.json b/enterprise/packages/auth/package.json index 492309ba1e5..db8cb212de4 100644 --- a/enterprise/packages/auth/package.json +++ b/enterprise/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-auth", - "version": "2.0.16", + "version": "2.0.17", "private": true, "main": "dist/index.js", "scripts": { diff --git a/enterprise/packages/billing/package.json b/enterprise/packages/billing/package.json index 5a26ecc22c1..602de65d6d4 100644 --- a/enterprise/packages/billing/package.json +++ b/enterprise/packages/billing/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-billing", - "version": "2.0.24", + "version": "2.0.26", "private": true, "main": "dist/index.js", "scripts": { diff --git a/enterprise/packages/translation/package.json b/enterprise/packages/translation/package.json index 864c14fd828..7cb57c60281 100644 --- a/enterprise/packages/translation/package.json +++ b/enterprise/packages/translation/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-translation", - "version": "2.0.16", + "version": "2.0.17", "private": true, "main": "dist/index.js", "scripts": { diff --git a/libs/application-generic/package.json b/libs/application-generic/package.json index 811e3523379..196e068ec2c 100644 --- a/libs/application-generic/package.json +++ b/libs/application-generic/package.json @@ -1,6 +1,6 @@ { "name": "@novu/application-generic", - "version": "3.19.0", + "version": "3.19.1", "description": "Generic backend code used inside of Novu's different services", "main": "build/main/index.js", "typings": "build/main/index.d.ts", diff --git a/libs/notifications/package.json b/libs/notifications/package.json index 69418377cc6..6e1a348139c 100644 --- a/libs/notifications/package.json +++ b/libs/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@novu/notifications", - "version": "1.0.12", + "version": "1.0.13", "description": "Novu notification templates and workflows", "main": "build/main/index.js", "typings": "build/main/index.d.ts", diff --git a/packages/framework/CHANGELOG.md b/packages/framework/CHANGELOG.md index 08f314a8668..fd9dc657f7a 100644 --- a/packages/framework/CHANGELOG.md +++ b/packages/framework/CHANGELOG.md @@ -1,3 +1,44 @@ +## v2.13.1 (2026-08-31) + +### 🚀 Features + +- **api-service,framework,human:** hitl multi-recipient to and invite fixes NV-8697 ([#12475](https://github.com/novuhq/novu/pull/12475)) +- **api-service,framework:** hitl agent context helpers fixes NV-8677 ([#12457](https://github.com/novuhq/novu/pull/12457)) +- **api-service,framework:** agents vision and file support fixes NV-7810 ([#12423](https://github.com/novuhq/novu/pull/12423)) +- **api-service:** structured workflow-origin hydration for agents fixes NV-8608 ([#12371](https://github.com/novuhq/novu/pull/12371)) +- **js,react,api:** headless useAgentChat send + open/resume fixes NV-8445 ([#12271](https://github.com/novuhq/novu/pull/12271)) +- **dashboard:** hide empty subscriber credential cards fixes NV-8338 ([#12015](https://github.com/novuhq/novu/pull/12015)) +- **dashboard,api-service:** add workflow agent assignment fixes NV-8422 ([#12139](https://github.com/novuhq/novu/pull/12139)) +- **api-service,worker,framework,providers:** rich chat card delivery flow fixes NV-8386 ([#12142](https://github.com/novuhq/novu/pull/12142)) +- **providers:** add Grafana endpoint-routed tool provider fixes NV-8455 ([#12101](https://github.com/novuhq/novu/pull/12101)) +- **shared:** chat provider content overrides with Slack schema fixes NV-8397 ([#12103](https://github.com/novuhq/novu/pull/12103)) +- **providers:** tool-webhook static and dynamic delivery modes fixes NV-8358 ([#12045](https://github.com/novuhq/novu/pull/12045)) +- **framework,api:** emit AgentEvents from self-hosted agents via ingest fixes NV-8360 ([#12062](https://github.com/novuhq/novu/pull/12062)) +- **dashboard,api-service,js,react,framework:** novu copilot agent for slack fixes NV-8316 ([#11973](https://github.com/novuhq/novu/pull/11973)) +- **providers:** add Ruach SMS provider integration and configuration fixes NV-8351 ([#11937](https://github.com/novuhq/novu/pull/11937)) +- **dashboard:** add opt-in per-provider tool content overrides fixes NV-8331 ([#11997](https://github.com/novuhq/novu/pull/11997)) +- **shared:** add Tool channel with PagerDuty, Opsgenie and Webhook providers fixes NV-8284 ([#11923](https://github.com/novuhq/novu/pull/11923)) + +### 🩹 Fixes + +- **api-service:** add translations support to sender name and preheader fields fixes NV-8691 ([#12458](https://github.com/novuhq/novu/pull/12458)) +- **docs:** enhance agent communication documentation with new channels and capabilities ([#12188](https://github.com/novuhq/novu/pull/12188)) +- **novu:** resolve LangChain Turbopack dynamic import failure fixes NV-8430 ([#12126](https://github.com/novuhq/novu/pull/12126)) +- **worker:** wire steps namespace into HTTP request compile context fixes NV-8364 ([#12052](https://github.com/novuhq/novu/pull/12052)) +- **framework:** resolve next/server ESM import for Next.js fixes NV-8366 ([#12053](https://github.com/novuhq/novu/pull/12053)) +- **framework:** digest filter renders "and 0 others" when maxNames covers all items ([#11984](https://github.com/novuhq/novu/pull/11984)) + +### ❤️ Thank You + +- Adam Chmara @ChmaraX +- David Onifade @zyzer01 +- Dima Grossman @scopsy +- George Djabarov @djabarovgeorge +- Nikita Grossman @nikitagrossman +- Pawan Jain +- Paweł Tymczuk @LetItRock +- WinkleMad @winklemad + ## v2.13.0 (2026-07-16) ### 🚀 Features diff --git a/packages/framework/package.json b/packages/framework/package.json index 7180d29d2c9..85f05f17597 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -1,6 +1,6 @@ { "name": "@novu/framework", - "version": "2.13.0", + "version": "2.13.1", "description": "The Code-First Notifications Workflow SDK.", "main": "./dist/cjs/index.cjs", "types": "./dist/cjs/index.d.cts", diff --git a/packages/novu/package.json b/packages/novu/package.json index 12bda9c7707..56366a1891f 100644 --- a/packages/novu/package.json +++ b/packages/novu/package.json @@ -1,6 +1,6 @@ { "name": "novu", - "version": "2.21.0-rc.5", + "version": "2.21.0", "description": "Novu CLI. Run Novu Studio and sync workflows with Novu Cloud", "main": "src/index.js", "publishConfig": {