diff --git a/apps/api/src/app/environments-v1/novu-bridge-client.ts b/apps/api/src/app/environments-v1/novu-bridge-client.ts index 39f8eb3aa7a..ddf123673db 100644 --- a/apps/api/src/app/environments-v1/novu-bridge-client.ts +++ b/apps/api/src/app/environments-v1/novu-bridge-client.ts @@ -1,4 +1,11 @@ -import { Inject } from '@nestjs/common'; +import { Inject, NotFoundException } from '@nestjs/common'; +import { + GetDecryptedSecretKey, + GetDecryptedSecretKeyCommand, + InMemoryLRUCacheService, + InMemoryLRUCacheStore, + PinoLogger, +} from '@novu/application-generic'; import { PostActionEnum, type Workflow } from '@novu/framework/internal'; import { Client, NovuHandler, NovuRequestHandler } from '@novu/framework/nest'; import { EnvironmentTypeEnum } from '@novu/shared'; @@ -18,10 +25,23 @@ export const frameworkName = 'novu-nest'; export class NovuBridgeClient { constructor( @Inject(NovuHandler) private novuHandler: NovuHandler, - private constructFrameworkWorkflow: ConstructFrameworkWorkflow + private constructFrameworkWorkflow: ConstructFrameworkWorkflow, + private getDecryptedSecretKey: GetDecryptedSecretKey, + private inMemoryLRUCacheService: InMemoryLRUCacheService, + private logger: PinoLogger ) {} public async handleRequest(req: Request, res: Response) { + const environmentId = req.params.environmentId; + if (!environmentId || !String(environmentId).trim()) { + res.status(400).json({ + error: 'Missing or invalid environmentId', + details: 'The bridge route requires a non-empty environmentId path parameter.', + }); + + return; + } + const workflows: Workflow[] = []; /* @@ -32,7 +52,7 @@ export class NovuBridgeClient { if (Object.values(PostActionEnum).includes(req.query.action as PostActionEnum)) { const programmaticallyConstructedWorkflow = await this.constructFrameworkWorkflow.execute( ConstructFrameworkWorkflowCommand.create({ - environmentId: req.params.environmentId, + environmentId, workflowId: req.query.workflowId as string, layoutId: req.query.layoutId as string, controlValues: req.body.controls, @@ -46,13 +66,76 @@ export class NovuBridgeClient { workflows.push(programmaticallyConstructedWorkflow); } + const cacheKey = `bridge-secret-key:${environmentId}`; + const storeName = InMemoryLRUCacheStore.VALIDATOR; + + let secretKey: string; + try { + const resolved = await this.inMemoryLRUCacheService.get( + storeName, + cacheKey, + () => + this.getDecryptedSecretKey.execute( + GetDecryptedSecretKeyCommand.create({ + environmentId, + }) + ), + { + environmentId, + cacheVariant: 'bridge-secret-key', + } + ); + + if (typeof resolved !== 'string' || !resolved.trim()) { + this.logger.error( + `Bridge secret key missing or invalid after cache lookup (store=${storeName}, cacheKey=${cacheKey}, environmentId=${environmentId})` + ); + res.status(500).json({ + error: 'Failed to resolve environment secret key', + details: `Empty or invalid secret from ${storeName} for cache key ${cacheKey}.`, + }); + + return; + } + + secretKey = resolved; + } catch (error) { + if (error instanceof NotFoundException) { + this.logger.warn( + `Environment not found for bridge secret (store=${storeName}, cacheKey=${cacheKey}): ${error.message}` + ); + res.status(404).json({ + error: 'Environment not found', + details: `No environment for cache key ${cacheKey} (${storeName}).`, + }); + + return; + } + + this.logger.error( + { err: error }, + `Failed to resolve bridge secret key (store=${storeName}, cacheKey=${cacheKey}, environmentId=${environmentId})` + ); + res.status(500).json({ + error: 'Failed to resolve environment secret key', + details: `Unexpected error while loading secret via ${storeName} for cache key ${cacheKey}.`, + }); + + return; + } + const novuRequestHandler = new NovuRequestHandler({ frameworkName, workflows, - client: new Client({ secretKey: 'INTERNAL_KEY', strictAuthentication: false, verbose: false }), + client: new Client({ secretKey, strictAuthentication: true, verbose: false }), handler: this.novuHandler.handler, }); - await novuRequestHandler.createHandler()(req as any, res as any); + const bridgeHandler = novuRequestHandler.createHandler() as ( + request: Request, + response: Response + ) => void | Promise; + + await bridgeHandler(req, res); } } diff --git a/apps/api/src/app/environments-v1/novu-bridge.module.ts b/apps/api/src/app/environments-v1/novu-bridge.module.ts index 02e654f8724..6587218e974 100644 --- a/apps/api/src/app/environments-v1/novu-bridge.module.ts +++ b/apps/api/src/app/environments-v1/novu-bridge.module.ts @@ -25,6 +25,7 @@ import { } from '@novu/dal'; import { NovuClient, NovuHandler } from '@novu/framework/nest'; import { GetOrganizationSettings } from '../organization/usecases/get-organization-settings/get-organization-settings.usecase'; +import { SharedModule } from '../shared/shared.module'; import { NovuBridgeController } from './novu-bridge.controller'; import { NovuBridgeClient } from './novu-bridge-client'; import { ConstructFrameworkWorkflow } from './usecases/construct-framework-workflow'; @@ -50,6 +51,7 @@ export const featureFlagsService = { }; @Module({ + imports: [SharedModule], controllers: [NovuBridgeController], providers: [ {