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
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<div align="center">
<img width="4800" height="2700" alt="Novu's Embeddable Inbox components" src="https://github.com/user-attachments/assets/00224c75-7ed0-4e19-b6fd-2a0bdced6258" />

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.

</div>

Expand Down Expand Up @@ -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) |

</details>

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@novu/api-service",
"version": "3.19.0",
"version": "3.19.1",
"description": "description",
"author": "",
"private": "true",
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
Original file line number Diff line number Diff line change
@@ -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');

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@novu/dashboard",
"private": true,
"version": "3.19.2",
"version": "3.19.3",
"type": "module",
"portless": {
"name": "dashboard.novu"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,6 @@ const CONTEXTUAL_GETTING_STARTED: Partial<Record<RouteContext, SuggestionItem[]>
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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
className="border-stroke-soft bg-bg-white pointer-events-none flex w-full flex-col gap-4 rounded-lg border p-2.75"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,3 @@ export function isChatPreviewSupported(providerId: string): boolean {
return getChatPreviewSkin(providerId).isSupported;
}

export function getChatShell(providerId: string): ComponentType<ChatShellProps> {
return getChatPreviewSkin(providerId).Shell;
}

export function getChatContentSkeleton(providerId: string): ComponentType {
return getChatPreviewSkin(providerId).ContentSkeleton;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
className="border-stroke-soft bg-bg-white pointer-events-none flex w-full flex-col gap-5 rounded-lg border p-2.75"
Expand Down
16 changes: 6 additions & 10 deletions apps/dashboard/src/hooks/use-web-chat-prompt.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down
Loading
Loading