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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 88 additions & 5 deletions apps/api/src/app/environments-v1/novu-bridge-client.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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[] = [];

/*
Expand All @@ -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,
Expand All @@ -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<void>;

await bridgeHandler(req, res);
}
}
2 changes: 2 additions & 0 deletions apps/api/src/app/environments-v1/novu-bridge.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -50,6 +51,7 @@ export const featureFlagsService = {
};

@Module({
imports: [SharedModule],
controllers: [NovuBridgeController],
providers: [
{
Expand Down
Loading