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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .env.demo
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ NATS_HOST=your-ip
NATS_PORT=4222
NATS_URL=nats://your-ip:4222

NATS_PASSWORD=xxxx
NATS_USER=xxxx

REDIS_HOST=your-ip
REDIS_PORT=6379

Expand Down Expand Up @@ -230,3 +233,16 @@ PRISMA_LOGS = error

# DB_ALERT_ENABLE=
# DB_ALERT_EMAILS=



CLIENT_EMAIL=
PRIVATE_KEY=
PROJECT_ID=

Comment thread
coderabbitai[bot] marked this conversation as resolved.
CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000)
CONSUMER_CONFIG_MAX_DELIVER=4
Comment on lines +243 to +244

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix CONSUMER_CONFIG_ACK_WAIT to avoid NaN.

Number("10_000") returns NaN, so ack_wait becomes invalid. Use a plain numeric string (and quote it to satisfy dotenv-linter).

✅ Suggested fix
-CONSUMER_CONFIG_ACK_WAIT=10_000  # IN nanos(10_000)
+CONSUMER_CONFIG_ACK_WAIT="10000" # milliseconds; parsed via Number(...)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000)
CONSUMER_CONFIG_MAX_DELIVER=4
CONSUMER_CONFIG_ACK_WAIT="10000" # milliseconds; parsed via Number(...)
CONSUMER_CONFIG_MAX_DELIVER=4
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 243-243: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In @.env.demo around lines 243 - 244, The env value for CONSUMER_CONFIG_ACK_WAIT
uses an underscore-style numeric literal which causes Number("10_000") to return
NaN and makes ack_wait invalid; change the value to a plain numeric string like
"10000" (quoted to satisfy dotenv-linter) so code that parses
Number(process.env.CONSUMER_CONFIG_ACK_WAIT) yields a valid number for ack_wait.


AGGREGATE_STREAM=aggregate
DID_STREAM=did-notify
PULL_CONSUMER=hub-pull-consumer
15 changes: 15 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ NATS_HOST='0.0.0.0'
NATS_PORT=4222
NATS_URL=nats://0.0.0.0:4222

NATS_PASSWORD=xxxx
NATS_USER=xxxx

REDIS_HOST='0.0.0.0'
REDIS_PORT=6379

Expand Down Expand Up @@ -259,3 +262,15 @@ RESEND_API_KEY=re_xxxxxxxxxx
# DB_ALERT_EMAILS=
# Boolean: to enable/disable db alerts. This needs the 'utility' microservice
# DB_ALERT_ENABLE=


CLIENT_EMAIL=
PRIVATE_KEY=
PROJECT_ID=

Comment on lines +265 to +270

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clean up the new env block ordering/spacing.

dotenv-linter flags extra blank lines and key ordering (CLIENT_EMAIL/PRIVATE_KEY before PROJECT_ID).

🧹 Suggested cleanup
-
-
-PROJECT_ID=
-CLIENT_EMAIL=
-PRIVATE_KEY=
+CLIENT_EMAIL=
+PRIVATE_KEY=
+PROJECT_ID=
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PROJECT_ID=
CLIENT_EMAIL=
PRIVATE_KEY=
CLIENT_EMAIL=
PRIVATE_KEY=
PROJECT_ID=
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 259-259: [ExtraBlankLine] Extra blank line detected

(ExtraBlankLine)


[warning] 261-261: [UnorderedKey] The CLIENT_EMAIL key should go before the PROJECT_ID key

(UnorderedKey)


[warning] 262-262: [UnorderedKey] The PRIVATE_KEY key should go before the PROJECT_ID key

(UnorderedKey)

🤖 Prompt for AI Agents
In @.env.sample around lines 258 - 263, Remove the extra blank lines and reorder
the keys in .env.sample so that CLIENT_EMAIL and PRIVATE_KEY appear before
PROJECT_ID (no blank lines between the three entries), i.e. place CLIENT_EMAIL=
then PRIVATE_KEY= then PROJECT_ID= on consecutive lines to satisfy dotenv-linter
key ordering and spacing rules; keep the variable names exactly as shown
(CLIENT_EMAIL, PRIVATE_KEY, PROJECT_ID).

CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000)
CONSUMER_CONFIG_MAX_DELIVER=4
Comment on lines +271 to +272

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix CONSUMER_CONFIG_ACK_WAIT to avoid NaN.

Number("10_000") returns NaN, so ack_wait becomes invalid. Use a plain numeric string and quote it.

✅ Suggested fix
-CONSUMER_CONFIG_ACK_WAIT=10_000  # IN nanos(10_000)
+CONSUMER_CONFIG_ACK_WAIT="10000" # milliseconds; parsed via Number(...)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CONSUMER_CONFIG_ACK_WAIT=10_000 # IN nanos(10_000)
CONSUMER_CONFIG_MAX_DELIVER=4
CONSUMER_CONFIG_ACK_WAIT="10000" # milliseconds; parsed via Number(...)
CONSUMER_CONFIG_MAX_DELIVER=4
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 264-264: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In @.env.sample around lines 264 - 265, The CONSUMER_CONFIG_ACK_WAIT value in
.env.sample is set to 10_000 which becomes NaN when parsed (e.g.,
Number("10_000")), so change the variable CONSUMER_CONFIG_ACK_WAIT to a plain
numeric string (e.g., 10000) and quote it so the parser receives a valid number;
ensure any code that reads this env (ack_wait) still expects a numeric string
and converts it (Number or parseInt) accordingly.


AGGREGATE_STREAM=aggregate
DID_STREAM=did-notify
PULL_CONSUMER=hub-pull-consumer
17 changes: 17 additions & 0 deletions apps/api-gateway/src/authz/guards/client-access-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class ClientAccessGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();

const { user } = request;

if (!user || !Object.prototype.hasOwnProperty.call(user, 'client_id')) {
throw new UnauthorizedException('You do not have access');
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return true;
}
}
1 change: 1 addition & 0 deletions apps/api-gateway/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async function bootstrap(): Promise<void> {
xssFilter: true
})
);
Logger.log('API-Gateway is listening to NATS', 'NATS-CONNECTION');
await app.listen(process.env.API_GATEWAY_PORT, `${process.env.API_GATEWAY_HOST}`);
Logger.log(`API Gateway is listening on port ${process.env.API_GATEWAY_PORT}`, 'Success');

Expand Down
7 changes: 6 additions & 1 deletion apps/api-gateway/src/notification/notification.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CustomExceptionFilter } from '@credebl/common/exception-handler';
import { Body, Controller, HttpStatus, Logger, Post, Res, UseFilters } from '@nestjs/common';
import { Body, Controller, HttpStatus, Logger, Post, Res, UseFilters, UseGuards } from '@nestjs/common';
import {
ApiBearerAuth,
ApiExcludeEndpoint,
ApiForbiddenResponse,
ApiOperation,
Expand All @@ -20,6 +21,8 @@ import { IResponse } from '@credebl/common/interfaces/response.interface';
import { Response } from 'express';
import { ResponseMessages } from '@credebl/common/response-messages';
import { NotificationService } from './notification.service';
import { ClientAccessGuard } from '../authz/guards/client-access-guard';
import { AuthGuard } from '@nestjs/passport';

@Controller('notification')
@UseFilters(CustomExceptionFilter)
Expand Down Expand Up @@ -93,6 +96,8 @@ export class NotificationController {
*/
@Post('/register/holder-notification')
// @ApiExcludeEndpoint()
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'), ClientAccessGuard)
@ApiOperation({
summary: `Register holder for notification`,
description: `Register holder for notification`
Expand Down
9 changes: 9 additions & 0 deletions apps/notification/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@credebl/notification",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"firebase-admin": "^13.6.0"
}
}
14 changes: 14 additions & 0 deletions apps/notification/src/nats/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface INatsEventMessage {
event: string;
sessionId: string;
timestamp: string;
senderCode: string;
}

export interface INatsUserRequestData {
did: string;
sessions: {
sessionId: string;
orgCode: string;
}[];
}
164 changes: 164 additions & 0 deletions apps/notification/src/nats/jetstream.consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { Consumer } from 'nats';
import { NatsService } from './nats.service';
import {
DID_STREAM,
ensureConsumer,
ensureDidStream,
publishToJetStream,
PULL_CONSUMER,
AGGREGATE_STREAM
} from './jetstream.setup';
import { PendingAckStore } from './pendingAckStore';
import { HolderNotificationRepository } from '../holder-notification.repository';
import { Message } from 'firebase-admin/lib/messaging/messaging-api';
import * as admin from 'firebase-admin';
import { IHolderNotification } from '@credebl/common/interfaces/holder-notification.interfaces';

const EVENT_PRESENTATION_ACK = 'presentation.ack';
const EVENT_PRESENTATION_PURGED = 'presentation.purged';
@Injectable()
export class JetStreamConsumer implements OnApplicationBootstrap {
constructor(
private readonly nats: NatsService,
private readonly logger: Logger,
private readonly pendingAckStore: PendingAckStore,
private readonly holderNotificationRepository: HolderNotificationRepository
) {}

async onApplicationBootstrap(): Promise<void> {
if (!admin.apps.length) {
const projectId = process.env.PROJECT_ID;
const clientEmail = process.env.CLIENT_EMAIL;
const privateKey = process.env.PRIVATE_KEY;

if (!projectId || !clientEmail || !privateKey) {
throw new Error('Missing Firebase credentials: PROJECT_ID, CLIENT_EMAIL, and PRIVATE_KEY are required');
}
admin.initializeApp({
credential: admin.credential.cert({
projectId,
clientEmail,
privateKey: privateKey.replace(/\\n/g, '\n')
})
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const js = this.nats.jetstream(); // ✅ now safe

const jsm = this.nats.jetstreamManager();
await ensureConsumer(jsm);
const consumer: Consumer = await js.consumers.get(AGGREGATE_STREAM, PULL_CONSUMER);

this.logger.log('[NATS] JetStream consumer started');

this.consume(consumer).catch((err) => {
this.logger.error('[NATS] Consumer crashed', err);
});
}

private async consume(consumer: Consumer): Promise<void> {
this.logger.log(`[NATS] Starting to consume messages from consumer ${consumer.info}`);
for await (const msg of await consumer.consume()) {
try {
const { subject } = msg;
this.logger.log(`[NATS] Message subject: ${subject}`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, domain, event, orgCode, sessionId] = subject.split('.');
const consumerName = `notify-session-${sessionId}`;

this.logger.log({
domain,
event,
orgCode,
sessionId
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const payload = msg.json();
const notificationDetail = await this.holderNotificationRepository.getHolderNotificationBySessionId(sessionId);
this.logger.debug(`[NATS] Message received ${JSON.stringify(payload)}`);
this.logger.log(`[NATS] Processing message, ${JSON.stringify({ deliveryCount: msg.info.deliveryCount })}`);
const maxDeliver = process.env.CONSUMER_CONFIG_MAX_DELIVER
? Number(process.env.CONSUMER_CONFIG_MAX_DELIVER)
: 4;
if (maxDeliver - 1 < msg.info.deliveryCount) {
//------------ Moving message to DID stream ---------------//
await this.moveMsgToDidStream(notificationDetail, msg);

//------------- Sending Push Notification via FCM ----------------//
this.sendNotificationToHolder(event, orgCode, notificationDetail);
msg.ack();
Comment on lines +84 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing null check for notificationDetail in maxDeliver branch.

When deliveryCount exceeds maxDeliver - 1, the code calls moveMsgToDidStream(notificationDetail, msg) and sendNotificationToHolder(event, orgCode, notificationDetail) without verifying notificationDetail is not null. This will cause a runtime error if the notification detail doesn't exist.

🔧 Proposed fix
         if (maxDeliver - 1 < msg.info.deliveryCount) {
+          if (!notificationDetail) {
+            this.logger.error(`[NATS] No notification detail found for session ID: ${sessionId}, message exhausted`);
+            msg.ack(); // Ack to prevent infinite retries since max deliver reached
+            continue;
+          }
           //------------ Moving message to DID stream ---------------//
           await this.moveMsgToDidStream(notificationDetail, msg);
🤖 Prompt for AI Agents
In `@apps/notification/src/nats/jetstream.consumer.ts` around lines 84 - 90, The
branch that handles deliveries exceeding maxDeliver calls
moveMsgToDidStream(notificationDetail, msg) and sendNotificationToHolder(event,
orgCode, notificationDetail) without checking notificationDetail; add a
null/undefined guard before these calls (e.g., if (!notificationDetail) {
log.warn/handle and ack/nack as appropriate } ) to avoid runtime errors, and
only call moveMsgToDidStream and sendNotificationToHolder when
notificationDetail is present; reference the variables notificationDetail and
msg and the methods moveMsgToDidStream and sendNotificationToHolder to locate
and update the logic.

} else {
// ------------- Publishing Messages via NATS ----------------//

const ackKey = this.pendingAckStore.save(AGGREGATE_STREAM, consumerName, msg);

this.logger.log(`[NATS] Notification detail fetched for session ID: ${sessionId}`);
if (!notificationDetail) {
this.logger.error(`[NATS] No notification detail found for session ID: ${sessionId}`);
msg.nak();
continue;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
await this.nats.publish(`${notificationDetail.holderDid}`, {
payload,
ackKey,
subject: msg.subject,
event: `${domain}.${event}`
});

this.logger.log(`[NATS] Message published to ${notificationDetail.holderDid} for session ${sessionId}`);
}

// business logic
// msg.ack();
} catch (err) {
this.logger.error('[NATS] Processing failed', err);
msg.nak();
}
}
}

private sendNotificationToHolder(event: string, orgCode: string, notificationDetail: IHolderNotification): void {
let notificationTitle = '';
if (EVENT_PRESENTATION_ACK === event) {
notificationTitle = `Your data delivered to ${orgCode}`;
} else if (EVENT_PRESENTATION_PURGED === event) {
notificationTitle = `Your data purged from ${orgCode}`;
}
this.logger.log('Now push notifications will be sent to user');
const notificationPayload: Message = {
notification: {
title: notificationTitle,
body: `Open an app to view more details`
},
token: notificationDetail.fcmToken
};
// Send the notification to the specified device
admin
.messaging()
.send(notificationPayload)
.then((response) => {
this.logger.log('Successfully sent message:', response);
})
.catch((error) => {
this.logger.error('Error sending message:', error);
});
}

private async moveMsgToDidStream(notificationDetail: IHolderNotification, msg): Promise<void> {
this.logger.log('[NATS] Moving message to DID stream for FCM notification');
//const notifyStream = process.env.STREAM_NOTIFY ?? "notify";
const subjectName = `${DID_STREAM}.${notificationDetail.holderDid}`;
const jsm = this.nats.jetstreamManager();
const jsc = this.nats.jetstream();
await ensureDidStream(jsm);

const decoder = new TextDecoder(); // or 'UTF-8', 'windows-1251', etc.
const msgData = decoder.decode(msg.data);

//await ensureStreamExists(streamName);
// Publish to JetStream for guaranteed delivery
const jsAck = await publishToJetStream(subjectName, msgData, jsc);
this.logger.log(`[NATS] Message moved to DID stream: ${JSON.stringify(jsAck)}`);
}
}
Loading