diff --git a/apps/api/package.json b/apps/api/package.json index a4a65b55bef..3eb206270b9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -66,6 +66,7 @@ "@novu/framework": "workspace:*", "@novu/maily-render": "workspace:*", "@novu/notifications": "workspace:*", + "@novu/chat-adapter-email": "workspace:*", "@novu/shared": "workspace:*", "@novu/stateless": "workspace:*", "@novu/testing": "workspace:*", diff --git a/apps/api/src/app/agents/agents.controller.ts b/apps/api/src/app/agents/agents.controller.ts index 0610b6a66e0..b8e2e767fc1 100644 --- a/apps/api/src/app/agents/agents.controller.ts +++ b/apps/api/src/app/agents/agents.controller.ts @@ -56,6 +56,8 @@ import { ListAgentsCommand } from './usecases/list-agents/list-agents.command'; import { ListAgents } from './usecases/list-agents/list-agents.usecase'; import { RemoveAgentIntegrationCommand } from './usecases/remove-agent-integration/remove-agent-integration.command'; import { RemoveAgentIntegration } from './usecases/remove-agent-integration/remove-agent-integration.usecase'; +import { SendAgentTestEmailCommand } from './usecases/send-agent-test-email/send-agent-test-email.command'; +import { SendAgentTestEmail } from './usecases/send-agent-test-email/send-agent-test-email.usecase'; import { UpdateAgentCommand } from './usecases/update-agent/update-agent.command'; import { UpdateAgent } from './usecases/update-agent/update-agent.usecase'; import { UpdateAgentIntegrationCommand } from './usecases/update-agent-integration/update-agent-integration.command'; @@ -79,7 +81,8 @@ export class AgentsController { private readonly listAgentIntegrationsUsecase: ListAgentIntegrations, private readonly updateAgentIntegrationUsecase: UpdateAgentIntegration, private readonly removeAgentIntegrationUsecase: RemoveAgentIntegration, - private readonly listAgentEmojiUsecase: ListAgentEmoji + private readonly listAgentEmojiUsecase: ListAgentEmoji, + private readonly sendAgentTestEmailUsecase: SendAgentTestEmail ) {} @Get('/emoji') @@ -257,6 +260,31 @@ export class AgentsController { ); } + @Post('/:identifier/test-email') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Send a test email to the agent inbound address', + description: + 'Sends a test email to the configured inbound address using the agent outbound provider (or the Novu demo integration as fallback). Used to verify the inbound email pipeline.', + }) + @ApiNotFoundResponse({ + description: 'The agent was not found.', + }) + @RequirePermissions(PermissionsEnum.AGENT_WRITE) + sendAgentTestEmail( + @UserSession() user: UserSessionData, + @Param('identifier') identifier: string + ): Promise<{ success: boolean }> { + return this.sendAgentTestEmailUsecase.execute( + SendAgentTestEmailCommand.create({ + userId: user._id, + environmentId: user.environmentId, + organizationId: user.organizationId, + agentIdentifier: identifier, + }) + ); + } + @Put('/:identifier/bridge') @ApiResponse(AgentResponseDto) @ApiOperation({ diff --git a/apps/api/src/app/agents/services/chat-sdk.service.ts b/apps/api/src/app/agents/services/chat-sdk.service.ts index fde2282214e..2f647c4c759 100644 --- a/apps/api/src/app/agents/services/chat-sdk.service.ts +++ b/apps/api/src/app/agents/services/chat-sdk.service.ts @@ -1,6 +1,8 @@ import { BadRequestException, Injectable, OnModuleDestroy } from '@nestjs/common'; -import { CacheService, PinoLogger } from '@novu/application-generic'; +import { CacheService, decryptCredentials, MailFactory, PinoLogger } from '@novu/application-generic'; +import { IntegrationRepository } from '@novu/dal'; import type { SentMessageInfo } from '@novu/framework'; +import { ChannelTypeEnum, EmailProviderIdEnum, type IEmailOptions } from '@novu/shared'; import type { AdapterPostableMessage, Chat, EmojiValue, Message, ReactionEvent, Thread } from 'chat'; import { Request as ExpressRequest, Response as ExpressResponse } from 'express'; import { LRUCache } from 'lru-cache'; @@ -12,6 +14,13 @@ import { sendWebResponse, toWebRequest } from '../utils/express-to-web-request'; import { AgentConfigResolver, ResolvedAgentConfig } from './agent-config-resolver.service'; import { AgentInboundHandler } from './agent-inbound-handler.service'; +/** Ensure a Message-ID value is wrapped in RFC 5322 angle brackets. */ +function wrapMsgId(id: string): string { + const trimmed = id.trim(); + + return trimmed.startsWith('<') && trimmed.endsWith('>') ? trimmed : `<${trimmed}>`; +} + /** * ICredentials field mapping per platform adapter: * @@ -58,7 +67,8 @@ export class ChatSdkService implements OnModuleDestroy { private readonly logger: PinoLogger, private readonly cacheService: CacheService, private readonly agentConfigResolver: AgentConfigResolver, - private readonly inboundHandler: AgentInboundHandler + private readonly inboundHandler: AgentInboundHandler, + private readonly integrationRepository: IntegrationRepository ) { this.instances = new LRUCache({ max: MAX_CACHED_INSTANCES, @@ -296,9 +306,75 @@ export class ChatSdkService implements OnModuleDestroy { token: c.token ?? null, phoneNumberIdentification: c.phoneNumberIdentification ?? null, connectionAccessToken: connectionAccessToken ?? null, + replyDomain: c.replyDomain ?? null, + outboundIntegrationId: c.outboundIntegrationId ?? null, }); } + private buildSendEmailCallback( + config: ResolvedAgentConfig, + outboundIntegrationId: string | undefined + ): (params: { to: string; subject: string; html: string; text?: string; inReplyTo?: string; references?: string; messageId?: string }) => Promise<{ messageId: string }> { + return async (params) => { + if (!outboundIntegrationId) { + throw new BadRequestException( + 'Email agent integration requires an outbound email provider (outboundIntegrationId). ' + + 'Configure one in the agent email setup.' + ); + } + + const integration = await this.integrationRepository.findOne({ + _id: outboundIntegrationId, + _environmentId: config.environmentId, + _organizationId: config.organizationId, + channel: ChannelTypeEnum.EMAIL, + }); + + if (!integration) { + throw new BadRequestException( + `Outbound email integration ${outboundIntegrationId} not found or does not belong to this environment` + ); + } + + if (integration.providerId === EmailProviderIdEnum.NovuAgent) { + throw new BadRequestException( + `Integration ${outboundIntegrationId} is the inbound NovuAgent provider and cannot be used as an outbound sender` + ); + } + + if (!integration.active) { + throw new BadRequestException( + `Outbound email integration ${outboundIntegrationId} (${integration.providerId}) is inactive` + ); + } + + const decrypted = decryptCredentials(integration.credentials); + const mailFactory = new MailFactory(); + const handler = mailFactory.getHandler( + { ...integration, credentials: decrypted }, + config.credentials.replyDomain + ); + + const mailOptions: IEmailOptions = { + to: [params.to], + subject: params.subject, + html: params.html, + text: params.text, + from: config.credentials.replyDomain, + senderName: config.credentials.senderName || undefined, + headers: { + ...(params.messageId ? { 'Message-ID': wrapMsgId(params.messageId) } : {}), + ...(params.inReplyTo ? { 'In-Reply-To': wrapMsgId(params.inReplyTo) } : {}), + ...(params.references ? { References: params.references.split(/\s+/).filter(Boolean).map(wrapMsgId).join(' ') } : {}), + }, + }; + + const result = await handler.send(mailOptions); + + return { messageId: result?.id || params.messageId || '' }; + }; + } + private async createChatInstance( instanceKey: string, platform: AgentPlatformEnum, @@ -397,6 +473,26 @@ export class ChatSdkService implements OnModuleDestroy { }), }; } + case AgentPlatformEnum.EMAIL: { + const { replyDomain, senderName, outboundIntegrationId } = credentials; + + if (!replyDomain || !credentials.secretKey) { + throw new BadRequestException( + 'Email agent integration requires replyDomain and secretKey credentials' + ); + } + + const { createNovuEmailAdapter } = await esmImport('@novu/chat-adapter-email'); + + return { + email: createNovuEmailAdapter({ + fromAddress: replyDomain, + fromName: senderName, + signingSecret: credentials.secretKey, + sendEmail: this.buildSendEmailCallback(config, outboundIntegrationId), + }), + }; + } default: throw new BadRequestException(`Unsupported platform: ${platform}`); } diff --git a/apps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.ts b/apps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.ts index 34cf66cd114..a67356ebfc1 100644 --- a/apps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.ts +++ b/apps/api/src/app/agents/usecases/add-agent-integration/add-agent-integration.usecase.ts @@ -1,5 +1,9 @@ +import { randomBytes } from 'node:crypto'; import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { encryptSecret } from '@novu/application-generic'; import { AgentIntegrationRepository, AgentRepository, IntegrationRepository } from '@novu/dal'; +import { EmailProviderIdEnum } from '@novu/shared'; + import type { AgentIntegrationResponseDto } from '../../dtos'; import { toAgentIntegrationResponse } from '../../mappers/agent-response.mapper'; import { AddAgentIntegrationCommand } from './add-agent-integration.command'; @@ -53,6 +57,10 @@ export class AddAgentIntegration { throw new ConflictException('This integration is already linked to the agent.'); } + if (integration.providerId === EmailProviderIdEnum.NovuAgent) { + await this.prepareNovuEmailIntegration(agent._id, integration._id, command); + } + const link = await this.agentIntegrationRepository.create({ _agentId: agent._id, _integrationId: integration._id, @@ -62,4 +70,62 @@ export class AddAgentIntegration { return toAgentIntegrationResponse(link, integration); } + + /** + * Enforces the singleton constraint (one NovuAgent email integration per + * agent) and seeds the `secretKey` credential the email adapter needs for + * HMAC verification of inbound webhook payloads. + */ + private async prepareNovuEmailIntegration( + agentId: string, + integrationId: string, + command: AddAgentIntegrationCommand + ): Promise { + await this.enforceSingletonEmail(agentId, command); + await this.seedEmailSecretKey(integrationId, command.environmentId, command.organizationId); + } + + private async enforceSingletonEmail( + agentId: string, + command: AddAgentIntegrationCommand + ): Promise { + const existingLinks = await this.agentIntegrationRepository.find( + { + _agentId: agentId, + _environmentId: command.environmentId, + _organizationId: command.organizationId, + }, + '*' + ); + + if (existingLinks.length === 0) return; + + const linkedIntegrationIds = existingLinks.map((link) => link._integrationId); + const linkedEmailIntegrations = await this.integrationRepository.find( + { + _id: { $in: linkedIntegrationIds }, + _environmentId: command.environmentId, + _organizationId: command.organizationId, + providerId: EmailProviderIdEnum.NovuAgent, + }, + '_id' + ); + + if (linkedEmailIntegrations.length > 0) { + throw new ConflictException('Only one email integration per agent is allowed.'); + } + } + + private async seedEmailSecretKey( + integrationId: string, + environmentId: string, + organizationId: string + ): Promise { + const dedicatedSecret = randomBytes(32).toString('hex'); + + await this.integrationRepository.update( + { _id: integrationId, _environmentId: environmentId, _organizationId: organizationId }, + { $set: { 'credentials.secretKey': encryptSecret(dedicatedSecret) } } + ); + } } diff --git a/apps/api/src/app/agents/usecases/index.ts b/apps/api/src/app/agents/usecases/index.ts index 6e84f8af724..9b3eb5ee09c 100644 --- a/apps/api/src/app/agents/usecases/index.ts +++ b/apps/api/src/app/agents/usecases/index.ts @@ -7,6 +7,7 @@ import { ListAgentEmoji } from './list-agent-emoji/list-agent-emoji.usecase'; import { ListAgentIntegrations } from './list-agent-integrations/list-agent-integrations.usecase'; import { ListAgents } from './list-agents/list-agents.usecase'; import { RemoveAgentIntegration } from './remove-agent-integration/remove-agent-integration.usecase'; +import { SendAgentTestEmail } from './send-agent-test-email/send-agent-test-email.usecase'; import { UpdateAgent } from './update-agent/update-agent.usecase'; import { UpdateAgentIntegration } from './update-agent-integration/update-agent-integration.usecase'; @@ -22,4 +23,5 @@ export const USE_CASES = [ UpdateAgentIntegration, RemoveAgentIntegration, HandleAgentReply, + SendAgentTestEmail, ]; diff --git a/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.command.ts b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.command.ts new file mode 100644 index 00000000000..dbac63741b7 --- /dev/null +++ b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.command.ts @@ -0,0 +1,9 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; + +export class SendAgentTestEmailCommand extends EnvironmentWithUserCommand { + @IsString() + @IsNotEmpty() + agentIdentifier: string; +} diff --git a/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts new file mode 100644 index 00000000000..8ac049a0bc2 --- /dev/null +++ b/apps/api/src/app/agents/usecases/send-agent-test-email/send-agent-test-email.usecase.ts @@ -0,0 +1,168 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { decryptCredentials, InstrumentUsecase, MailFactory } from '@novu/application-generic'; +import { AgentIntegrationRepository, AgentRepository, IntegrationRepository } from '@novu/dal'; +import { ChannelTypeEnum, EmailProviderIdEnum, IEmailOptions } from '@novu/shared'; + +import { SendAgentTestEmailCommand } from './send-agent-test-email.command'; + +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +@Injectable() +export class SendAgentTestEmail { + constructor( + private readonly agentRepository: AgentRepository, + private readonly integrationRepository: IntegrationRepository, + private readonly agentIntegrationRepository: AgentIntegrationRepository + ) {} + + @InstrumentUsecase() + async execute(command: SendAgentTestEmailCommand): Promise<{ success: boolean }> { + const agent = await this.agentRepository.findOne( + { + identifier: command.agentIdentifier, + _environmentId: command.environmentId, + _organizationId: command.organizationId, + }, + '*' + ); + + if (!agent) { + throw new NotFoundException(`Agent "${command.agentIdentifier}" not found.`); + } + + const links = await this.agentIntegrationRepository.findLinksForAgents({ + organizationId: command.organizationId, + environmentId: command.environmentId, + agentIds: [agent._id], + }); + + const integrationIds = links.map((l) => l._integrationId).filter(Boolean); + if (integrationIds.length === 0) { + throw new BadRequestException('No email integration linked to this agent.'); + } + + const emailIntegration = await this.integrationRepository.findOne({ + _id: { $in: integrationIds } as unknown as string, + _environmentId: command.environmentId, + _organizationId: command.organizationId, + providerId: EmailProviderIdEnum.NovuAgent, + channel: ChannelTypeEnum.EMAIL, + }); + + if (!emailIntegration) { + throw new BadRequestException('No Novu Email integration found for this agent.'); + } + + const inboundAddress = emailIntegration.credentials?.inboundAddress as string | undefined; + const inboundDomain = emailIntegration.credentials?.inboundDomain as string | undefined; + + if (!inboundAddress || !inboundDomain) { + throw new BadRequestException('Inbound address is not configured. Set the address and domain first.'); + } + + const to = `${inboundAddress}@${inboundDomain}`; + const outboundIntegrationId = emailIntegration.credentials?.outboundIntegrationId as string | undefined; + + const senderIntegration = await this.findSenderIntegration( + command.environmentId, + command.organizationId, + outboundIntegrationId + ); + const mailFactory = new MailFactory(); + const handler = mailFactory.getHandler(senderIntegration, senderIntegration.credentials?.from as string); + + const escapedName = escapeHtml(agent.name); + const mailOptions: IEmailOptions = { + to: [to], + subject: `Test email for agent "${agent.name}"`, + html: [ + '
', + '

Test Email

', + `

`, + 'This is an automated test email sent to verify the inbound email configuration ', + `for agent ${escapedName}.`, + '

', + '

', + 'If your agent processes this email successfully, the connection test has passed.', + '

', + '
', + ].join(''), + from: senderIntegration.credentials?.from as string, + senderName: (senderIntegration.credentials?.senderName as string) || 'Novu', + }; + + await handler.send(mailOptions); + + return { success: true }; + } + + private async findSenderIntegration(environmentId: string, organizationId: string, outboundIntegrationId?: string) { + if (outboundIntegrationId) { + const configured = await this.integrationRepository.findOne({ + _id: outboundIntegrationId, + _environmentId: environmentId, + _organizationId: organizationId, + channel: ChannelTypeEnum.EMAIL, + active: true, + }); + + if (!configured) { + throw new BadRequestException('Configured outbound integration not found or inactive.'); + } + + if (configured.providerId === EmailProviderIdEnum.Novu) { + return { + ...configured, + credentials: { + apiKey: process.env.NOVU_EMAIL_INTEGRATION_API_KEY, + from: 'no-reply@novu.co', + senderName: 'Novu', + ipPoolName: 'Demo', + }, + }; + } + + return { ...configured, credentials: decryptCredentials(configured.credentials ?? {}) }; + } + + const novuDemo = await this.integrationRepository.findOne({ + _environmentId: environmentId, + _organizationId: organizationId, + providerId: EmailProviderIdEnum.Novu, + channel: ChannelTypeEnum.EMAIL, + active: true, + }); + + if (novuDemo) { + return { + ...novuDemo, + credentials: { + apiKey: process.env.NOVU_EMAIL_INTEGRATION_API_KEY, + from: 'no-reply@novu.co', + senderName: 'Novu', + ipPoolName: 'Demo', + }, + }; + } + + const anyEmailProvider = await this.integrationRepository.findOne({ + _environmentId: environmentId, + _organizationId: organizationId, + channel: ChannelTypeEnum.EMAIL, + active: true, + providerId: { $nin: [EmailProviderIdEnum.NovuAgent, EmailProviderIdEnum.Novu] } as unknown as string, + }); + + if (!anyEmailProvider) { + throw new BadRequestException('No active email provider available to send the test email.'); + } + + return { ...anyEmailProvider, credentials: decryptCredentials(anyEmailProvider.credentials ?? {}) }; + } +} diff --git a/apps/api/src/app/agents/utils/provider-to-platform.ts b/apps/api/src/app/agents/utils/provider-to-platform.ts index 213412bd3d8..39bc97a6299 100644 --- a/apps/api/src/app/agents/utils/provider-to-platform.ts +++ b/apps/api/src/app/agents/utils/provider-to-platform.ts @@ -1,10 +1,11 @@ -import { ChatProviderIdEnum } from '@novu/shared'; +import { ChatProviderIdEnum, EmailProviderIdEnum } from '@novu/shared'; import { AgentPlatformEnum } from '../dtos/agent-platform.enum'; const PROVIDER_TO_PLATFORM: Partial> = { [ChatProviderIdEnum.Slack]: AgentPlatformEnum.SLACK, [ChatProviderIdEnum.MsTeams]: AgentPlatformEnum.TEAMS, [ChatProviderIdEnum.WhatsAppBusiness]: AgentPlatformEnum.WHATSAPP, + [EmailProviderIdEnum.NovuAgent]: AgentPlatformEnum.EMAIL, }; export function resolveAgentPlatform(providerId: string): AgentPlatformEnum | null { diff --git a/apps/dashboard/src/api/agents.ts b/apps/dashboard/src/api/agents.ts index e70462ad4ac..3deffeba21b 100644 --- a/apps/dashboard/src/api/agents.ts +++ b/apps/dashboard/src/api/agents.ts @@ -258,6 +258,13 @@ export function removeAgentIntegration( }); } +export async function sendAgentTestEmail( + environment: IEnvironment, + agentIdentifier: string +): Promise<{ success: boolean }> { + return post<{ success: boolean }>(`/agents/${encodeURIComponent(agentIdentifier)}/test-email`, { environment }); +} + export type AgentEmojiEntry = { name: string; unicode: string; diff --git a/apps/dashboard/src/components/agents/agent-integration-guides/email-agent-integration-guide.tsx b/apps/dashboard/src/components/agents/agent-integration-guides/email-agent-integration-guide.tsx new file mode 100644 index 00000000000..45b7fdb4c1c --- /dev/null +++ b/apps/dashboard/src/components/agents/agent-integration-guides/email-agent-integration-guide.tsx @@ -0,0 +1,59 @@ +import { EmailProviderIdEnum } from '@novu/shared'; +import type { AgentIntegrationLink, AgentResponse } from '@/api/agents'; +import { EmailSetupGuide } from '@/components/agents/email-setup-guide'; +import { AgentIntegrationGuideLayout } from './agent-integration-guide-layout'; +import { AgentIntegrationGuideSection } from './agent-integration-guide-section'; + +type EmailAgentIntegrationGuideProps = { + onBack: () => void; + embedded?: boolean; + agent: AgentResponse; + integrationLink?: AgentIntegrationLink; + canRemoveIntegration: boolean; + onRequestRemoveIntegration?: () => void; + isRemovingIntegration?: boolean; +}; + +export function EmailAgentIntegrationGuide({ + onBack, + embedded = false, + agent, + integrationLink, + canRemoveIntegration, + onRequestRemoveIntegration, + isRemovingIntegration, +}: EmailAgentIntegrationGuideProps) { + const isConnected = Boolean(integrationLink?.connectedAt); + const integrationId = integrationLink?.integration?._id; + + return ( + + + {isConnected ? ( +

+ This agent is connected to email. Subscribers can send emails to the configured inbound address to start + conversations, and the agent will reply through the selected outbound provider. +

+ ) : ( +

+ Connect email so this agent can send and receive messages via email. Configure an outbound email provider and + an inbound address below. +

+ )} +
+ {!isConnected && integrationId && ( + + )} +
+ ); +} diff --git a/apps/dashboard/src/components/agents/agent-integration-guides/resolve-agent-integration-guide.tsx b/apps/dashboard/src/components/agents/agent-integration-guides/resolve-agent-integration-guide.tsx index bbdc7111b80..a81b9d1b943 100644 --- a/apps/dashboard/src/components/agents/agent-integration-guides/resolve-agent-integration-guide.tsx +++ b/apps/dashboard/src/components/agents/agent-integration-guides/resolve-agent-integration-guide.tsx @@ -1,8 +1,10 @@ -import { ChatProviderIdEnum } from '@novu/shared'; +import { ChatProviderIdEnum, EmailProviderIdEnum } from '@novu/shared'; import type { AgentIntegrationLink, AgentResponse } from '@/api/agents'; +import { EmailSetupGuide } from '@/components/agents/email-setup-guide'; import { SlackSetupGuide } from '@/components/agents/slack-setup-guide'; import { TeamsSetupGuide } from '@/components/agents/teams-setup-guide'; import { WhatsAppSetupGuide } from '@/components/agents/whatsapp-setup-guide'; +import { EmailAgentIntegrationGuide } from './email-agent-integration-guide'; import { GenericAgentIntegrationGuide } from './generic-agent-integration-guide'; import { SlackAgentIntegrationGuide } from './slack-agent-integration-guide'; import { TeamsAgentIntegrationGuide } from './teams-agent-integration-guide'; @@ -83,6 +85,24 @@ export function ResolveAgentIntegrationGuide({ ); } + if (providerId === EmailProviderIdEnum.NovuAgent && !integrationLink.connectedAt) { + return ; + } + + if (providerId === EmailProviderIdEnum.NovuAgent) { + return ( + + ); + } + return ( void; + embedded?: boolean; +}; + +type OutboundDropdownItem = { + providerId: string; + displayName: string; + integration?: IIntegration; + isDemo: boolean; +}; + +function DemoBadge() { + return ( + + Demo + + ); +} + +const OUTBOUND_EMAIL_PROVIDERS = emailProviderConfigs.filter( + (p) => p.id !== EmailProviderIdEnum.NovuAgent +); + +function buildOutboundItems(allIntegrations: IIntegration[] | undefined): OutboundDropdownItem[] { + const integrationsByProvider = new Map(); + for (const i of allIntegrations ?? []) { + if (i.channel !== ChannelTypeEnum.EMAIL) continue; + if (i.providerId === EmailProviderIdEnum.NovuAgent) continue; + const list = integrationsByProvider.get(i.providerId) ?? []; + list.push(i); + integrationsByProvider.set(i.providerId, list); + } + + const items: OutboundDropdownItem[] = []; + for (const cfg of OUTBOUND_EMAIL_PROVIDERS) { + const existing = integrationsByProvider.get(cfg.id); + const isDemo = cfg.id === EmailProviderIdEnum.Novu; + if (existing?.length) { + for (const integration of existing) { + items.push({ + providerId: cfg.id, + displayName: integration.name || cfg.displayName, + integration, + isDemo, + }); + } + } + if (!isDemo) { + items.push({ providerId: cfg.id, displayName: cfg.displayName, isDemo: false }); + } + } + + return items; +} + +function getItemKey(item: OutboundDropdownItem, index: number): string { + return item.integration ? `${item.providerId}-${item.integration._id}` : `${item.providerId}-new-${index}`; +} + +function OutboundProviderSelect({ + selectedId, + onSelect, +}: { + selectedId: string | undefined; + onSelect: (integrationId: string) => void; +}) { + const [open, setOpen] = useState(false); + const [pendingKey, setPendingKey] = useState(null); + const { integrations } = useFetchIntegrations(); + const { currentEnvironment } = useEnvironment(); + const queryClient = useQueryClient(); + + const items = useMemo(() => buildOutboundItems(integrations), [integrations]); + + const selected = useMemo( + () => (selectedId ? items.find((i) => i.integration?._id === selectedId) : undefined), + [items, selectedId] + ); + + const isBusy = pendingKey !== null; + + const createMutation = useMutation({ + mutationFn: async (vars: { providerId: string; name: string }) => { + const environment = requireEnvironment(currentEnvironment, 'No environment selected'); + const response = await createIntegration( + { + providerId: vars.providerId, + channel: ChannelTypeEnum.EMAIL, + credentials: {}, + configurations: {}, + name: vars.name, + active: true, + _environmentId: environment._id, + }, + environment + ); + + return response.data; + }, + }); + + async function handleSelect(item: OutboundDropdownItem, index: number) { + if (isBusy) return; + if (!currentEnvironment?._id) { + showErrorToast('No environment selected.', 'Cannot select provider'); + + return; + } + + const key = getItemKey(item, index); + setPendingKey(key); + + try { + if (item.integration) { + onSelect(item.integration._id); + } else { + const count = (integrations ?? []).filter((i) => i.providerId === item.providerId).length; + const created = await createMutation.mutateAsync({ + providerId: item.providerId, + name: `${item.displayName} ${count + 1}`, + }); + await queryClient.invalidateQueries({ queryKey: [QueryKeys.fetchIntegrations, currentEnvironment._id] }); + onSelect(created._id); + } + setOpen(false); + } catch (err) { + const message = err instanceof Error ? err.message : 'Could not select provider.'; + showErrorToast(message, 'Selection failed'); + } finally { + setPendingKey(null); + } + } + + return ( +
+
+ Send emails via + +
+ +
+ + + + + + + +
+ + +
+ + No email providers found. + + {items.map((item, index) => { + const key = getItemKey(item, index); + const isRowPending = pendingKey === key; + + return ( + { + void handleSelect(item, index); + }} + className={cn( + 'flex items-center gap-2 rounded-md p-1', + item.integration?._id === selectedId && 'bg-bg-muted' + )} + > +
+ + + {item.displayName} + + {item.isDemo && } +
+ {isRowPending && ( + + )} + {!isRowPending && item.integration && ( + + {item.integration.identifier} + + )} + {!isRowPending && !item.integration && ( + + )} +
+ ); + })} +
+
+
+
+
+
+ + {selected?.isDemo && ( +

+ This is a demo provider for development and testing. Switch to a production provider (e.g. Resend, SendGrid) + before going live. +

+ )} +
+ ); +} + +function InboundAddressConfig({ + localPart, + domainName, + domains, + onLocalPartChange, + onLocalPartBlur, + onDomainChange, +}: { + localPart: string; + domainName: string; + domains: DomainResponse[]; + onLocalPartChange: (v: string) => void; + onLocalPartBlur: () => void; + onDomainChange: (v: string) => void; +}) { + const [domainOpen, setDomainOpen] = useState(false); + const { currentEnvironment } = useEnvironment(); + const navigate = useNavigate(); + + const domainsPath = currentEnvironment?.slug + ? buildRoute(ROUTES.DOMAINS, { environmentSlug: currentEnvironment.slug }) + : ROUTES.INTEGRATIONS; + + const verifiedDomains = domains.filter( + (d) => d.status === DomainStatusEnum.VERIFIED && d.mxRecordConfigured + ); + + return ( +
+
+
+ onLocalPartChange(e.target.value)} + onBlur={onLocalPartBlur} + /> +
+ @ + + + + + + +
+ + +
+ + No domains found. + + {verifiedDomains.map((d) => ( + { + onDomainChange(d.name); + setDomainOpen(false); + }} + className={cn( + 'flex items-center gap-2 rounded-md p-1', + d.name === domainName && 'bg-bg-muted' + )} + > + {d.name} + + ))} + { + setDomainOpen(false); + navigate(domainsPath); + }} + className="flex items-center gap-2 rounded-md p-1" + > + Add domain + + + + +
+
+
+
+

+ + Configure custom domains + + {' by adding them to Novu.'} +

+
+ ); +} + +export function EmailSetupGuide({ + agent, + integrationId, + stepOffset = 1, + onStepsCompleted, + embedded = false, +}: EmailSetupGuideProps) { + const { currentEnvironment } = useEnvironment(); + const { integrations } = useFetchIntegrations(); + const { mutateAsync: updateIntegration } = useUpdateIntegration(); + + const [isCredentialsSidebarOpen, setIsCredentialsSidebarOpen] = useState(false); + const [isCredentialsSaved, setIsCredentialsSaved] = useState(false); + + const testEmailMutation = useMutation({ + mutationFn: async () => { + const environment = requireEnvironment(currentEnvironment, 'No environment selected'); + + return sendAgentTestEmail(environment, agent.identifier); + }, + onSuccess: () => { + showSuccessToast('Test email sent to the configured inbound address.'); + }, + onError: (err) => { + const message = err instanceof Error ? err.message : 'Could not send test email.'; + showErrorToast(message, 'Test email failed'); + }, + }); + + const emailIntegration = useMemo( + () => integrations?.find((i) => i._id === integrationId && i.providerId === EmailProviderIdEnum.NovuAgent), + [integrations, integrationId] + ); + + const serverCredentials = emailIntegration?.credentials ?? {}; + const credentialsRef = useRef>(serverCredentials); + useEffect(() => { + credentialsRef.current = { ...credentialsRef.current, ...serverCredentials }; + }, [emailIntegration]); + + const [outboundId, setOutboundId] = useState(''); + const [localPart, setLocalPart] = useState(''); + const [domainName, setDomainName] = useState(''); + + const hasInitializedFromServer = useRef(false); + useEffect(() => { + if (!emailIntegration || hasInitializedFromServer.current) return; + hasInitializedFromServer.current = true; + const creds = emailIntegration.credentials ?? {}; + if (creds.outboundIntegrationId) setOutboundId(creds.outboundIntegrationId as string); + if (creds.inboundAddress) setLocalPart(creds.inboundAddress as string); + if (creds.inboundDomain) setDomainName(creds.inboundDomain as string); + }, [emailIntegration]); + + const outboundIntegration = useMemo( + () => (outboundId ? integrations?.find((i) => i._id === outboundId) : undefined), + [integrations, outboundId] + ); + + const isOutboundDemo = outboundIntegration?.providerId === EmailProviderIdEnum.Novu; + const needsCredentialsStep = Boolean(outboundIntegration) && !isOutboundDemo; + + const outboundProviderConfig = useMemo( + () => (outboundIntegration ? emailProviderConfigs.find((p) => p.id === outboundIntegration.providerId) : undefined), + [outboundIntegration] + ); + + const domainsQuery = useQuery({ + queryKey: [QueryKeys.fetchDomains, currentEnvironment?._id], + queryFn: () => fetchDomains(requireEnvironment(currentEnvironment, 'No environment selected')), + enabled: Boolean(currentEnvironment), + }); + const domains = domainsQuery.data ?? []; + + const saveQueueRef = useRef>(Promise.resolve()); + + function saveCredentials(patch: Record) { + if (!emailIntegration) return; + + credentialsRef.current = { ...credentialsRef.current, ...patch }; + const snapshot = { ...credentialsRef.current }; + + saveQueueRef.current = saveQueueRef.current + .then(() => + updateIntegration({ + integrationId: emailIntegration._id, + data: { + name: emailIntegration.name, + identifier: emailIntegration.identifier, + active: emailIntegration.active, + primary: emailIntegration.primary ?? false, + credentials: snapshot, + configurations: {}, + check: false, + }, + }) + ) + .then(() => undefined) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : 'Could not save credentials.'; + showErrorToast(message, 'Settings not saved'); + }); + } + + function upsertAgentRoute(address: string, domain: DomainResponse) { + if (!currentEnvironment || !agent._id) return; + + const existingRoutes = domain.routes ?? []; + const hasRoute = existingRoutes.some( + (r) => r.address === address && r.type === DomainRouteTypeEnum.AGENT && r.destination === agent._id + ); + + if (hasRoute) return; + + const updatedRoutes = existingRoutes.filter( + (r) => !(r.address === address && r.type === DomainRouteTypeEnum.AGENT) + ); + + updatedRoutes.push({ address, type: DomainRouteTypeEnum.AGENT, destination: agent._id }); + + updateDomain(domain._id, { routes: updatedRoutes }, currentEnvironment).catch(() => { + showErrorToast('Could not create inbound route on the domain.', 'Route creation failed'); + }); + } + + function handleOutboundSelect(id: string) { + setOutboundId(id); + setIsCredentialsSaved(false); + saveCredentials({ outboundIntegrationId: id }); + } + + function handleLocalPartBlur() { + if (!localPart || localPart === credentialsRef.current.inboundAddress) return; + + const replyDomain = domainName ? `${localPart}@${domainName}` : undefined; + saveCredentials({ inboundAddress: localPart, ...(replyDomain ? { replyDomain } : {}) }); + + if (domainName) { + const domain = domains.find((d) => d.name === domainName); + if (domain) upsertAgentRoute(localPart, domain); + } + } + + function handleDomainChange(name: string) { + setDomainName(name); + const replyDomain = localPart ? `${localPart}@${name}` : undefined; + saveCredentials({ inboundDomain: name, ...(replyDomain ? { replyDomain } : {}) }); + + if (localPart) { + const domain = domains.find((d) => d.name === name); + if (domain) upsertAgentRoute(localPart, domain); + } + } + + const base = stepOffset; + + const credentialsStepIndex = base + 1; + const inboundStepIndex = needsCredentialsStep ? base + 2 : base + 1; + const testStepIndex = inboundStepIndex + 1; + + const firstIncompleteStep = useMemo(() => { + if (!outboundId) return base; + if (needsCredentialsStep && !isCredentialsSaved) return base + 1; + if (!localPart || !domainName) return inboundStepIndex; + + return testStepIndex; + }, [base, outboundId, needsCredentialsStep, isCredentialsSaved, localPart, domainName, inboundStepIndex, testStepIndex]); + + const stepsColumn = ( + <> + + {'Choose which email provider sends outbound replies from your agent. '} + + Manage email providers + + + } + rightContent={ + + } + /> + + {needsCredentialsStep && ( + + {'Paste API keys or credentials from your email provider into the integration. '} + {outboundProviderConfig?.docReference && ( + + View setup guide + + )} + + } + rightContent={ + } + onClick={() => setIsCredentialsSidebarOpen(true)} + > + Configure credentials + + } + /> + )} + + + } + /> + + + ) : ( + + ) + } + disabled={firstIncompleteStep < testStepIndex || testEmailMutation.isPending} + onClick={() => testEmailMutation.mutate()} + > + {testEmailMutation.isPending ? 'Sending...' : 'Send test email'} + + } + /> + + ); + + const listening = ( + + ); + + const credentialsSidebar = outboundId && needsCredentialsStep ? ( + setIsCredentialsSidebarOpen(false)} + onSaveSuccess={() => setIsCredentialsSaved(true)} + /> + ) : null; + + if (embedded) { + return ( +
+
+
+ {stepsColumn} +
+ {listening} + {credentialsSidebar} +
+ ); + } + + return ( + <> + {stepsColumn} + {listening} + {credentialsSidebar} + + ); +} diff --git a/apps/dashboard/src/components/agents/provider-dropdown.tsx b/apps/dashboard/src/components/agents/provider-dropdown.tsx index b36a7022357..827dbfac09f 100644 --- a/apps/dashboard/src/components/agents/provider-dropdown.tsx +++ b/apps/dashboard/src/components/agents/provider-dropdown.tsx @@ -1,6 +1,7 @@ import { CONVERSATIONAL_PROVIDERS, type ConversationalProvider, + EmailProviderIdEnum, type IIntegration, providers as novuProviders, PROVIDER_ID_TO_CHANNEL_MAP, @@ -88,11 +89,14 @@ function buildDropdownItems( } } - supported.push({ - providerId: cp.providerId, - displayName: providerConfig?.displayName || cp.displayName, - comingSoon: false, - }); + const isSingleton = cp.providerId === EmailProviderIdEnum.NovuAgent; + if (!(isSingleton && existing?.length)) { + supported.push({ + providerId: cp.providerId, + displayName: providerConfig?.displayName || cp.displayName, + comingSoon: false, + }); + } } return { supported, comingSoon }; @@ -134,13 +138,27 @@ export function ProviderDropdown({ [integrations] ); + const hasLinkedNovuAgent = useMemo(() => { + if (!linkedIntegrationIds?.size || !integrations?.length) return false; + + return integrations.some( + (i) => i.providerId === EmailProviderIdEnum.NovuAgent && linkedIntegrationIds.has(i._id) + ); + }, [integrations, linkedIntegrationIds]); + const supported = useMemo(() => { - if (!excludeLinked || !linkedIntegrationIds?.size) { - return allSupported; + let items = allSupported; + + if (excludeLinked && linkedIntegrationIds?.size) { + items = items.filter((item) => !item.integration || !linkedIntegrationIds.has(item.integration._id)); + } + + if (hasLinkedNovuAgent) { + items = items.filter((item) => item.providerId !== EmailProviderIdEnum.NovuAgent); } - return allSupported.filter((item) => !item.integration || !linkedIntegrationIds.has(item.integration._id)); - }, [allSupported, excludeLinked, linkedIntegrationIds]); + return items; + }, [allSupported, excludeLinked, linkedIntegrationIds, hasLinkedNovuAgent]); const selected = useMemo(() => { if (selectedIntegrationId) { @@ -248,8 +266,9 @@ export function ProviderDropdown({ onSelect(item.providerId, item.integration); setOpen(false); } else { + const isSingletonProvider = item.providerId === EmailProviderIdEnum.NovuAgent; const sameProviderCount = (integrations ?? []).filter((i) => i.providerId === item.providerId).length; - const uniqueName = `${item.displayName} ${sameProviderCount + 1}`; + const uniqueName = isSingletonProvider ? item.displayName : `${item.displayName} ${sameProviderCount + 1}`; const created = await createIntegrationMutation.mutateAsync({ providerId: item.providerId, @@ -359,13 +378,15 @@ export function ProviderDropdown({
- {isRowPending ? ( + {isRowPending && ( - ) : item.integration ? ( + )} + {!isRowPending && item.integration && item.providerId !== EmailProviderIdEnum.NovuAgent && ( {item.integration.identifier} - ) : ( + )} + {!isRowPending && !item.integration && ( )} @@ -429,7 +450,6 @@ export function ProviderDropdown({ -

{'💡 You can always add more providers.'}

); } diff --git a/apps/dashboard/src/components/integrations/components/integrations-list.tsx b/apps/dashboard/src/components/integrations/components/integrations-list.tsx index 820184173a6..638eacd8cac 100644 --- a/apps/dashboard/src/components/integrations/components/integrations-list.tsx +++ b/apps/dashboard/src/components/integrations/components/integrations-list.tsx @@ -1,4 +1,4 @@ -import { ChannelTypeEnum, providers as novuProviders } from '@novu/shared'; +import { ChannelTypeEnum, EmailProviderIdEnum, providers as novuProviders } from '@novu/shared'; import { useMemo } from 'react'; import { Skeleton } from '@/components/primitives/skeleton'; import { useEnvironment } from '@/context/environment/hooks'; @@ -84,20 +84,22 @@ export function IntegrationsList({ onItemClick, excludeIntegrationIds, variant = const availableIntegrations = novuProviders; const groupedIntegrations = useMemo(() => { - return integrations?.reduce( - (acc, integration) => { - const channel = integration.channel; + return integrations + ?.filter((i) => i.providerId !== EmailProviderIdEnum.NovuAgent) + .reduce( + (acc, integration) => { + const channel = integration.channel; - if (!acc[channel]) { - acc[channel] = []; - } + if (!acc[channel]) { + acc[channel] = []; + } - acc[channel].push(integration); + acc[channel].push(integration); - return acc; - }, - {} as Record - ); + return acc; + }, + {} as Record + ); }, [integrations]); if (isLoading || !currentEnvironment) { diff --git a/apps/dashboard/src/utils/provider-square-icon.ts b/apps/dashboard/src/utils/provider-square-icon.ts index a59190d70a0..de06c69cb8d 100644 --- a/apps/dashboard/src/utils/provider-square-icon.ts +++ b/apps/dashboard/src/utils/provider-square-icon.ts @@ -1,5 +1,6 @@ const PROVIDER_SQUARE_ICON_FILE_ALIASES: Record = { whatsapp: 'whatsapp-business', + 'novu-email-agent': 'novu-email', }; export function getProviderSquareIconFileName(platform: string): string { diff --git a/apps/worker/src/app/shared/shared.module.ts b/apps/worker/src/app/shared/shared.module.ts index b194ccb4763..7b6d74b6a63 100644 --- a/apps/worker/src/app/shared/shared.module.ts +++ b/apps/worker/src/app/shared/shared.module.ts @@ -38,6 +38,7 @@ import { WorkflowRunService, } from '@novu/application-generic'; import { + AgentIntegrationRepository, ControlValuesRepository, DalService, EnvironmentRepository, @@ -64,6 +65,7 @@ import { UNIQUE_WORKER_DEPENDENCIES } from '../../config/worker-init.config'; import { ActiveJobsMetricService } from '../workflow/services'; const DAL_MODELS = [ + AgentIntegrationRepository, EnvironmentRepository, EnvironmentVariableRepository, ExecutionDetailsRepository, diff --git a/apps/worker/src/app/workflow/specs/inbound-email-parse.spec.ts b/apps/worker/src/app/workflow/specs/inbound-email-parse.spec.ts index 594cb73b810..14ba45ac39f 100644 --- a/apps/worker/src/app/workflow/specs/inbound-email-parse.spec.ts +++ b/apps/worker/src/app/workflow/specs/inbound-email-parse.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { CompileTemplate, SendWebhookMessage } from '@novu/application-generic'; -import { DomainRepository, JobRepository, MessageRepository } from '@novu/dal'; +import { CompileTemplate, HttpClientService, SendWebhookMessage } from '@novu/application-generic'; +import { AgentIntegrationRepository, DomainRepository, IntegrationRepository, JobRepository, MessageRepository } from '@novu/dal'; import axios, { AxiosResponse } from 'axios'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -37,6 +37,9 @@ describe('Should handle the new arrived mail', () => { { provide: DomainRepository, useValue: sandbox.createStubInstance(DomainRepository) }, { provide: SendWebhookMessage, useValue: sandbox.createStubInstance(SendWebhookMessage) }, { provide: CompileTemplate, useValue: compileTemplate }, + { provide: HttpClientService, useValue: sandbox.createStubInstance(HttpClientService) }, + { provide: IntegrationRepository, useValue: sandbox.createStubInstance(IntegrationRepository) }, + { provide: AgentIntegrationRepository, useValue: sandbox.createStubInstance(AgentIntegrationRepository) }, ], }).compile(); diff --git a/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.spec.ts b/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.spec.ts index e795fd61f94..48283f521db 100644 --- a/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.spec.ts +++ b/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.spec.ts @@ -1,5 +1,5 @@ -import { SendWebhookMessage } from '@novu/application-generic'; -import { DomainRepository } from '@novu/dal'; +import { encryptSecret, HttpClientService, SendWebhookMessage } from '@novu/application-generic'; +import { AgentIntegrationRepository, DomainRepository, IntegrationRepository } from '@novu/dal'; import { DomainRouteTypeEnum, DomainStatusEnum } from '@novu/shared'; import { expect } from 'chai'; import sinon from 'sinon'; @@ -47,21 +47,50 @@ function makeCommand(localPart: string): InboundEmailParseCommand { } as unknown as InboundEmailParseCommand; } +const TEST_ENCRYPTION_KEY = '12345678901234567890123456789012'; // 32 chars for AES-256 + describe('DomainRouteStrategy', () => { let domainRepository: sinon.SinonStubbedInstance; let sendWebhookMessage: sinon.SinonStubbedInstance; + let httpClientService: sinon.SinonStubbedInstance; + let integrationRepository: sinon.SinonStubbedInstance; + let agentIntegrationRepository: sinon.SinonStubbedInstance; let strategy: DomainRouteStrategy; let sandbox: sinon.SinonSandbox; + let originalEncryptionKey: string | undefined; beforeEach(() => { + originalEncryptionKey = process.env.STORE_ENCRYPTION_KEY; + process.env.STORE_ENCRYPTION_KEY = TEST_ENCRYPTION_KEY; + sandbox = sinon.createSandbox(); domainRepository = sandbox.createStubInstance(DomainRepository); sendWebhookMessage = sandbox.createStubInstance(SendWebhookMessage); - strategy = new DomainRouteStrategy(domainRepository as any, sendWebhookMessage as any); + httpClientService = sandbox.createStubInstance(HttpClientService); + integrationRepository = sandbox.createStubInstance(IntegrationRepository); + agentIntegrationRepository = sandbox.createStubInstance(AgentIntegrationRepository); + + agentIntegrationRepository.findLinksForAgents.resolves([ + { _integrationId: 'integration-001', _agentId: 'agent-001' } as any, + ]); + integrationRepository.findOne.resolves({ + identifier: 'novu-email-agent-test', + credentials: { secretKey: encryptSecret('test-secret-key') }, + } as any); + httpClientService.request.resolves({ body: {}, statusCode: 200, headers: {} }); + + strategy = new DomainRouteStrategy( + domainRepository as any, + sendWebhookMessage as any, + httpClientService as any, + integrationRepository as any, + agentIntegrationRepository as any + ); }); afterEach(() => { sandbox.restore(); + process.env.STORE_ENCRYPTION_KEY = originalEncryptionKey; }); it('should NOT fire webhook when no WEBHOOK route exists', async () => { diff --git a/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.ts b/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.ts index 8c8ca1e6b37..925931bacec 100644 --- a/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.ts +++ b/apps/worker/src/app/workflow/usecases/inbound-email-parse/strategies/domain-route.strategy.ts @@ -1,9 +1,22 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import { SendWebhookMessage } from '@novu/application-generic'; -import { DomainEntity, DomainRepository, DomainRoute } from '@novu/dal'; -import { DomainRouteTypeEnum, DomainStatusEnum, WebhookEventEnum, WebhookObjectTypeEnum } from '@novu/shared'; +import { + buildNovuSignatureHeader, + decryptSecret, + HttpClientService, + SendWebhookMessage, +} from '@novu/application-generic'; +import { AgentIntegrationRepository, DomainEntity, DomainRepository, DomainRoute, IntegrationRepository } from '@novu/dal'; +import { + ChannelTypeEnum, + DomainRouteTypeEnum, + DomainStatusEnum, + EmailProviderIdEnum, + EmailWebhookPayload, + WebhookEventEnum, + WebhookObjectTypeEnum, +} from '@novu/shared'; import { InboundEmailParseCommand } from '../inbound-email-parse.command'; -import { normalizeReferences, resolveThreadId } from './resolve-thread-id'; +import { normalizeReferences } from './resolve-thread-id'; type RoutableDomain = Pick< DomainEntity, @@ -40,7 +53,10 @@ export type DomainRouteEmailPayload = { export class DomainRouteStrategy { constructor( private domainRepository: DomainRepository, - private sendWebhookMessage: SendWebhookMessage + private sendWebhookMessage: SendWebhookMessage, + private httpClientService: HttpClientService, + private integrationRepository: IntegrationRepository, + private agentIntegrationRepository: AgentIntegrationRepository ) {} async execute(command: InboundEmailParseCommand): Promise { @@ -123,28 +139,107 @@ export class DomainRouteStrategy { private async handleAgentRoute( command: InboundEmailParseCommand, - _domain: RoutableDomain, + domain: RoutableDomain, route: DomainRoute, toAddress: string ): Promise { - const threadInfo = { - threadId: resolveThreadId(toAddress, command.messageId, command.inReplyTo, command.references), - messageId: command.messageId, - inReplyTo: command.inReplyTo ?? null, - references: normalizeReferences(command.references), - subject: command.subject, - isReply: !!command.inReplyTo, - }; + const agentId = route.destination; + if (!agentId) { + this.throwError(`Agent route for ${toAddress} has no destination`); + } + + const { identifier: integrationIdentifier, secretKey } = await this.resolveIntegration( + agentId, + domain._environmentId, + domain._organizationId + ); + + const payload = this.buildWebhookPayload(command); + const signature = buildNovuSignatureHeader(secretKey, payload); + const apiBaseUrl = process.env.API_ROOT_URL; + if (!apiBaseUrl) { + this.throwError('API_ROOT_URL environment variable is not set — cannot forward inbound email to agent webhook'); + } + const url = `${apiBaseUrl}/v1/agents/${encodeURIComponent(agentId)}/webhook/${encodeURIComponent(integrationIdentifier)}`; + + await this.httpClientService.request({ + url, + method: 'POST', + body: payload, + headers: { 'novu-signature': signature, 'content-type': 'application/json' }, + timeout: 30_000, + }); - // TODO: Implement agent request in next step - // await this.sendToAgent(route.destination, _agentPayload, threadInfo); Logger.log( - { toAddress, destination: route.destination, threadInfo }, - 'Agent route — thread info collected, forwarding not yet implemented', + { toAddress, agentId, integrationIdentifier }, + 'Forwarded inbound email to agent webhook', LOG_CONTEXT ); } + private async resolveIntegration( + agentId: string, + environmentId: string, + organizationId: string + ): Promise<{ identifier: string; secretKey: string }> { + const links = await this.agentIntegrationRepository.findLinksForAgents({ + organizationId, + environmentId, + agentIds: [agentId], + }); + + const integrationIds = links.map((l) => l._integrationId).filter(Boolean); + if (integrationIds.length === 0) { + this.throwError(`No integration linked to agent ${agentId}`); + } + + const integration = await this.integrationRepository.findOne( + { + _id: { $in: integrationIds } as unknown as string, + _environmentId: environmentId, + _organizationId: organizationId, + providerId: EmailProviderIdEnum.NovuAgent, + channel: ChannelTypeEnum.EMAIL, + }, + 'identifier credentials' + ); + if (!integration) { + this.throwError(`No active NovuAgent email integration found for agent ${agentId}`); + } + + const encryptedSecret = integration.credentials?.secretKey; + if (!encryptedSecret) { + this.throwError(`Integration ${integration.identifier} is missing its webhook secret — re-link the email integration to regenerate it`); + } + + return { identifier: integration.identifier, secretKey: decryptSecret(encryptedSecret) }; + } + + private buildWebhookPayload(command: InboundEmailParseCommand): EmailWebhookPayload { + const from = command.from[0]; + const refs = normalizeReferences(command.references); + + return { + messageId: command.messageId, + inReplyTo: command.inReplyTo ?? undefined, + references: refs.length > 0 ? refs.join(' ') : undefined, + from: { address: from.address, name: from.name }, + to: command.to.map((t: { address: string; name?: string }) => ({ + address: t.address, + name: t.name, + })), + subject: command.subject, + text: command.text || undefined, + html: command.html || undefined, + attachments: command.attachments?.map((a: { filename: string; contentType: string; url?: string }) => ({ + filename: a.filename, + contentType: a.contentType, + url: a.url, + })), + date: (() => { const d = new Date(command.date as unknown as string); return Number.isNaN(d.getTime()) ? new Date().toISOString() : d.toISOString(); })(), + }; + } + private throwError(error: string): never { Logger.error(error, LOG_CONTEXT); throw new BadRequestException(error); diff --git a/libs/application-generic/src/dtos/credentials.dto.ts b/libs/application-generic/src/dtos/credentials.dto.ts index 2483af40cab..e6368f9a4f4 100644 --- a/libs/application-generic/src/dtos/credentials.dto.ts +++ b/libs/application-generic/src/dtos/credentials.dto.ts @@ -251,4 +251,29 @@ export class CredentialsDto implements ICredentials { @IsOptional() @IsString() AppIOBaseUrl?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + signingSecret?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + replyDomain?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + outboundIntegrationId?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + inboundAddress?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + inboundDomain?: string; } diff --git a/libs/dal/src/repositories/integration/integration.schema.ts b/libs/dal/src/repositories/integration/integration.schema.ts index 7113c64a25b..a02b8ba980f 100644 --- a/libs/dal/src/repositories/integration/integration.schema.ts +++ b/libs/dal/src/repositories/integration/integration.schema.ts @@ -67,6 +67,10 @@ const integrationSchema = new Schema( servicePlanId: Schema.Types.String, tenantId: Schema.Types.String, signingSecret: Schema.Types.String, + replyDomain: Schema.Types.String, + outboundIntegrationId: Schema.Types.String, + inboundAddress: Schema.Types.String, + inboundDomain: Schema.Types.String, AppIOBaseUrl: Schema.Types.String, AppIOSubscriptionId: Schema.Types.String, AppIOBearerToken: Schema.Types.String, diff --git a/packages/chat-adapter-email/package.json b/packages/chat-adapter-email/package.json new file mode 100644 index 00000000000..098511bc18e --- /dev/null +++ b/packages/chat-adapter-email/package.json @@ -0,0 +1,40 @@ +{ + "name": "@novu/chat-adapter-email", + "version": "0.0.1", + "private": true, + "type": "module", + "description": "Email adapter for Novu conversational agents (Chat SDK)", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/" + ], + "scripts": { + "prebuild": "rimraf dist", + "build": "tsc -p tsconfig.json", + "watch:build": "tsc -p tsconfig.json -w", + "check": "biome check .", + "check:fix": "biome check --write ." + }, + "dependencies": { + "@novu/shared": "workspace:*", + "@react-email/components": "1.0.12", + "@react-email/render": "2.0.7", + "hast-util-to-html": "9.0.5", + "mdast-util-to-hast": "13.2.1" + }, + "peerDependencies": { + "chat": ">=4.25.0", + "react": ">=18.0.0 || >=19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "rimraf": "~3.0.2", + "typescript": "5.6.2" + }, + "nx": { + "tags": [ + "type:package" + ] + } +} diff --git a/packages/chat-adapter-email/src/adapter.ts b/packages/chat-adapter-email/src/adapter.ts new file mode 100644 index 00000000000..3100aa53295 --- /dev/null +++ b/packages/chat-adapter-email/src/adapter.ts @@ -0,0 +1,256 @@ +import type { + Adapter, + AdapterPostableMessage, + ChatInstance, + FetchResult, + FormattedContent, + Message, + RawMessage, + Root, + ThreadInfo, + WebhookOptions, +} from 'chat'; +import type { CardNode } from './card-renderer.js'; +import { EmailFormatConverter } from './format-converter.js'; +import { MessageParser } from './message-parser.js'; +import { renderMessage } from './message-renderer.js'; +import { ThreadResolver } from './thread-resolver.js'; +import type { NovuEmailAdapterConfig, NovuEmailRawMessage, NovuEmailThreadId } from './types.js'; +import { generateMessageId, hashMessageId, parseEmailAddress } from './utils.js'; +import { WebhookHandler } from './webhook-handler.js'; + +class NotImplementedError extends Error { + constructor(method: string) { + super(`${method} is not supported by the email adapter`); + this.name = 'NotImplementedError'; + } +} + +export class NovuEmailAdapterImpl implements Adapter { + readonly name = 'email'; + readonly userName: string; + readonly persistMessageHistory = true; + + private readonly config: NovuEmailAdapterConfig; + private chat: ChatInstance | null = null; + private readonly threadResolver = new ThreadResolver(); + private readonly messageParser = new MessageParser(); + private readonly formatConverter = new EmailFormatConverter(); + private readonly webhookHandler: WebhookHandler; + + constructor(config: NovuEmailAdapterConfig) { + this.config = config; + this.userName = config.fromAddress; + this.webhookHandler = new WebhookHandler(config.signingSecret); + } + + async initialize(chat: ChatInstance): Promise { + this.chat = chat; + this.threadResolver.setStateAdapter(chat.getState()); + + const chatModule = await import('chat'); + this.messageParser.setChatModule(chatModule.Message as any, chatModule.parseMarkdown); + } + + // -- Thread ID methods -- + + encodeThreadId(data: NovuEmailThreadId): string { + return this.threadResolver.encodeThreadId(data); + } + + decodeThreadId(threadId: string): NovuEmailThreadId { + return this.threadResolver.decodeThreadId(threadId); + } + + channelIdFromThreadId(threadId: string): string { + const { recipientAddress } = this.threadResolver.decodeThreadId(threadId); + + return `email:${recipientAddress}`; + } + + // -- Inbound -- + + async handleWebhook(request: Request, options?: WebhookOptions): Promise { + if (!this.chat) { + throw new Error('Adapter not initialized. Call initialize() first.'); + } + + const result = await this.webhookHandler.parseAndVerify(request); + if (!result.payload) { + return new Response(null, { status: result.status }); + } + + const { payload } = result; + const senderAddress = parseEmailAddress(payload.from.address); + + const threadId = await this.threadResolver.resolveThreadId({ + recipientAddress: senderAddress, + messageId: payload.messageId, + inReplyTo: payload.inReplyTo, + references: payload.references, + }); + + await this.threadResolver.trackSubject(threadId, payload.subject); + + const message = this.parseMessage(this.toRawMessage(payload, threadId)); + this.chat.processMessage(this, threadId, message, options); + + return new Response(null, { status: 200 }); + } + + private toRawMessage(payload: import('./types.js').EmailWebhookPayload, _threadId: string): NovuEmailRawMessage { + return { + id: payload.messageId, + messageId: payload.messageId, + from: payload.from.name ? `${payload.from.name} <${payload.from.address}>` : payload.from.address, + to: payload.to.map((t: { address: string; name?: string }) => t.address), + subject: payload.subject, + text: payload.text, + html: payload.html, + createdAt: payload.date, + attachments: payload.attachments, + }; + } + + // -- Message parsing -- + + parseMessage(raw: NovuEmailRawMessage): Message { + return this.messageParser.parse(raw, this.config.fromAddress); + } + + // -- Outbound -- + + async postMessage(threadId: string, message: AdapterPostableMessage): Promise> { + const normalized = this.normalizeMessage(message); + const decoded = this.threadResolver.decodeThreadId(threadId); + const rendered = await renderMessage(normalized); + + const fromHeader = this.config.fromName + ? `${this.config.fromName} <${this.config.fromAddress}>` + : this.config.fromAddress; + + const messageId = generateMessageId(this.config.fromAddress); + const replyHeaders = await this.threadResolver.getReplyHeaders(threadId); + const storedSubject = await this.threadResolver.getSubject(threadId); + const subject = storedSubject + ? /^re:/i.test(storedSubject) + ? storedSubject + : `Re: ${storedSubject}` + : 'New message'; + + const result = await this.config.sendEmail({ + to: decoded.recipientAddress, + subject, + html: rendered.html, + text: rendered.text, + messageId, + inReplyTo: replyHeaders?.['In-Reply-To'], + references: replyHeaders?.References, + }); + + const sentMessageId = result.messageId || messageId; + await this.threadResolver.trackMessage(threadId, sentMessageId); + + const raw: NovuEmailRawMessage = { + id: sentMessageId, + messageId: sentMessageId, + from: fromHeader, + to: [decoded.recipientAddress], + subject, + text: rendered.text, + html: rendered.html, + headers: replyHeaders, + createdAt: new Date().toISOString(), + }; + + return { id: sentMessageId, raw, threadId }; + } + + /** + * Normalize AdapterPostableMessage variants into a uniform shape. + */ + private normalizeMessage(message: AdapterPostableMessage): { text?: string; formatted?: Root; card?: CardNode } { + if (typeof message === 'string') { + return { text: message }; + } + if ('markdown' in message) { + return { text: (message as { markdown: string }).markdown }; + } + if ('raw' in message) { + return { text: (message as { raw: string }).raw }; + } + if ('ast' in message) { + return { formatted: (message as { ast: Root }).ast }; + } + if ('card' in message) { + return { card: (message as { card: CardNode }).card }; + } + if ('type' in message) { + return { card: message as CardNode }; + } + + return message as { text?: string; formatted?: Root; card?: CardNode }; + } + + // -- Rendering -- + + renderFormatted(content: FormattedContent): string { + return this.formatConverter.fromAst(content); + } + + // -- Thread metadata -- + + async fetchThread(threadId: string): Promise { + const decoded = this.threadResolver.decodeThreadId(threadId); + const subject = await this.threadResolver.getSubject(threadId); + + return { + id: threadId, + channelId: this.channelIdFromThreadId(threadId), + metadata: { + title: subject || `Conversation with ${decoded.recipientAddress}`, + recipientAddress: decoded.recipientAddress, + }, + }; + } + + async fetchMessages(_threadId: string): Promise> { + return { messages: [] }; + } + + async openDM(email: string): Promise { + const messageId = generateMessageId(email); + const hash = hashMessageId(messageId); + + return this.threadResolver.encodeThreadId({ + recipientAddress: email, + rootMessageIdHash: hash, + }); + } + + // -- Unsupported operations -- + + async startTyping(_threadId: string): Promise { + // No-op: email has no typing indicators + } + + async editMessage( + _threadId: string, + _messageId: string, + _message: AdapterPostableMessage + ): Promise> { + throw new NotImplementedError('editMessage'); + } + + async deleteMessage(_threadId: string, _messageId: string): Promise { + throw new NotImplementedError('deleteMessage'); + } + + async addReaction(_threadId: string, _messageId: string, _emoji: string): Promise { + throw new NotImplementedError('addReaction'); + } + + async removeReaction(_threadId: string, _messageId: string, _emoji: string): Promise { + throw new NotImplementedError('removeReaction'); + } +} diff --git a/packages/chat-adapter-email/src/card-renderer.tsx b/packages/chat-adapter-email/src/card-renderer.tsx new file mode 100644 index 00000000000..fea66f358b9 --- /dev/null +++ b/packages/chat-adapter-email/src/card-renderer.tsx @@ -0,0 +1,156 @@ +import { + Body, + Button, + Container, + Heading, + Hr, + Html, + Img, + Link, + Section, + Text, +} from '@react-email/components'; +import { render } from '@react-email/render'; +import React from 'react'; + +/** + * Matches the Chat SDK CardElement / CardChild shapes. + * See: chat/dist/jsx-runtime-*.d.ts + */ +export interface CardNode { + type: string; + title?: string; + subtitle?: string; + content?: string; + label?: string; + value?: string; + url?: string; + imageUrl?: string; + style?: string; + children?: CardNode[]; + props?: Record; +} + +const SAFE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']); + +function safeUrl(value: string | undefined): string | undefined { + if (!value) return undefined; + try { + const url = new URL(value); + + return SAFE_URL_PROTOCOLS.has(url.protocol) ? value : undefined; + } catch { + return undefined; + } +} + +function renderChildren(children: CardNode[] | undefined): React.ReactNode { + if (!children || children.length === 0) return null; + + return children.map((child, i) => {renderNode(child)}); +} + +function renderNode(node: CardNode): React.ReactNode { + switch (node.type) { + case 'card': + return ( + + {node.title && ( + + {node.title} + + )} + {node.subtitle && ( + {node.subtitle} + )} + {safeUrl(node.imageUrl) && } + {renderChildren(node.children)} + + ); + + case 'text': + return {node.content || ''}; + + case 'divider': + return
; + + case 'image': + return ( + {node.label + ); + + case 'actions': + return
{renderChildren(node.children)}
; + + case 'link-button': + return ( + + ); + + case 'button': + if (node.url && safeUrl(node.url)) { + return ( + + ); + } + + return {node.label || ''}; + + case 'link': + return {node.label || ''}; + + case 'section': + return
{renderChildren(node.children)}
; + + case 'field': + return ( + + {node.label || ''}: {node.value || ''} + + ); + + case 'fields': + return
{renderChildren(node.children)}
; + + default: + return {node.content || node.label || ''}; + } +} + +export async function renderCard(card: CardNode): Promise { + const emailComponent = ( + + + {renderNode(card)} + + + ); + + return await render(emailComponent); +} + +export function extractTextFromCard(card: CardNode): string { + const parts: string[] = []; + if (card.title) parts.push(card.title); + if (card.subtitle) parts.push(card.subtitle); + if (card.content) parts.push(card.content); + if (card.label) parts.push(card.label); + if (card.value) parts.push(card.value); + + if (Array.isArray(card.children)) { + for (const child of card.children) { + const childText = extractTextFromCard(child); + if (childText) parts.push(childText); + } + } + + return parts.join('\n'); +} diff --git a/packages/chat-adapter-email/src/format-converter.ts b/packages/chat-adapter-email/src/format-converter.ts new file mode 100644 index 00000000000..8288edb045e --- /dev/null +++ b/packages/chat-adapter-email/src/format-converter.ts @@ -0,0 +1,32 @@ +import type { Content, Root } from 'chat'; +import { toHtml } from 'hast-util-to-html'; +import { toHast } from 'mdast-util-to-hast'; + +/** + * Converts mdast AST to HTML for email bodies + * using the standard mdast → hast → html pipeline. + */ +export class EmailFormatConverter { + fromAst(ast: Root): string { + const hast = toHast(ast); + if (!hast) return ''; + + return toHtml(hast); + } + + toAst(text: string): Root { + if (!text || text.trim() === '') { + return { type: 'root', children: [] }; + } + + const paragraphs = text.split(/\n\n+/); + const children: Content[] = paragraphs + .filter((p) => p.trim() !== '') + .map((p) => ({ + type: 'paragraph' as const, + children: [{ type: 'text' as const, value: p.trim() }], + })); + + return { type: 'root', children }; + } +} diff --git a/packages/chat-adapter-email/src/index.ts b/packages/chat-adapter-email/src/index.ts new file mode 100644 index 00000000000..6473fb94a72 --- /dev/null +++ b/packages/chat-adapter-email/src/index.ts @@ -0,0 +1,12 @@ +import type { Adapter } from 'chat'; +import { NovuEmailAdapterImpl } from './adapter.js'; +import type { NovuEmailAdapterConfig, NovuEmailRawMessage, NovuEmailThreadId } from './types.js'; + +export type { NovuEmailAdapterConfig, NovuEmailRawMessage, NovuEmailThreadId, SendEmailParams } from './types.js'; +export type { EmailWebhookPayload, NovuEmailAttachment } from './types.js'; + +export function createNovuEmailAdapter( + config: NovuEmailAdapterConfig +): Adapter { + return new NovuEmailAdapterImpl(config); +} diff --git a/packages/chat-adapter-email/src/message-parser.ts b/packages/chat-adapter-email/src/message-parser.ts new file mode 100644 index 00000000000..f593f019770 --- /dev/null +++ b/packages/chat-adapter-email/src/message-parser.ts @@ -0,0 +1,56 @@ +import type { Message } from 'chat'; +import type { NovuEmailRawMessage } from './types.js'; +import { extractDisplayName, parseEmailAddress, stripHtml } from './utils.js'; + +type MessageConstructor = new (data: unknown) => Message; +type ParseMarkdownFn = (text: string) => import('chat').Root; + +/** + * Converts raw email data into a Chat SDK Message. + * Requires chat SDK classes injected via setChatModule() since `chat` is ESM-only. + */ +export class MessageParser { + private MessageClass: MessageConstructor | null = null; + private parseMarkdownFn: ParseMarkdownFn | null = null; + + setChatModule(MessageClass: MessageConstructor, parseMarkdownFn: ParseMarkdownFn): void { + this.MessageClass = MessageClass; + this.parseMarkdownFn = parseMarkdownFn; + } + + parse(raw: NovuEmailRawMessage, fromAddress: string): Message { + if (!this.MessageClass || !this.parseMarkdownFn) { + throw new Error('MessageParser not initialized — call setChatModule() first'); + } + + const authorEmail = parseEmailAddress(raw.from); + const authorName = extractDisplayName(raw.from); + const text = raw.text || stripHtml(raw.html || ''); + + return new this.MessageClass({ + id: raw.id, + threadId: '', + text, + formatted: this.parseMarkdownFn(text), + raw, + author: { + userId: authorEmail, + userName: authorEmail, + fullName: authorName, + isBot: false, + isMe: authorEmail === fromAddress, + }, + metadata: { + dateSent: (() => { const d = new Date(raw.createdAt); return Number.isNaN(d.getTime()) ? new Date() : d; })(), + edited: false, + }, + attachments: (raw.attachments || []).map((a: { filename: string; contentType: string; url?: string }) => ({ + type: 'file' as const, + name: a.filename, + mimeType: a.contentType, + url: a.url, + })), + isMention: true, + }); + } +} diff --git a/packages/chat-adapter-email/src/message-renderer.ts b/packages/chat-adapter-email/src/message-renderer.ts new file mode 100644 index 00000000000..0ddedf571b0 --- /dev/null +++ b/packages/chat-adapter-email/src/message-renderer.ts @@ -0,0 +1,58 @@ +import type { Root } from 'chat'; +import type { CardNode } from './card-renderer.js'; +import { extractTextFromCard, renderCard } from './card-renderer.js'; +import { EmailFormatConverter } from './format-converter.js'; + +interface RenderInput { + text?: string; + formatted?: Root; + card?: CardNode; +} + +interface RenderOutput { + html: string; + text: string; +} + +const converter = new EmailFormatConverter(); + +export async function renderMessage(input: RenderInput): Promise { + if (input.card) { + const html = await renderCard(input.card); + const text = extractTextFromCard(input.card) || input.text || ''; + + return { html, text }; + } + + if (input.formatted) { + const html = converter.fromAst(input.formatted); + const text = input.text || stripForText(html); + + return { html, text }; + } + + const text = input.text || ''; + const html = `
${escapeHtml(text)}
`; + + return { html, text }; +} + +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function stripForText(html: string): string { + const chars: string[] = []; + let depth = 0; + for (const ch of html) { + if (ch === '<') { depth++; continue; } + if (ch === '>') { if (depth > 0) depth--; continue; } + if (depth === 0) chars.push(ch); + } + + return chars.join('').replace(/\s+/g, ' ').trim(); +} diff --git a/packages/chat-adapter-email/src/thread-resolver.ts b/packages/chat-adapter-email/src/thread-resolver.ts new file mode 100644 index 00000000000..8cd461e2793 --- /dev/null +++ b/packages/chat-adapter-email/src/thread-resolver.ts @@ -0,0 +1,155 @@ +import type { StateAdapter } from 'chat'; +import type { NovuEmailThreadId } from './types.js'; +import { hashMessageId } from './utils.js'; + +const WHITESPACE_RE = /\s+/; +const STATE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days + +function msgKey(messageId: string): string { + return `email:msg:${messageId}`; +} + +function threadMessagesKey(threadId: string): string { + return `email:thread:${threadId}:messages`; +} + +function threadSubjectKey(threadId: string): string { + return `email:thread:${threadId}:subject`; +} + +interface ResolveInput { + recipientAddress: string; + messageId: string; + inReplyTo?: string; + references?: string; +} + +export class ThreadResolver { + private state: StateAdapter | null = null; + + setStateAdapter(state: StateAdapter): void { + this.state = state; + } + + private getState(): StateAdapter { + if (!this.state) { + throw new Error('ThreadResolver not initialized — call setStateAdapter() first'); + } + + return this.state; + } + + encodeThreadId(id: NovuEmailThreadId): string { + return `email:${encodeURIComponent(id.recipientAddress)}:${id.rootMessageIdHash}`; + } + + decodeThreadId(threadId: string): NovuEmailThreadId { + const parts = threadId.split(':'); + if (parts.length !== 3 || parts[0] !== 'email' || !parts[1] || !parts[2]) { + throw new Error(`Invalid email thread ID format: ${threadId}`); + } + + return { + recipientAddress: decodeURIComponent(parts[1]), + rootMessageIdHash: parts[2], + }; + } + + async resolveThreadId(input: ResolveInput): Promise { + const state = this.getState(); + const { recipientAddress, messageId, inReplyTo, references } = input; + + if (inReplyTo || references) { + const candidateIds = this.extractMessageIds(inReplyTo, references); + for (const candidate of candidateIds) { + const existingThread = await state.get(msgKey(candidate)); + if (existingThread) { + await this.trackMessage(existingThread, messageId); + + return existingThread; + } + } + } + + const hash = hashMessageId(messageId); + const threadId = this.encodeThreadId({ recipientAddress, rootMessageIdHash: hash }); + await this.trackMessage(threadId, messageId); + + return threadId; + } + + async trackMessage(threadId: string, messageId: string): Promise { + const state = this.getState(); + await Promise.all([ + state.set(msgKey(messageId), threadId, STATE_TTL_MS), + state.appendToList(threadMessagesKey(threadId), messageId, { + maxLength: 100, + ttlMs: STATE_TTL_MS, + }), + ]); + } + + async trackSubject(threadId: string, subject: string): Promise { + const state = this.getState(); + await state.setIfNotExists(threadSubjectKey(threadId), subject, STATE_TTL_MS); + } + + async getReplyHeaders(threadId: string): Promise | undefined> { + const state = this.getState(); + const messages = await state.getList(threadMessagesKey(threadId)); + if (!messages || messages.length === 0) { + return undefined; + } + + const lastMessageId = messages[messages.length - 1]!; + + return { + 'In-Reply-To': lastMessageId, + References: messages.join(' '), + }; + } + + async getSubject(threadId: string): Promise { + const state = this.getState(); + + return (await state.get(threadSubjectKey(threadId))) ?? undefined; + } + + /** + * Extract candidate message IDs from In-Reply-To and References headers. + * Handles both RFC 2822 whitespace-separated format and JSON-encoded arrays. + */ + private extractMessageIds(inReplyTo?: string, references?: string): string[] { + const ids: string[] = []; + + if (references) { + const trimmed = references.trim(); + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + ids.push( + ...parsed + .filter((s): s is string => typeof s === 'string') + .map((s) => s.trim()) + .filter(Boolean) + ); + } + } catch { + ids.push(...trimmed.split(WHITESPACE_RE).filter(Boolean)); + } + } else { + ids.push(...trimmed.split(WHITESPACE_RE).filter(Boolean)); + } + } + + if (inReplyTo) { + const trimmed = inReplyTo.trim(); + if (trimmed && !ids.includes(trimmed)) { + ids.push(trimmed); + } + } + + return ids; + } +} diff --git a/packages/chat-adapter-email/src/types.ts b/packages/chat-adapter-email/src/types.ts new file mode 100644 index 00000000000..c8e701feded --- /dev/null +++ b/packages/chat-adapter-email/src/types.ts @@ -0,0 +1,39 @@ +import type { Adapter } from 'chat'; +export type { EmailWebhookPayload, NovuEmailAttachment } from '@novu/shared'; + +export interface NovuEmailAdapterConfig { + fromAddress: string; + fromName?: string; + signingSecret: string; + sendEmail: (params: SendEmailParams) => Promise<{ messageId: string }>; +} + +export interface SendEmailParams { + to: string; + subject: string; + html: string; + text?: string; + inReplyTo?: string; + references?: string; + messageId?: string; +} + +export interface NovuEmailThreadId { + recipientAddress: string; + rootMessageIdHash: string; +} + +export interface NovuEmailRawMessage { + id: string; + messageId: string; + from: string; + to: string[]; + subject: string; + text?: string; + html?: string; + headers?: Record; + createdAt: string; + attachments?: import('@novu/shared').NovuEmailAttachment[]; +} + +export type NovuEmailAdapter = Adapter; diff --git a/packages/chat-adapter-email/src/utils.ts b/packages/chat-adapter-email/src/utils.ts new file mode 100644 index 00000000000..d0a1bcb0beb --- /dev/null +++ b/packages/chat-adapter-email/src/utils.ts @@ -0,0 +1,55 @@ +import { createHash, randomUUID } from 'node:crypto'; + +// Use [^<>] to prevent catastrophic backtracking on adversarial inputs with many '<' chars. +const EMAIL_ANGLE_BRACKET_RE = /<([^<>]+)>/; +const DISPLAY_NAME_RE = /^([^<]+)<[^<>]+>$/; +const SAFE_DOMAIN_RE = /^[a-z0-9.-]+$/i; + +export function hashMessageId(messageId: string): string { + return createHash('sha256').update(messageId).digest('hex').slice(0, 16); +} + +export function parseEmailAddress(input: string): string { + const trimmed = input.trim(); + const match = trimmed.match(EMAIL_ANGLE_BRACKET_RE); + + return (match?.[1] ?? trimmed).toLowerCase(); +} + +export function extractDisplayName(from: string): string { + const match = from.match(DISPLAY_NAME_RE); + + return match?.[1]?.trim() ?? from; +} + +export function generateMessageId(fromAddress: string): string { + const candidateDomain = fromAddress.split('@').at(-1)?.trim().toLowerCase(); + const domain = candidateDomain && SAFE_DOMAIN_RE.test(candidateDomain) ? candidateDomain : 'novu.co'; + + return `<${randomUUID()}@${domain}>`; +} + +const NAMED_ENTITIES: Record = { + amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', +}; + +function decodeEntities(text: string): string { + return text.replace(/&(?:#(\d+)|#x([0-9a-f]+)|([a-z]+));/gi, (match, dec, hex, name) => { + if (dec) return String.fromCodePoint(Number(dec)); + if (hex) return String.fromCodePoint(parseInt(hex, 16)); + + return NAMED_ENTITIES[name.toLowerCase()] ?? match; + }); +} + +export function stripHtml(html: string): string { + const chars: string[] = []; + let depth = 0; + for (const ch of html) { + if (ch === '<') { depth++; continue; } + if (ch === '>') { if (depth > 0) depth--; continue; } + if (depth === 0) chars.push(ch); + } + + return decodeEntities(chars.join('').replace(/\s+/g, ' ')).trim(); +} diff --git a/packages/chat-adapter-email/src/webhook-handler.ts b/packages/chat-adapter-email/src/webhook-handler.ts new file mode 100644 index 00000000000..49ea1be0b56 --- /dev/null +++ b/packages/chat-adapter-email/src/webhook-handler.ts @@ -0,0 +1,74 @@ +import { createHmac, timingSafeEqual } from 'node:crypto'; +import type { EmailWebhookPayload } from './types.js'; + +const SIGNATURE_HEADER = 'novu-signature'; +const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000; // 5 minutes +const MAX_FUTURE_SKEW_MS = 30 * 1000; // 30 seconds tolerance for clock drift + +interface VerifyResult { + payload: EmailWebhookPayload | null; + status: number; +} + +export class WebhookHandler { + constructor(private readonly signingSecret: string) {} + + async parseAndVerify(request: Request): Promise { + const signature = request.headers.get(SIGNATURE_HEADER); + if (!signature) { + return { status: 401, payload: null }; + } + + const body = await request.text(); + + if (!this.verifySignature(signature, body)) { + return { status: 401, payload: null }; + } + + try { + const payload = JSON.parse(body) as EmailWebhookPayload; + if (!payload.messageId || !payload.from?.address) { + return { status: 400, payload: null }; + } + + return { status: 200, payload }; + } catch { + return { status: 400, payload: null }; + } + } + + /** + * Verify HMAC signature matching the format produced by + * `buildNovuSignatureHeader` in libs/application-generic/src/utils/hmac.ts. + * + * Format: t={timestamp},v1={hmac-hex} + * HMAC input: "{timestamp}.{body}" + */ + private verifySignature(signature: string, body: string): boolean { + const parts = signature.split(','); + const timestampPart = parts.find((p) => p.startsWith('t=')); + const hmacPart = parts.find((p) => p.startsWith('v1=')); + + if (!timestampPart || !hmacPart) { + return false; + } + + const timestamp = timestampPart.slice(2); + const receivedHmac = hmacPart.slice(3); + + const age = Date.now() - Number(timestamp); + if (Number.isNaN(age) || age > MAX_TIMESTAMP_AGE_MS || age < -MAX_FUTURE_SKEW_MS) { + return false; + } + + const expectedHmac = createHmac('sha256', this.signingSecret) + .update(`${timestamp}.${body}`) + .digest('hex'); + + if (receivedHmac.length !== expectedHmac.length) { + return false; + } + + return timingSafeEqual(Buffer.from(receivedHmac, 'hex'), Buffer.from(expectedHmac, 'hex')); + } +} diff --git a/packages/chat-adapter-email/tsconfig.json b/packages/chat-adapter-email/tsconfig.json new file mode 100644 index 00000000000..4e5aaf96b0c --- /dev/null +++ b/packages/chat-adapter-email/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "module": "nodenext", + "moduleDetection": "force", + "moduleResolution": "nodenext", + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./src", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "strictPropertyInitialization": false, + "target": "ES2021", + "verbatimModuleSyntax": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/node_modules/**"] +} diff --git a/packages/shared/src/consts/providers/channels/email.ts b/packages/shared/src/consts/providers/channels/email.ts index f80479ab564..7cee508f444 100644 --- a/packages/shared/src/consts/providers/channels/email.ts +++ b/packages/shared/src/consts/providers/channels/email.ts @@ -186,4 +186,12 @@ export const emailProviders: IProviderConfig[] = [ docReference: `https://docs.novu.co/channels/email/email-webhook${UTM_CAMPAIGN_QUERY_PARAM}`, logoFileName: { light: 'email_webhook.svg', dark: 'email_webhook.svg' }, }, + { + id: EmailProviderIdEnum.NovuAgent, + displayName: 'Novu Email', + channel: ChannelTypeEnum.EMAIL, + credentials: [], + docReference: `https://docs.novu.co/platform/agents/email${UTM_CAMPAIGN_QUERY_PARAM}`, + logoFileName: { light: 'novu.png', dark: 'novu.png' }, + }, ]; diff --git a/packages/shared/src/consts/providers/conversational-providers.ts b/packages/shared/src/consts/providers/conversational-providers.ts index a9e307fda11..48cd0f4ddf4 100644 --- a/packages/shared/src/consts/providers/conversational-providers.ts +++ b/packages/shared/src/consts/providers/conversational-providers.ts @@ -1,4 +1,4 @@ -import { ChatProviderIdEnum } from '../../types'; +import { ChatProviderIdEnum, EmailProviderIdEnum } from '../../types'; export type ConversationalProvider = { providerId: string; @@ -10,6 +10,7 @@ export const CONVERSATIONAL_PROVIDERS: ConversationalProvider[] = [ { providerId: ChatProviderIdEnum.Slack, displayName: 'Slack' }, { providerId: ChatProviderIdEnum.MsTeams, displayName: 'MS Teams' }, { providerId: ChatProviderIdEnum.WhatsAppBusiness, displayName: 'WhatsApp Business' }, + { providerId: EmailProviderIdEnum.NovuAgent, displayName: 'Novu Email' }, { providerId: 'telegram', displayName: 'Telegram', comingSoon: true }, { providerId: 'google-chat', displayName: 'Google Chat', comingSoon: true }, { providerId: 'linear', displayName: 'Linear', comingSoon: true }, diff --git a/packages/shared/src/consts/providers/providers.ts b/packages/shared/src/consts/providers/providers.ts index bf544d9577f..645571d9c67 100644 --- a/packages/shared/src/consts/providers/providers.ts +++ b/packages/shared/src/consts/providers/providers.ts @@ -23,6 +23,7 @@ export const NOVU_PROVIDERS: ProvidersIdEnum[] = [ InAppProviderIdEnum.Novu, SmsProviderIdEnum.Novu, EmailProviderIdEnum.Novu, + EmailProviderIdEnum.NovuAgent, ChatProviderIdEnum.Novu, ]; diff --git a/packages/shared/src/entities/integration/credential.interface.ts b/packages/shared/src/entities/integration/credential.interface.ts index bb3f182f121..c73046cd18c 100644 --- a/packages/shared/src/entities/integration/credential.interface.ts +++ b/packages/shared/src/entities/integration/credential.interface.ts @@ -54,4 +54,8 @@ export interface ICredentials { servicePlanId?: string; tenantId?: string; signingSecret?: string; + replyDomain?: string; + outboundIntegrationId?: string; + inboundAddress?: string; + inboundDomain?: string; } diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts new file mode 100644 index 00000000000..22ea524bdf5 --- /dev/null +++ b/packages/shared/src/types/agent.ts @@ -0,0 +1,18 @@ +export interface NovuEmailAttachment { + filename: string; + contentType: string; + url?: string; +} + +export interface EmailWebhookPayload { + messageId: string; + inReplyTo?: string; + references?: string; + from: { address: string; name?: string }; + to: { address: string; name?: string }[]; + subject: string; + text?: string; + html?: string; + attachments?: NovuEmailAttachment[]; + date: string; +} diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index c2975b29cbf..a1e09f5c344 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './agent'; export * from './ai'; export * from './auth'; export * from './billing'; diff --git a/packages/shared/src/types/providers.ts b/packages/shared/src/types/providers.ts index 2d87089f6f7..d6389133bc1 100644 --- a/packages/shared/src/types/providers.ts +++ b/packages/shared/src/types/providers.ts @@ -53,6 +53,10 @@ export enum CredentialsKeyEnum { ServicePlanId = 'servicePlanId', TenantId = 'tenantId', SigningSecret = 'signingSecret', + ReplyDomain = 'replyDomain', + OutboundIntegrationId = 'outboundIntegrationId', + InboundAddress = 'inboundAddress', + InboundDomain = 'inboundDomain', } export type ConfigurationKey = keyof IConfigurations; @@ -79,6 +83,7 @@ export enum EmailProviderIdEnum { SparkPost = 'sparkpost', EmailWebhook = 'email-webhook', Braze = 'braze', + NovuAgent = 'novu-email-agent', } export enum SmsProviderIdEnum { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ba22d99caa..baec07363ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,7 +361,7 @@ importers: version: 7.4.0(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/throttler': specifier: 6.2.1 version: 6.2.1(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(reflect-metadata@0.2.2) @@ -371,6 +371,9 @@ importers: '@novu/application-generic': specifier: workspace:* version: link:../../libs/application-generic + '@novu/chat-adapter-email': + specifier: workspace:* + version: link:../../packages/chat-adapter-email '@novu/dal': specifier: workspace:* version: link:../../libs/dal @@ -708,7 +711,7 @@ importers: version: 3.0.51(react@19.2.3)(zod@4.3.5) '@better-auth/sso': specifier: ^1.3.0 - version: 1.4.7(better-auth@1.5.6(14eb71b76c563af6394284c30ec93f47)) + version: 1.4.7(better-auth@1.5.6(f5eede22a65d6cd0c93a8bf1a87b70c5)) '@calcom/embed-react': specifier: 1.5.2 version: 1.5.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -915,7 +918,7 @@ importers: version: 6.2.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) better-auth: specifier: ^1.4.9 - version: 1.5.6(14eb71b76c563af6394284c30ec93f47) + version: 1.5.6(f5eede22a65d6cd0c93a8bf1a87b70c5) class-variance-authority: specifier: ^0.7.0 version: 0.7.1 @@ -1322,7 +1325,7 @@ importers: version: 10.4.18(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@novu/application-generic': specifier: workspace:* version: link:../../libs/application-generic @@ -1485,7 +1488,7 @@ importers: version: 7.4.0(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@novu/application-generic': specifier: workspace:* version: link:../../libs/application-generic @@ -1721,7 +1724,7 @@ importers: version: 7.4.0(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/websockets': specifier: 10.4.18 version: 10.4.18(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(@nestjs/platform-socket.io@10.4.18)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -1890,7 +1893,7 @@ importers: version: 1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0)) '@langchain/langgraph-checkpoint-mongodb': specifier: ^1.1.6 - version: 1.1.6(@aws-sdk/credential-providers@3.637.0)(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + version: 1.1.6(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) '@langchain/openai': specifier: ^1.2.4 version: 1.2.4(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0))(ws@8.20.0) @@ -2060,7 +2063,7 @@ importers: dependencies: '@better-auth/sso': specifier: ^1.4.9 - version: 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(better-auth@1.5.6(b8a7a1c167ccbf2262fb279c12fb16c6))(better-call@1.3.2(zod@4.3.6)) + version: 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(better-auth@1.5.6(aac79383f71d2c0e811dbd569d97ebd4))(better-call@1.3.2(zod@4.3.6)) '@clerk/backend': specifier: ^1.25.2 version: 1.25.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -2099,7 +2102,7 @@ importers: version: link:../../../packages/stateless better-auth: specifier: ^1.4.9 - version: 1.5.6(b8a7a1c167ccbf2262fb279c12fb16c6) + version: 1.5.6(aac79383f71d2c0e811dbd569d97ebd4) better-call: specifier: ^1.3.2 version: 1.3.2(zod@4.3.6) @@ -2114,7 +2117,7 @@ importers: version: 3.1.0 mongoose: specifier: ^8.9.5 - version: 8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + version: 8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) passport: specifier: 0.7.0 version: 0.7.0 @@ -2214,7 +2217,7 @@ importers: version: 3.2.0(date-fns@4.1.0) mongoose: specifier: ^8.9.5 - version: 8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + version: 8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) rxjs: specifier: 7.8.1 version: 7.8.1 @@ -2433,7 +2436,7 @@ importers: version: 7.4.0(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/testing': specifier: 10.4.18 version: 10.4.18(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(@nestjs/platform-express@10.4.18) @@ -2514,7 +2517,7 @@ importers: version: 1.39.0 '@pulsecron/pulse': specifier: 1.6.8 - version: 1.6.8(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + version: 1.6.8(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) '@segment/analytics-node': specifier: ^1.1.4 version: 1.1.4(encoding@0.1.13) @@ -3318,6 +3321,40 @@ importers: specifier: 5.6.2 version: 5.6.2 + packages/chat-adapter-email: + dependencies: + '@novu/shared': + specifier: workspace:* + version: link:../shared + '@react-email/components': + specifier: 1.0.12 + version: 1.0.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-email/render': + specifier: 2.0.7 + version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + chat: + specifier: '>=4.25.0' + version: 4.26.0 + hast-util-to-html: + specifier: 9.0.5 + version: 9.0.5 + mdast-util-to-hast: + specifier: 13.2.1 + version: 13.2.1 + react: + specifier: '>=18.0.0 || >=19.0.0' + version: 19.2.3 + devDependencies: + '@types/react': + specifier: ^19.0.0 + version: 19.2.8 + rimraf: + specifier: ~3.0.2 + version: 3.0.2 + typescript: + specifier: 5.6.2 + version: 5.6.2 + packages/framework: dependencies: ajv: @@ -11299,42 +11336,98 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/body@0.3.0': + resolution: {integrity: sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/button@0.2.0': resolution: {integrity: sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/button@0.2.1': + resolution: {integrity: sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/code-block@0.1.0': resolution: {integrity: sha512-jSpHFsgqnQXxDIssE4gvmdtFncaFQz5D6e22BnVjcCPk/udK+0A9jRwGFEG8JD2si9ZXBmU4WsuqQEczuZn4ww==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/code-block@0.2.1': + resolution: {integrity: sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/code-inline@0.0.5': resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/code-inline@0.0.6': + resolution: {integrity: sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/column@0.0.13': resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/column@0.0.14': + resolution: {integrity: sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/components@0.5.7': resolution: {integrity: sha512-ECyVoyDcev2FSQ7C0buXaIJ0+6MRDXNUbCOZwBRrlLdCCRjap2b4+MHrYSTXFzo5kqfjjRoyo/2PbJXFQni67g==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/components@1.0.12': + resolution: {integrity: sha512-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/container@0.0.15': resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/container@0.0.16': + resolution: {integrity: sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/font@0.0.10': + resolution: {integrity: sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/font@0.0.9': resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==} peerDependencies: @@ -11346,48 +11439,103 @@ packages: peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/head@0.0.13': + resolution: {integrity: sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/heading@0.0.15': resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/heading@0.0.16': + resolution: {integrity: sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/hr@0.0.11': resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/hr@0.0.12': + resolution: {integrity: sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/html@0.0.11': resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/html@0.0.12': + resolution: {integrity: sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/img@0.0.11': resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/img@0.0.12': + resolution: {integrity: sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/link@0.0.12': resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/link@0.0.13': + resolution: {integrity: sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/markdown@0.0.16': resolution: {integrity: sha512-KSUHmoBMYhvc6iGwlIDkm0DRGbGQ824iNjLMCJsBVUoKHGQYs7F/N3b1tnS1YzRUX+GwHIexSsHuIUEi1m+8OQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/markdown@0.0.18': + resolution: {integrity: sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/preview@0.0.13': resolution: {integrity: sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/preview@0.0.14': + resolution: {integrity: sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/render@1.4.0': resolution: {integrity: sha512-ZtJ3noggIvW1ZAryoui95KJENKdCzLmN5F7hyZY1F/17B1vwzuxHB7YkuCg0QqHjDivc5axqYEYdIOw4JIQdUw==} engines: {node: '>=18.0.0'} @@ -11395,30 +11543,104 @@ packages: react: ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/render@2.0.6': + resolution: {integrity: sha512-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/render@2.0.7': + resolution: {integrity: sha512-XCsqujKURb4egU8+z7RX1/yxRx1Qo89uGhy6UXyB5Oxq1SK+48t0AD/3qeuDGgDvyS+Ti+0oDT3nn5/dcG4Ttg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/row@0.0.12': resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/row@0.0.13': + resolution: {integrity: sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/section@0.0.16': resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/section@0.0.17': + resolution: {integrity: sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/tailwind@1.2.2': resolution: {integrity: sha512-heO9Khaqxm6Ulm6p7HQ9h01oiiLRrZuuEQuYds/O7Iyp3c58sMVHZGIxiRXO/kSs857NZQycpjewEVKF3jhNTw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/tailwind@2.0.7': + resolution: {integrity: sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + '@react-email/body': '>=0' + '@react-email/button': '>=0' + '@react-email/code-block': '>=0' + '@react-email/code-inline': '>=0' + '@react-email/container': '>=0' + '@react-email/heading': '>=0' + '@react-email/hr': '>=0' + '@react-email/img': '>=0' + '@react-email/link': '>=0' + '@react-email/preview': '>=0' + '@react-email/text': '>=0' + react: ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@react-email/body': + optional: true + '@react-email/button': + optional: true + '@react-email/code-block': + optional: true + '@react-email/code-inline': + optional: true + '@react-email/container': + optional: true + '@react-email/heading': + optional: true + '@react-email/hr': + optional: true + '@react-email/img': + optional: true + '@react-email/link': + optional: true + '@react-email/preview': + optional: true + '@react-email/text@0.1.5': resolution: {integrity: sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-email/text@0.1.6': + resolution: {integrity: sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==} + engines: {node: '>=20.0.0'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@reduxjs/toolkit@2.8.2': resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} peerDependencies: @@ -27545,8 +27767,8 @@ snapshots: '@aws-crypto/sha1-browser': 3.0.0 '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.575.0 - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/core': 3.575.0 '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-bucket-endpoint': 3.575.0 @@ -27842,11 +28064,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.575.0': + '@aws-sdk/client-sso-oidc@3.575.0(@aws-sdk/client-sts@3.575.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/core': 3.575.0 '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-host-header': 3.575.0 @@ -27885,6 +28107,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: + - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)': @@ -28063,11 +28286,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)': + '@aws-sdk/client-sts@3.575.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.575.0 + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) '@aws-sdk/core': 3.575.0 '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-host-header': 3.575.0 @@ -28106,7 +28329,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.637.0': @@ -28311,7 +28533,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0)': dependencies: - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/credential-provider-env': 3.575.0 '@aws-sdk/credential-provider-process': 3.575.0 '@aws-sdk/credential-provider-sso': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) @@ -28364,25 +28586,6 @@ snapshots: - aws-crt optional: true - '@aws-sdk/credential-provider-ini@3.637.0(@aws-sdk/client-sts@3.637.0)': - dependencies: - '@aws-sdk/client-sts': 3.637.0 - '@aws-sdk/credential-provider-env': 3.620.1 - '@aws-sdk/credential-provider-http': 3.635.0 - '@aws-sdk/credential-provider-process': 3.620.1 - '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0) - '@aws-sdk/credential-provider-web-identity': 3.621.0(@aws-sdk/client-sts@3.637.0) - '@aws-sdk/types': 3.609.0 - '@smithy/credential-provider-imds': 3.2.0 - '@smithy/property-provider': 3.1.3 - '@smithy/shared-ini-file-loader': 3.1.4 - '@smithy/types': 3.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - optional: true - '@aws-sdk/credential-provider-ini@3.972.10': dependencies: '@aws-sdk/core': 3.973.26 @@ -28506,26 +28709,6 @@ snapshots: - aws-crt optional: true - '@aws-sdk/credential-provider-node@3.637.0(@aws-sdk/client-sts@3.637.0)': - dependencies: - '@aws-sdk/credential-provider-env': 3.620.1 - '@aws-sdk/credential-provider-http': 3.635.0 - '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sts@3.637.0) - '@aws-sdk/credential-provider-process': 3.620.1 - '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0) - '@aws-sdk/credential-provider-web-identity': 3.621.0(@aws-sdk/client-sts@3.637.0) - '@aws-sdk/types': 3.609.0 - '@smithy/credential-provider-imds': 3.2.0 - '@smithy/property-provider': 3.1.3 - '@smithy/shared-ini-file-loader': 3.1.4 - '@smithy/types': 3.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - '@aws-sdk/client-sts' - - aws-crt - optional: true - '@aws-sdk/credential-provider-node@3.972.11': dependencies: '@aws-sdk/credential-provider-env': 3.972.10 @@ -28664,7 +28847,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.575.0(@aws-sdk/client-sts@3.575.0)': dependencies: - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/types': 3.575.0 '@smithy/property-provider': 3.1.3 '@smithy/types': 3.3.0 @@ -28703,7 +28886,7 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-providers@3.637.0': + '@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.575.0)': dependencies: '@aws-sdk/client-cognito-identity': 3.637.0 '@aws-sdk/client-sso': 3.637.0 @@ -28711,8 +28894,8 @@ snapshots: '@aws-sdk/credential-provider-cognito-identity': 3.637.0 '@aws-sdk/credential-provider-env': 3.620.1 '@aws-sdk/credential-provider-http': 3.635.0 - '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sts@3.637.0) - '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.637.0) '@aws-sdk/credential-provider-process': 3.620.1 '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0) '@aws-sdk/credential-provider-web-identity': 3.621.0(@aws-sdk/client-sts@3.637.0) @@ -28726,7 +28909,7 @@ snapshots: - aws-crt optional: true - '@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.575.0)': + '@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))': dependencies: '@aws-sdk/client-cognito-identity': 3.637.0 '@aws-sdk/client-sso': 3.637.0 @@ -28734,10 +28917,10 @@ snapshots: '@aws-sdk/credential-provider-cognito-identity': 3.637.0 '@aws-sdk/credential-provider-env': 3.620.1 '@aws-sdk/credential-provider-http': 3.635.0 - '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.637.0) - '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-ini': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) + '@aws-sdk/credential-provider-node': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0))(@aws-sdk/client-sts@3.637.0) '@aws-sdk/credential-provider-process': 3.620.1 - '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/credential-provider-sso': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) '@aws-sdk/credential-provider-web-identity': 3.621.0(@aws-sdk/client-sts@3.637.0) '@aws-sdk/types': 3.609.0 '@smithy/credential-provider-imds': 3.2.0 @@ -29207,7 +29390,7 @@ snapshots: '@aws-sdk/token-providers@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.575.0 + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) '@aws-sdk/types': 3.575.0 '@smithy/property-provider': 3.1.3 '@smithy/shared-ini-file-loader': 3.1.4 @@ -29216,7 +29399,7 @@ snapshots: '@aws-sdk/token-providers@3.614.0(@aws-sdk/client-sso-oidc@3.575.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.575.0 + '@aws-sdk/client-sso-oidc': 3.575.0(@aws-sdk/client-sts@3.575.0) '@aws-sdk/types': 3.609.0 '@smithy/property-provider': 3.1.3 '@smithy/shared-ini-file-loader': 3.1.4 @@ -30624,7 +30807,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.39.0 '@standard-schema/spec': 1.1.0 - better-call: 1.3.2(zod@4.3.6) + better-call: 1.3.2(zod@4.3.5) jose: 6.1.3 kysely: 0.28.14 nanostores: 1.2.0 @@ -30647,33 +30830,33 @@ snapshots: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/utils': 0.3.1 - '@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))': + '@better-auth/mongo-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))': dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/utils': 0.3.1 optionalDependencies: - mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) '@better-auth/prisma-adapter@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)': dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/utils': 0.3.1 - '@better-auth/sso@1.4.7(better-auth@1.5.6(14eb71b76c563af6394284c30ec93f47))': + '@better-auth/sso@1.4.7(better-auth@1.5.6(f5eede22a65d6cd0c93a8bf1a87b70c5))': dependencies: '@better-fetch/fetch': 1.1.21 - better-auth: 1.5.6(14eb71b76c563af6394284c30ec93f47) + better-auth: 1.5.6(f5eede22a65d6cd0c93a8bf1a87b70c5) fast-xml-parser: 5.5.8 jose: 6.1.3 samlify: 2.10.2 zod: 4.3.5 - '@better-auth/sso@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(better-auth@1.5.6(b8a7a1c167ccbf2262fb279c12fb16c6))(better-call@1.3.2(zod@4.3.6))': + '@better-auth/sso@1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(better-auth@1.5.6(aac79383f71d2c0e811dbd569d97ebd4))(better-call@1.3.2(zod@4.3.6))': dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 - better-auth: 1.5.6(b8a7a1c167ccbf2262fb279c12fb16c6) + better-auth: 1.5.6(aac79383f71d2c0e811dbd569d97ebd4) better-call: 1.3.2(zod@4.3.6) fast-xml-parser: 5.5.8 jose: 6.1.3 @@ -33087,11 +33270,11 @@ snapshots: - ws optional: true - '@langchain/langgraph-checkpoint-mongodb@1.1.6(@aws-sdk/credential-providers@3.637.0)(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)': + '@langchain/langgraph-checkpoint-mongodb@1.1.6(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0))(@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)': dependencies: '@langchain/core': 1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0) '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.18(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.212.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0))(openai@6.17.0(ws@8.20.0)(zod@3.25.20))(ws@8.20.0)) - mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' @@ -33725,7 +33908,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.1 - '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.14.3)(@grpc/proto-loader@0.8.0)(@nestjs/axios@3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1))(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7))(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.18(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.18)(@nestjs/websockets@10.4.18)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -33737,7 +33920,7 @@ snapshots: '@grpc/grpc-js': 1.14.3 '@grpc/proto-loader': 0.8.0 '@nestjs/axios': 3.0.3(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.15.0)(rxjs@7.8.1) - mongoose: 8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + mongoose: 8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) '@nestjs/testing@10.4.18(@nestjs/common@10.4.18(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.18)(@nestjs/platform-express@10.4.18)': dependencies: @@ -36070,14 +36253,14 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@pulsecron/pulse@1.6.8(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)': + '@pulsecron/pulse@1.6.8(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)': dependencies: cron-parser: 4.9.0 date.js: 0.3.3 debug: 4.3.6 human-interval: 2.0.1 moment-timezone: 0.5.48 - mongodb: 6.8.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + mongodb: 6.8.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' @@ -38189,6 +38372,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/body@0.3.0(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/button@0.2.0(react@18.3.1)': dependencies: react: 18.3.1 @@ -38197,6 +38384,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/button@0.2.1(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/code-block@0.1.0(react@18.3.1)': dependencies: prismjs: 1.30.0 @@ -38207,6 +38398,11 @@ snapshots: prismjs: 1.30.0 react: 19.2.3 + '@react-email/code-block@0.2.1(react@19.2.3)': + dependencies: + prismjs: 1.30.0 + react: 19.2.3 + '@react-email/code-inline@0.0.5(react@18.3.1)': dependencies: react: 18.3.1 @@ -38215,6 +38411,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/code-inline@0.0.6(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/column@0.0.13(react@18.3.1)': dependencies: react: 18.3.1 @@ -38223,6 +38423,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/column@0.0.14(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/components@0.5.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@react-email/body': 0.1.0(react@18.3.1) @@ -38275,6 +38479,32 @@ snapshots: transitivePeerDependencies: - react-dom + '@react-email/components@1.0.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@react-email/body': 0.3.0(react@19.2.3) + '@react-email/button': 0.2.1(react@19.2.3) + '@react-email/code-block': 0.2.1(react@19.2.3) + '@react-email/code-inline': 0.0.6(react@19.2.3) + '@react-email/column': 0.0.14(react@19.2.3) + '@react-email/container': 0.0.16(react@19.2.3) + '@react-email/font': 0.0.10(react@19.2.3) + '@react-email/head': 0.0.13(react@19.2.3) + '@react-email/heading': 0.0.16(react@19.2.3) + '@react-email/hr': 0.0.12(react@19.2.3) + '@react-email/html': 0.0.12(react@19.2.3) + '@react-email/img': 0.0.12(react@19.2.3) + '@react-email/link': 0.0.13(react@19.2.3) + '@react-email/markdown': 0.0.18(react@19.2.3) + '@react-email/preview': 0.0.14(react@19.2.3) + '@react-email/render': 2.0.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-email/row': 0.0.13(react@19.2.3) + '@react-email/section': 0.0.17(react@19.2.3) + '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.3))(@react-email/button@0.2.1(react@19.2.3))(@react-email/code-block@0.2.1(react@19.2.3))(@react-email/code-inline@0.0.6(react@19.2.3))(@react-email/container@0.0.16(react@19.2.3))(@react-email/heading@0.0.16(react@19.2.3))(@react-email/hr@0.0.12(react@19.2.3))(@react-email/img@0.0.12(react@19.2.3))(@react-email/link@0.0.13(react@19.2.3))(@react-email/preview@0.0.14(react@19.2.3))(@react-email/text@0.1.6(react@19.2.3))(react@19.2.3) + '@react-email/text': 0.1.6(react@19.2.3) + react: 19.2.3 + transitivePeerDependencies: + - react-dom + '@react-email/container@0.0.15(react@18.3.1)': dependencies: react: 18.3.1 @@ -38283,6 +38513,14 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/container@0.0.16(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/font@0.0.10(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/font@0.0.9(react@18.3.1)': dependencies: react: 18.3.1 @@ -38299,6 +38537,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/head@0.0.13(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/heading@0.0.15(react@18.3.1)': dependencies: react: 18.3.1 @@ -38307,6 +38549,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/heading@0.0.16(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/hr@0.0.11(react@18.3.1)': dependencies: react: 18.3.1 @@ -38315,6 +38561,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/hr@0.0.12(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/html@0.0.11(react@18.3.1)': dependencies: react: 18.3.1 @@ -38323,6 +38573,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/html@0.0.12(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/img@0.0.11(react@18.3.1)': dependencies: react: 18.3.1 @@ -38331,6 +38585,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/img@0.0.12(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/link@0.0.12(react@18.3.1)': dependencies: react: 18.3.1 @@ -38339,6 +38597,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/link@0.0.13(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/markdown@0.0.16(react@18.3.1)': dependencies: marked: 15.0.12 @@ -38349,6 +38611,11 @@ snapshots: marked: 15.0.12 react: 19.2.3 + '@react-email/markdown@0.0.18(react@19.2.3)': + dependencies: + marked: 15.0.12 + react: 19.2.3 + '@react-email/preview@0.0.13(react@18.3.1)': dependencies: react: 18.3.1 @@ -38357,6 +38624,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/preview@0.0.14(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/render@1.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: html-to-text: 9.0.5 @@ -38373,6 +38644,20 @@ snapshots: react-dom: 19.2.3(react@19.2.3) react-promise-suspense: 0.3.4 + '@react-email/render@2.0.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.7.4 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@react-email/render@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.7.4 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@react-email/row@0.0.12(react@18.3.1)': dependencies: react: 18.3.1 @@ -38381,6 +38666,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/row@0.0.13(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/section@0.0.16(react@18.3.1)': dependencies: react: 18.3.1 @@ -38389,6 +38678,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/section@0.0.17(react@19.2.3)': + dependencies: + react: 19.2.3 + '@react-email/tailwind@1.2.2(react@18.3.1)': dependencies: react: 18.3.1 @@ -38397,6 +38690,23 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.3))(@react-email/button@0.2.1(react@19.2.3))(@react-email/code-block@0.2.1(react@19.2.3))(@react-email/code-inline@0.0.6(react@19.2.3))(@react-email/container@0.0.16(react@19.2.3))(@react-email/heading@0.0.16(react@19.2.3))(@react-email/hr@0.0.12(react@19.2.3))(@react-email/img@0.0.12(react@19.2.3))(@react-email/link@0.0.13(react@19.2.3))(@react-email/preview@0.0.14(react@19.2.3))(@react-email/text@0.1.6(react@19.2.3))(react@19.2.3)': + dependencies: + '@react-email/text': 0.1.6(react@19.2.3) + react: 19.2.3 + tailwindcss: 4.1.18 + optionalDependencies: + '@react-email/body': 0.3.0(react@19.2.3) + '@react-email/button': 0.2.1(react@19.2.3) + '@react-email/code-block': 0.2.1(react@19.2.3) + '@react-email/code-inline': 0.0.6(react@19.2.3) + '@react-email/container': 0.0.16(react@19.2.3) + '@react-email/heading': 0.0.16(react@19.2.3) + '@react-email/hr': 0.0.12(react@19.2.3) + '@react-email/img': 0.0.12(react@19.2.3) + '@react-email/link': 0.0.13(react@19.2.3) + '@react-email/preview': 0.0.14(react@19.2.3) + '@react-email/text@0.1.5(react@18.3.1)': dependencies: react: 18.3.1 @@ -38405,6 +38715,10 @@ snapshots: dependencies: react: 19.2.3 + '@react-email/text@0.1.6(react@19.2.3)': + dependencies: + react: 19.2.3 + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': dependencies: '@standard-schema/spec': 1.1.0 @@ -43881,13 +44195,13 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - better-auth@1.5.6(14eb71b76c563af6394284c30ec93f47): + better-auth@1.5.6(aac79383f71d2c0e811dbd569d97ebd4): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14) '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)) + '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)) '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0)) '@better-auth/utils': 0.3.1 @@ -43901,50 +44215,59 @@ snapshots: nanostores: 1.2.0 zod: 4.3.6 optionalDependencies: - '@sveltejs/kit': 2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@2.5.3(svelte@5.53.5)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)))(svelte@5.53.5)(typescript@5.6.2)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)) - mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + '@sveltejs/kit': 2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@2.5.3(svelte@5.53.5)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.5.8))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)))(svelte@5.53.5)(typescript@5.6.2)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.5.8))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)) + mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) next: 16.2.3(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.8) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) solid-js: 1.9.6 svelte: 5.53.5 - vitest: 2.1.9(@edge-runtime/vm@4.0.2)(@types/node@22.15.13)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3) + vitest: 2.1.9(@edge-runtime/vm@4.0.2)(@types/node@22.15.13)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.5.8))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' - better-auth@1.5.6(b8a7a1c167ccbf2262fb279c12fb16c6): + better-auth@1.5.6(f5eede22a65d6cd0c93a8bf1a87b70c5): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14) '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)) + '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7)) '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.14)(nanostores@1.2.0)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 '@noble/hashes': 2.0.1 - better-call: 1.3.2(zod@4.3.6) + better-call: 1.3.2(zod@4.3.5) defu: 6.1.6 jose: 6.1.3 kysely: 0.28.14 nanostores: 1.2.0 zod: 4.3.6 optionalDependencies: - '@sveltejs/kit': 2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@2.5.3(svelte@5.53.5)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.5.8))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)))(svelte@5.53.5)(typescript@5.6.2)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.5.8))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)) - mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + '@sveltejs/kit': 2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@2.5.3(svelte@5.53.5)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)))(svelte@5.53.5)(typescript@5.6.2)(vite@6.4.2(@types/node@22.15.13)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3)) + mongodb: 6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) next: 16.2.3(@babel/core@7.28.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.77.8) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) solid-js: 1.9.6 svelte: 5.53.5 - vitest: 2.1.9(@edge-runtime/vm@4.0.2)(@types/node@22.15.13)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.5.8))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3) + vitest: 2.1.9(@edge-runtime/vm@4.0.2)(@types/node@22.15.13)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.0)(less@4.2.0)(lightningcss@1.30.2)(sass@1.77.8)(sugarss@4.0.1(postcss@8.4.47))(terser@5.31.6)(tsx@4.19.0)(yaml@2.8.3) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' + better-call@1.3.2(zod@4.3.5): + dependencies: + '@better-auth/utils': 0.3.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 3.1.0 + optionalDependencies: + zod: 4.3.5 + better-call@1.3.2(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.1 @@ -45570,6 +45893,7 @@ snapshots: ms: 2.1.3 optionalDependencies: supports-color: 8.1.1 + optional: true debug@4.3.1(supports-color@8.1.1): dependencies: @@ -51703,33 +52027,33 @@ snapshots: gcp-metadata: 5.3.0(encoding@0.1.13) socks: 2.8.7 - mongodb@6.20.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): + mongodb@6.20.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): dependencies: '@mongodb-js/saslprep': 1.4.4 bson: 6.10.4 mongodb-connection-string-url: 3.0.2 optionalDependencies: - '@aws-sdk/credential-providers': 3.637.0 + '@aws-sdk/credential-providers': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) gcp-metadata: 5.3.0(encoding@0.1.13) socks: 2.8.7 - mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): + mongodb@6.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): dependencies: '@mongodb-js/saslprep': 1.4.4 bson: 6.10.4 mongodb-connection-string-url: 3.0.2 optionalDependencies: - '@aws-sdk/credential-providers': 3.637.0 + '@aws-sdk/credential-providers': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) gcp-metadata: 5.3.0(encoding@0.1.13) socks: 2.8.7 - mongodb@6.8.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): + mongodb@6.8.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): dependencies: '@mongodb-js/saslprep': 1.1.8 bson: 6.8.0 mongodb-connection-string-url: 3.0.1 optionalDependencies: - '@aws-sdk/credential-providers': 3.637.0 + '@aws-sdk/credential-providers': 3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)) gcp-metadata: 5.3.0(encoding@0.1.13) socks: 2.8.7 @@ -51756,11 +52080,11 @@ snapshots: - socks - supports-color - mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): + mongoose@8.21.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7): dependencies: bson: 6.10.4 kareem: 2.6.3 - mongodb: 6.20.0(@aws-sdk/credential-providers@3.637.0)(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) + mongodb: 6.20.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.637.0(@aws-sdk/client-sts@3.637.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.8.7) mpath: 0.9.0 mquery: 5.0.0 ms: 2.1.3 @@ -51893,7 +52217,7 @@ snapshots: needle@3.2.0: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) iconv-lite: 0.6.3 sax: 1.5.0 transitivePeerDependencies: @@ -53223,7 +53547,7 @@ snapshots: portfinder@1.0.32: dependencies: async: 2.6.4 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) mkdirp: 0.5.6 transitivePeerDependencies: - supports-color