From 9fdd296d70a6f302ef25038cd0a6b23df6ef6089 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 16 Jun 2026 13:57:25 +0530 Subject: [PATCH 01/58] wip Signed-off-by: sujitaw --- apps/api-gateway/src/main.ts | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 67f0d0bb4..86c69c46f 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -21,6 +21,46 @@ import { EcosystemSwaggerFilter } from './authz/guards/ecosystem-swagger.filter' dotenv.config(); async function bootstrap(): Promise { + const baoUrl = process.env.BAO_URL; + const baoToken = process.env.BAO_TOKEN; + const secretPath = process.env.BAO_SECRET_PATH; + + if (!baoToken) { + throw new Error('Missing BAO_TOKEN environment variable'); + } + + // Fetch secrets from OpenBao before initializing the NestJS ConfigModule + // console.log('🔐 Fetching secrets from OpenBao...',baoUrl, baoToken, secretPath); + try { + const response = await fetch(`${baoUrl}/v1/${secretPath}`, { + method: 'GET', + headers: { + 'X-Vault-Token': baoToken, + 'Content-Type': 'application/json' + } + }); + // console.log('Received response from OpenBao with status:', response); + if (!response.ok) { + throw new Error(`OpenBao responded with status: ${response.status}`); + } + + const result = await response.json(); + + // OpenBao structures KV v2 data inside data.data + const secrets = result.data.data; + + // Inject the fetched secrets into the process.env so NestJS ConfigService can read them + Object.keys(secrets).forEach((key) => { + process.env[key] = secrets[key]; + }); + + // console.log('✅ Environment variables successfully loaded from OpenBao',secrets); + } catch (error) { + // eslint-disable-next-line no-console + console.error('❌ Failed to load secrets from OpenBao:', error.message); + process.exit(1); // Stop the app if secrets can't be loaded + } + try { if (otelSDK) { await otelSDK.start(); From 30435067b94323afbfcac821c52cf7dcac9fb24c Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 16 Jun 2026 14:08:11 +0530 Subject: [PATCH 02/58] comments code rabbit Signed-off-by: sujitaw --- apps/api-gateway/src/main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 86c69c46f..4ad189833 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -51,7 +51,9 @@ async function bootstrap(): Promise { // Inject the fetched secrets into the process.env so NestJS ConfigService can read them Object.keys(secrets).forEach((key) => { - process.env[key] = secrets[key]; + if ('__proto__' !== key && 'constructor' !== key && 'prototype' !== key) { + process.env[key] = secrets[key]; + } }); // console.log('✅ Environment variables successfully loaded from OpenBao',secrets); From 78e89afa65a825c0a297d03145f99a088c618384 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 16 Jun 2026 15:53:03 +0530 Subject: [PATCH 03/58] fix comments Signed-off-by: sujitaw --- apps/api-gateway/src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 4ad189833..2564ed36d 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -51,7 +51,7 @@ async function bootstrap(): Promise { // Inject the fetched secrets into the process.env so NestJS ConfigService can read them Object.keys(secrets).forEach((key) => { - if ('__proto__' !== key && 'constructor' !== key && 'prototype' !== key) { + if (Object.prototype.hasOwnProperty.call(secrets, key)) { process.env[key] = secrets[key]; } }); From 901dfd36e115604b229cce760a0761521c8aeac4 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 16 Jun 2026 16:00:44 +0530 Subject: [PATCH 04/58] fix comments Signed-off-by: sujitaw --- apps/api-gateway/src/main.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 2564ed36d..9188685de 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -49,11 +49,32 @@ async function bootstrap(): Promise { // OpenBao structures KV v2 data inside data.data const secrets = result.data.data; - // Inject the fetched secrets into the process.env so NestJS ConfigService can read them + const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); + + if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { + throw new Error('Unexpected secrets payload received from OpenBao'); + } + Object.keys(secrets).forEach((key) => { - if (Object.prototype.hasOwnProperty.call(secrets, key)) { - process.env[key] = secrets[key]; + if (FORBIDDEN_KEYS.has(key)) { + // eslint-disable-next-line no-console + console.warn(`Skipping forbidden key from OpenBao response: ${key}`); + return; } + + if (!Object.prototype.hasOwnProperty.call(secrets, key)) { + return; + } + + const value = secrets[key]; + + // process.env values must be strings + Object.defineProperty(process.env, key, { + value: String(value), + enumerable: true, + configurable: true, + writable: true + }); }); // console.log('✅ Environment variables successfully loaded from OpenBao',secrets); From 383aaf4c72b3ea0beb1a2196681d331eeb6414c6 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 16 Jun 2026 16:12:22 +0530 Subject: [PATCH 05/58] fix comments Signed-off-by: sujitaw --- apps/api-gateway/src/main.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 9188685de..210b64e78 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -48,7 +48,6 @@ async function bootstrap(): Promise { // OpenBao structures KV v2 data inside data.data const secrets = result.data.data; - const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { @@ -57,20 +56,11 @@ async function bootstrap(): Promise { Object.keys(secrets).forEach((key) => { if (FORBIDDEN_KEYS.has(key)) { - // eslint-disable-next-line no-console - console.warn(`Skipping forbidden key from OpenBao response: ${key}`); return; } - if (!Object.prototype.hasOwnProperty.call(secrets, key)) { - return; - } - - const value = secrets[key]; - - // process.env values must be strings Object.defineProperty(process.env, key, { - value: String(value), + value: String(secrets[key]), enumerable: true, configurable: true, writable: true From 6a8d21ba4077eac69fbc4585b5f75be851ca040c Mon Sep 17 00:00:00 2001 From: sujitaw Date: Wed, 17 Jun 2026 16:54:13 +0530 Subject: [PATCH 06/58] feat: fetch token using role and secret Signed-off-by: sujitaw --- .env.demo | 4 ++++ apps/api-gateway/src/main.ts | 34 +++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.env.demo b/.env.demo index f5de4cd3c..66b606b06 100644 --- a/.env.demo +++ b/.env.demo @@ -368,3 +368,7 @@ ADMIN_KEYCLOAK_SECRET= # Add in case the keycloak id of the old platform client is changed and want to update all the users in database with the new admin client PLATFORM_ADMIN_OLD_CLIENT_ID= +BAO_URL= +BAO_SECRET_PATH= +BAO_ROLE_ID= +BAO_SECRET_ID= \ No newline at end of file diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 210b64e78..f3f5f7acf 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -22,15 +22,42 @@ dotenv.config(); async function bootstrap(): Promise { const baoUrl = process.env.BAO_URL; - const baoToken = process.env.BAO_TOKEN; + let baoToken = ''; const secretPath = process.env.BAO_SECRET_PATH; + const roleId = process.env.BAO_ROLE_ID; + const secretId = process.env.BAO_SECRET_ID; + + if (!roleId || !secretId) { + // eslint-disable-next-line no-console + console.error('❌ Critical Error: BAO_ROLE_ID and BAO_SECRET_ID must be set in the environment.'); + process.exit(1); + } + + try { + // 2. Simple POST request to exchange RoleID/SecretID for an access token + const authResponse = await fetch(`${baoUrl}/v1/auth/approle/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ role_id: roleId, secret_id: secretId }) + }); + + if (!authResponse.ok) { + throw new Error(`Authentication failed with status ${authResponse.status}`); + } + + const authData: { auth: { client_token: string } } = await authResponse.json(); + baoToken = authData.auth.client_token; + } catch (error) { + // eslint-disable-next-line no-console + console.error('❌ OpenBao Lifecycle Boot Failure:', error.message); + process.exit(1); // Force shutdown if secrets cannot be securely mounted + } if (!baoToken) { throw new Error('Missing BAO_TOKEN environment variable'); } // Fetch secrets from OpenBao before initializing the NestJS ConfigModule - // console.log('🔐 Fetching secrets from OpenBao...',baoUrl, baoToken, secretPath); try { const response = await fetch(`${baoUrl}/v1/${secretPath}`, { method: 'GET', @@ -39,7 +66,6 @@ async function bootstrap(): Promise { 'Content-Type': 'application/json' } }); - // console.log('Received response from OpenBao with status:', response); if (!response.ok) { throw new Error(`OpenBao responded with status: ${response.status}`); } @@ -66,8 +92,6 @@ async function bootstrap(): Promise { writable: true }); }); - - // console.log('✅ Environment variables successfully loaded from OpenBao',secrets); } catch (error) { // eslint-disable-next-line no-console console.error('❌ Failed to load secrets from OpenBao:', error.message); From be08e7c801c9c3974ed1b6ca596be214d20f1090 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Fri, 19 Jun 2026 15:48:29 +0530 Subject: [PATCH 07/58] wip Signed-off-by: sujitaw --- apps/agent-provisioning/src/main.ts | 6 +++ apps/agent-service/src/main.ts | 5 ++ apps/api-gateway/src/main.ts | 78 +-------------------------- apps/cloud-wallet/src/main.ts | 5 ++ apps/connection/src/main.ts | 5 ++ apps/ecosystem/src/main.ts | 5 ++ apps/geo-location/src/main.ts | 5 ++ apps/issuance/src/main.ts | 5 ++ apps/ledger/src/main.ts | 5 ++ apps/notification/src/main.ts | 5 ++ apps/oid4vc-issuance/src/main.ts | 6 +++ apps/oid4vc-verification/src/main.ts | 5 ++ apps/organization/src/main.ts | 5 ++ apps/user/src/main.ts | 5 ++ apps/utility/src/main.ts | 5 ++ apps/verification/src/main.ts | 5 ++ apps/webhook/src/main.ts | 5 ++ apps/x509/src/main.ts | 5 ++ libs/aws/src/aws.service.ts | 1 + libs/common/src/resend-helper-file.ts | 19 ++++--- libs/config/src/index.ts | 3 +- 21 files changed, 103 insertions(+), 85 deletions(-) diff --git a/apps/agent-provisioning/src/main.ts b/apps/agent-provisioning/src/main.ts index 6b3e2963c..fd7f31d86 100644 --- a/apps/agent-provisioning/src/main.ts +++ b/apps/agent-provisioning/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; @@ -6,9 +7,14 @@ import { AgentProvisioningModule } from './agent-provisioning.module'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); + const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(AgentProvisioningModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/agent-service/src/main.ts b/apps/agent-service/src/main.ts index c949023a5..7bf1b795a 100644 --- a/apps/agent-service/src/main.ts +++ b/apps/agent-service/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @@ -9,10 +10,14 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import { Ledgers } from '@credebl/enum/enum'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(AgentServiceModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index f3f5f7acf..b8bcddbf6 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -17,86 +17,12 @@ import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapt import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-validation-pipe'; import * as useragent from 'express-useragent'; import { EcosystemSwaggerFilter } from './authz/guards/ecosystem-swagger.filter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; dotenv.config(); async function bootstrap(): Promise { - const baoUrl = process.env.BAO_URL; - let baoToken = ''; - const secretPath = process.env.BAO_SECRET_PATH; - const roleId = process.env.BAO_ROLE_ID; - const secretId = process.env.BAO_SECRET_ID; - - if (!roleId || !secretId) { - // eslint-disable-next-line no-console - console.error('❌ Critical Error: BAO_ROLE_ID and BAO_SECRET_ID must be set in the environment.'); - process.exit(1); - } - - try { - // 2. Simple POST request to exchange RoleID/SecretID for an access token - const authResponse = await fetch(`${baoUrl}/v1/auth/approle/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ role_id: roleId, secret_id: secretId }) - }); - - if (!authResponse.ok) { - throw new Error(`Authentication failed with status ${authResponse.status}`); - } - - const authData: { auth: { client_token: string } } = await authResponse.json(); - baoToken = authData.auth.client_token; - } catch (error) { - // eslint-disable-next-line no-console - console.error('❌ OpenBao Lifecycle Boot Failure:', error.message); - process.exit(1); // Force shutdown if secrets cannot be securely mounted - } - - if (!baoToken) { - throw new Error('Missing BAO_TOKEN environment variable'); - } - - // Fetch secrets from OpenBao before initializing the NestJS ConfigModule - try { - const response = await fetch(`${baoUrl}/v1/${secretPath}`, { - method: 'GET', - headers: { - 'X-Vault-Token': baoToken, - 'Content-Type': 'application/json' - } - }); - if (!response.ok) { - throw new Error(`OpenBao responded with status: ${response.status}`); - } - - const result = await response.json(); - - // OpenBao structures KV v2 data inside data.data - const secrets = result.data.data; - const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); - - if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { - throw new Error('Unexpected secrets payload received from OpenBao'); - } - - Object.keys(secrets).forEach((key) => { - if (FORBIDDEN_KEYS.has(key)) { - return; - } - - Object.defineProperty(process.env, key, { - value: String(secrets[key]), - enumerable: true, - configurable: true, - writable: true - }); - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error('❌ Failed to load secrets from OpenBao:', error.message); - process.exit(1); // Stop the app if secrets can't be loaded - } + await loadBaoSecrets(); try { if (otelSDK) { diff --git a/apps/cloud-wallet/src/main.ts b/apps/cloud-wallet/src/main.ts index a411b0700..d0d4b9811 100644 --- a/apps/cloud-wallet/src/main.ts +++ b/apps/cloud-wallet/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { CloudWalletModule } from './cloud-wallet.module'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; @@ -6,10 +7,14 @@ import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(CloudWalletModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/connection/src/main.ts b/apps/connection/src/main.ts index c98e3ea74..81dac6a5f 100644 --- a/apps/connection/src/main.ts +++ b/apps/connection/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { ConnectionModule } from './connection.module'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; @@ -6,10 +7,14 @@ import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(ConnectionModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/ecosystem/src/main.ts b/apps/ecosystem/src/main.ts index ebb2a53df..16fd8fc8b 100644 --- a/apps/ecosystem/src/main.ts +++ b/apps/ecosystem/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -8,10 +9,14 @@ import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; // import { nkeyAuthenticator } from 'nats'; import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(EcosystemModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/geo-location/src/main.ts b/apps/geo-location/src/main.ts index 84800754f..cb4b94602 100644 --- a/apps/geo-location/src/main.ts +++ b/apps/geo-location/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @@ -6,10 +7,14 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { GeoLocationModule } from './geo-location.module'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(GeoLocationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/issuance/src/main.ts b/apps/issuance/src/main.ts index b69e8e57c..d89fb2437 100644 --- a/apps/issuance/src/main.ts +++ b/apps/issuance/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; @@ -6,10 +7,14 @@ import { IssuanceModule } from '../src/issuance.module'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(IssuanceModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/ledger/src/main.ts b/apps/ledger/src/main.ts index 022dffa76..6c2ddbb5b 100644 --- a/apps/ledger/src/main.ts +++ b/apps/ledger/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { LedgerModule } from './ledger.module'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; @@ -6,8 +7,12 @@ import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(LedgerModule, { transport: Transport.NATS, options: getNatsOptions(CommonConstants.LEDGER_SERVICE, process.env.LEDGER_NKEY_SEED, process.env.NATS_CREDS_FILE) diff --git a/apps/notification/src/main.ts b/apps/notification/src/main.ts index 64d505ed7..4112bc44a 100644 --- a/apps/notification/src/main.ts +++ b/apps/notification/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; @@ -6,10 +7,14 @@ import { NotificationModule } from '../src/notification.module'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(NotificationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts index 68aa19bbf..bfe17a72f 100644 --- a/apps/oid4vc-issuance/src/main.ts +++ b/apps/oid4vc-issuance/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; @@ -6,10 +7,15 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { Oid4vcIssuanceModule } from './oid4vc-issuance.module'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); + if (!process.env.STATUS_LIST_HOST) { logger.warn('STATUS_LIST_HOST is not configured. Revocable SD-JWT flow will be disabled.'); } diff --git a/apps/oid4vc-verification/src/main.ts b/apps/oid4vc-verification/src/main.ts index 667a31102..e86a8ee32 100644 --- a/apps/oid4vc-verification/src/main.ts +++ b/apps/oid4vc-verification/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { Logger } from '@nestjs/common'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; @@ -5,10 +6,14 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { Oid4vpModule } from './oid4vc-verification.module'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(Oid4vpModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/organization/src/main.ts b/apps/organization/src/main.ts index 8ec4de9d2..f8dc27f48 100644 --- a/apps/organization/src/main.ts +++ b/apps/organization/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @@ -7,10 +8,14 @@ import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(OrganizationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/user/src/main.ts b/apps/user/src/main.ts index 01411c446..cf52215ad 100644 --- a/apps/user/src/main.ts +++ b/apps/user/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -7,10 +8,14 @@ import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { UserModule } from './user.module'; import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(UserModule, { transport: Transport.NATS, options: getNatsOptions(CommonConstants.USER_SERVICE, process.env.USER_NKEY_SEED, process.env.NATS_CREDS_FILE) diff --git a/apps/utility/src/main.ts b/apps/utility/src/main.ts index fcf71b53a..e0173b4ee 100644 --- a/apps/utility/src/main.ts +++ b/apps/utility/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; @@ -6,10 +7,14 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { UtilitiesModule } from './utilities.module'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(UtilitiesModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/verification/src/main.ts b/apps/verification/src/main.ts index 0195a8ee1..046d9a0e0 100644 --- a/apps/verification/src/main.ts +++ b/apps/verification/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; @@ -6,10 +7,14 @@ import { VerificationModule } from './verification.module'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(VerificationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/webhook/src/main.ts b/apps/webhook/src/main.ts index 07ca3bcea..25b3380ce 100644 --- a/apps/webhook/src/main.ts +++ b/apps/webhook/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; @@ -6,10 +7,14 @@ import { WebhookModule } from '../src/webhook.module'; import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(WebhookModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/x509/src/main.ts b/apps/x509/src/main.ts index eb77bb520..208e1d3aa 100644 --- a/apps/x509/src/main.ts +++ b/apps/x509/src/main.ts @@ -1,3 +1,4 @@ +import * as dotenv from 'dotenv'; import { NestFactory } from '@nestjs/core'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; @@ -6,8 +7,12 @@ import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { X509Module } from './x509.module'; +import { loadBaoSecrets } from '@credebl/config/bao-secrets'; + +dotenv.config(); async function bootstrap(): Promise { + await loadBaoSecrets(); const app = await NestFactory.createMicroservice(X509Module, { transport: Transport.NATS, options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED, process.env.NATS_CREDS_FILE) diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 19286edea..1f402a7f4 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -1,4 +1,5 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; + import { RpcException } from '@nestjs/microservices'; import { S3 } from 'aws-sdk'; import { promisify } from 'util'; diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index 4d0326bd8..1c1170724 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -1,26 +1,29 @@ -import * as dotenv from 'dotenv'; - import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; import { Resend } from 'resend'; -dotenv.config(); +let resend: Resend | null = null; -const emailProvider = process.env.EMAIL_PROVIDER; -const apiKey = process.env.RESEND_API_KEY; +function getResendClient(): Resend { + if (resend) { + return resend; + } -let resend: Resend | null = null; + const apiKey = process.env.RESEND_API_KEY; -if ('resend' === emailProvider) { if (!apiKey) { throw new Error('Missing RESEND_API_KEY in environment variables.'); } + resend = new Resend(apiKey); + return resend; } export const sendWithResend = async (emailDto: EmailDto): Promise => { try { - const response = await resend.emails.send({ + const client = getResendClient(); + + const response = await client.emails.send({ from: emailDto.emailFrom, to: emailDto.emailTo, subject: emailDto.emailSubject, diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts index 8d518dbf6..0ec7a716f 100644 --- a/libs/config/src/index.ts +++ b/libs/config/src/index.ts @@ -1,3 +1,4 @@ export * from './config.service'; export * from './config.module'; -export * from './global-config.module'; \ No newline at end of file +export * from './global-config.module'; +export { loadBaoSecrets } from './bao-secrets'; From c54e8e2d3e6d5f84641f29ecfd0b0822326d4c80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:26:21 +0000 Subject: [PATCH 08/58] chore(deps): bump nodemailer from 7.0.13 to 9.0.1 Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 7.0.13 to 9.0.1. - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v9.0.1) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 9.0.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c6bdb9daf..45b843abe 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "nestjs-typeorm-paginate": "catalog:", "node-html-to-image": "^4.0.0", "node-qpdf2": "^2.0.0", - "nodemailer": "^7.0.13", + "nodemailer": "^9.0.1", "p-limit": "^3.1.0", "papaparse": "^5.5.3", "passport": "^0.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86fed180d..688a3c71b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,8 +303,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 nodemailer: - specifier: ^7.0.13 - version: 7.0.13 + specifier: ^9.0.1 + version: 9.0.1 p-limit: specifier: ^3.1.0 version: 3.1.0 @@ -4921,6 +4921,7 @@ packages: nats@2.29.3: resolution: {integrity: sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==} engines: {node: '>= 14.0.0'} + deprecated: Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5022,8 +5023,8 @@ packages: node-releases@2.0.38: resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} - nodemailer@7.0.13: - resolution: {integrity: sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==} + nodemailer@9.0.1: + resolution: {integrity: sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==} engines: {node: '>=6.0.0'} normalize-path@3.0.0: @@ -12365,7 +12366,7 @@ snapshots: node-releases@2.0.38: {} - nodemailer@7.0.13: {} + nodemailer@9.0.1: {} normalize-path@3.0.0: {} From 0ae3dc4d6d5dcb1b4179c1e27bf4e954e7c37122 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 23 Jun 2026 17:37:04 +0530 Subject: [PATCH 09/58] wip Signed-off-by: sujitaw --- apps/agent-provisioning/src/main.ts | 14 ++++++++------ apps/agent-service/src/main.ts | 18 ++++++++++-------- apps/api-gateway/src/main.ts | 4 ++-- apps/cloud-wallet/src/main.ts | 14 ++++++++------ apps/connection/src/main.ts | 14 ++++++++------ apps/ecosystem/src/main.ts | 5 +++-- apps/geo-location/src/main.ts | 14 ++++++++------ apps/issuance/src/main.ts | 16 +++++++++------- apps/ledger/src/main.ts | 16 +++++++++------- apps/notification/src/main.ts | 14 ++++++++------ apps/oid4vc-issuance/src/main.ts | 14 ++++++++------ apps/oid4vc-verification/src/main.ts | 12 +++++++----- apps/organization/src/main.ts | 12 +++++++----- apps/user/src/main.ts | 5 +++-- apps/utility/src/main.ts | 14 ++++++++------ apps/verification/src/main.ts | 14 ++++++++------ apps/webhook/src/main.ts | 14 ++++++++------ apps/x509/src/main.ts | 14 ++++++++------ libs/config/src/index.ts | 2 +- 19 files changed, 131 insertions(+), 99 deletions(-) diff --git a/apps/agent-provisioning/src/main.ts b/apps/agent-provisioning/src/main.ts index fd7f31d86..f02b18dd2 100644 --- a/apps/agent-provisioning/src/main.ts +++ b/apps/agent-provisioning/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + import { AgentProvisioningModule } from './agent-provisioning.module'; -import { getNatsOptions } from '@credebl/common/nats.config'; import { CommonConstants } from '@credebl/common/common.constant'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(AgentProvisioningModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/agent-service/src/main.ts b/apps/agent-service/src/main.ts index 7bf1b795a..f992d5b96 100644 --- a/apps/agent-service/src/main.ts +++ b/apps/agent-service/src/main.ts @@ -1,23 +1,25 @@ import * as dotenv from 'dotenv'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; -import { NestFactory } from '@nestjs/core'; -import { AgentServiceModule } from './agent-service.module'; -import { AgentServiceService } from './agent-service.service'; + import { IAgentSpinupDto, IUserRequestInterface } from './interface/agent-service.interface'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; + +import { AgentServiceModule } from './agent-service.module'; +import { AgentServiceService } from './agent-service.service'; import { CommonConstants } from '@credebl/common/common.constant'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Ledgers } from '@credebl/enum/enum'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(AgentServiceModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index b8bcddbf6..c6e4da5cf 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -17,12 +17,12 @@ import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapt import { UpdatableValidationPipe } from '@credebl/common/custom-overrideable-validation-pipe'; import * as useragent from 'express-useragent'; import { EcosystemSwaggerFilter } from './authz/guards/ecosystem-swagger.filter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); try { if (otelSDK) { diff --git a/apps/cloud-wallet/src/main.ts b/apps/cloud-wallet/src/main.ts index d0d4b9811..1da37b7ec 100644 --- a/apps/cloud-wallet/src/main.ts +++ b/apps/cloud-wallet/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + import { CloudWalletModule } from './cloud-wallet.module'; +import { CommonConstants } from '@credebl/common/common.constant'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(CloudWalletModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/connection/src/main.ts b/apps/connection/src/main.ts index 81dac6a5f..10ee34ff8 100644 --- a/apps/connection/src/main.ts +++ b/apps/connection/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +import { CommonConstants } from '@credebl/common/common.constant'; import { ConnectionModule } from './connection.module'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(ConnectionModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/ecosystem/src/main.ts b/apps/ecosystem/src/main.ts index 16fd8fc8b..71fed8e97 100644 --- a/apps/ecosystem/src/main.ts +++ b/apps/ecosystem/src/main.ts @@ -1,4 +1,5 @@ import * as dotenv from 'dotenv'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -9,14 +10,14 @@ import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; // import { nkeyAuthenticator } from 'nats'; import { getNatsOptions } from '@credebl/common/nats.config'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(EcosystemModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/geo-location/src/main.ts b/apps/geo-location/src/main.ts index cb4b94602..9e16df13e 100644 --- a/apps/geo-location/src/main.ts +++ b/apps/geo-location/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +import { CommonConstants } from '@credebl/common/common.constant'; +import { GeoLocationModule } from './geo-location.module'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { GeoLocationModule } from './geo-location.module'; -import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(GeoLocationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/issuance/src/main.ts b/apps/issuance/src/main.ts index d89fb2437..9d137b58d 100644 --- a/apps/issuance/src/main.ts +++ b/apps/issuance/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { IssuanceModule } from '../src/issuance.module'; -import { getNatsOptions } from '@credebl/common/nats.config'; + import { CommonConstants } from '@credebl/common/common.constant'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { IssuanceModule } from '../src/issuance.module'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(IssuanceModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/ledger/src/main.ts b/apps/ledger/src/main.ts index 6c2ddbb5b..b25a6a6b0 100644 --- a/apps/ledger/src/main.ts +++ b/apps/ledger/src/main.ts @@ -1,18 +1,20 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; -import { LedgerModule } from './ledger.module'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; + import { CommonConstants } from '@credebl/common/common.constant'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { LedgerModule } from './ledger.module'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(LedgerModule, { transport: Transport.NATS, options: getNatsOptions(CommonConstants.LEDGER_SERVICE, process.env.LEDGER_NKEY_SEED, process.env.NATS_CREDS_FILE) diff --git a/apps/notification/src/main.ts b/apps/notification/src/main.ts index 4112bc44a..892524c40 100644 --- a/apps/notification/src/main.ts +++ b/apps/notification/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +import { CommonConstants } from '@credebl/common/common.constant'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { NestFactory } from '@nestjs/core'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { NotificationModule } from '../src/notification.module'; import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(NotificationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts index bfe17a72f..4e9702a07 100644 --- a/apps/oid4vc-issuance/src/main.ts +++ b/apps/oid4vc-issuance/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; + import { CommonConstants } from '@credebl/common/common.constant'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { Oid4vcIssuanceModule } from './oid4vc-issuance.module'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); if (!process.env.STATUS_LIST_HOST) { logger.warn('STATUS_LIST_HOST is not configured. Revocable SD-JWT flow will be disabled.'); diff --git a/apps/oid4vc-verification/src/main.ts b/apps/oid4vc-verification/src/main.ts index e86a8ee32..2303643e6 100644 --- a/apps/oid4vc-verification/src/main.ts +++ b/apps/oid4vc-verification/src/main.ts @@ -1,19 +1,21 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; -import { Logger } from '@nestjs/common'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; + import { CommonConstants } from '@credebl/common/common.constant'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { Oid4vpModule } from './oid4vc-verification.module'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(Oid4vpModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/organization/src/main.ts b/apps/organization/src/main.ts index f8dc27f48..625c4f59c 100644 --- a/apps/organization/src/main.ts +++ b/apps/organization/src/main.ts @@ -1,21 +1,23 @@ import * as dotenv from 'dotenv'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +import { CommonConstants } from '@credebl/common/common.constant'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { OrganizationModule } from './organization.module'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; // import { nkeyAuthenticator } from 'nats'; import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(OrganizationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/user/src/main.ts b/apps/user/src/main.ts index cf52215ad..cd3d9ad77 100644 --- a/apps/user/src/main.ts +++ b/apps/user/src/main.ts @@ -1,4 +1,5 @@ import * as dotenv from 'dotenv'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -8,14 +9,14 @@ import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { UserModule } from './user.module'; import { getNatsOptions } from '@credebl/common/nats.config'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(UserModule, { transport: Transport.NATS, options: getNatsOptions(CommonConstants.USER_SERVICE, process.env.USER_NKEY_SEED, process.env.NATS_CREDS_FILE) diff --git a/apps/utility/src/main.ts b/apps/utility/src/main.ts index e0173b4ee..07d9bfa10 100644 --- a/apps/utility/src/main.ts +++ b/apps/utility/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +import { CommonConstants } from '@credebl/common/common.constant'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; -import { UtilitiesModule } from './utilities.module'; -import { CommonConstants } from '@credebl/common/common.constant'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { UtilitiesModule } from './utilities.module'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(UtilitiesModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/verification/src/main.ts b/apps/verification/src/main.ts index 046d9a0e0..5fe2fd73d 100644 --- a/apps/verification/src/main.ts +++ b/apps/verification/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +import { CommonConstants } from '@credebl/common/common.constant'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { NestFactory } from '@nestjs/core'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { VerificationModule } from './verification.module'; import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(VerificationModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/webhook/src/main.ts b/apps/webhook/src/main.ts index 25b3380ce..a7cc3bb61 100644 --- a/apps/webhook/src/main.ts +++ b/apps/webhook/src/main.ts @@ -1,20 +1,22 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; + +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; + +import { CommonConstants } from '@credebl/common/common.constant'; import { HttpExceptionFilter } from 'libs/http-exception.filter'; import { Logger } from '@nestjs/common'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { NestFactory } from '@nestjs/core'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { WebhookModule } from '../src/webhook.module'; import { getNatsOptions } from '@credebl/common/nats.config'; -import { CommonConstants } from '@credebl/common/common.constant'; -import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); const logger = new Logger(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(WebhookModule, { transport: Transport.NATS, options: getNatsOptions( diff --git a/apps/x509/src/main.ts b/apps/x509/src/main.ts index 208e1d3aa..b181e45fd 100644 --- a/apps/x509/src/main.ts +++ b/apps/x509/src/main.ts @@ -1,18 +1,20 @@ import * as dotenv from 'dotenv'; -import { NestFactory } from '@nestjs/core'; -import { HttpExceptionFilter } from 'libs/http-exception.filter'; -import { Logger } from '@nestjs/common'; + import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { getNatsOptions } from '@credebl/common/nats.config'; + import { CommonConstants } from '@credebl/common/common.constant'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { NestFactory } from '@nestjs/core'; import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; import { X509Module } from './x509.module'; -import { loadBaoSecrets } from '@credebl/config/bao-secrets'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { loadConfigSecrets } from '@credebl/config/secret-storage/secrets-loader'; dotenv.config(); async function bootstrap(): Promise { - await loadBaoSecrets(); + await loadConfigSecrets(); const app = await NestFactory.createMicroservice(X509Module, { transport: Transport.NATS, options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED, process.env.NATS_CREDS_FILE) diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts index 0ec7a716f..c2451a178 100644 --- a/libs/config/src/index.ts +++ b/libs/config/src/index.ts @@ -1,4 +1,4 @@ export * from './config.service'; export * from './config.module'; export * from './global-config.module'; -export { loadBaoSecrets } from './bao-secrets'; +export { loadBaoSecrets } from './secret-storage/bao-secrets'; From 3f193b3bd2ae340176f9426ae68d884b490571fe Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 23 Jun 2026 17:40:51 +0530 Subject: [PATCH 10/58] wip Signed-off-by: sujitaw --- .../src/secret-storage/openbao.provider.ts | 61 +++++++++++++++++ .../secret-provider.interface.ts | 13 ++++ .../src/secret-storage/secrets-loader.ts | 65 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 libs/config/src/secret-storage/openbao.provider.ts create mode 100644 libs/config/src/secret-storage/secret-provider.interface.ts create mode 100644 libs/config/src/secret-storage/secrets-loader.ts diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts new file mode 100644 index 000000000..7ec73b072 --- /dev/null +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -0,0 +1,61 @@ +import { SecretProvider } from './secret-provider.interface'; + +export class OpenBaoProvider implements SecretProvider { + readonly name = 'OpenBao'; + + isEnabled(): boolean { + return 'true' === process.env.ENABLE_BAO?.trim()?.toLowerCase(); + } + + async loadSecrets(): Promise> { + const baoUrl = process.env.BAO_URL; + const secretPath = process.env.BAO_SECRET_PATH; + const roleId = process.env.BAO_ROLE_ID; + const secretId = process.env.BAO_SECRET_ID; + + if (!roleId || !secretId) { + throw new Error('BAO_ROLE_ID and BAO_SECRET_ID must be set.'); + } + + // --- Authentication --- + const authResponse = await fetch(`${baoUrl}/v1/auth/approle/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + // eslint-disable-next-line camelcase + body: JSON.stringify({ role_id: roleId, secret_id: secretId }) + }); + + if (!authResponse.ok) { + throw new Error(`Authentication failed: Status ${authResponse.status}`); + } + + const authData = await authResponse.json(); + const baoToken = authData.auth?.client_token; + + if (!baoToken) { + throw new Error('Failed to retrieve client token from OpenBao.'); + } + + // --- Fetch Secrets --- + const response = await fetch(`${baoUrl}/v1/${secretPath}`, { + method: 'GET', + headers: { + 'X-Vault-Token': baoToken, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`Fetch failed: Status ${response.status}`); + } + + const result = await response.json(); + const secrets = result.data?.data; + // console.log('🔐 Successfully fetched secrets from OpenBao', result); + if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { + throw new Error('Unexpected secrets payload structure.'); + } + + return secrets as Record; + } +} diff --git a/libs/config/src/secret-storage/secret-provider.interface.ts b/libs/config/src/secret-storage/secret-provider.interface.ts new file mode 100644 index 000000000..9d734319e --- /dev/null +++ b/libs/config/src/secret-storage/secret-provider.interface.ts @@ -0,0 +1,13 @@ +export interface SecretProvider { + /** + * A human-readable name used for logging purposes (e.g., 'AWS Secrets Manager') + */ + name: string; + + /** + * Fetches the secrets from the external service and returns them + * as a flat key-value object of strings. + * * @returns A promise resolving to Record + */ + loadSecrets(): Promise>; +} diff --git a/libs/config/src/secret-storage/secrets-loader.ts b/libs/config/src/secret-storage/secrets-loader.ts new file mode 100644 index 000000000..251a37f4d --- /dev/null +++ b/libs/config/src/secret-storage/secrets-loader.ts @@ -0,0 +1,65 @@ +// import { AwsSecretsProvider } from './providers/aws-secrets.provider'; +// libs/config/src/secrets-loader.ts +import { Logger } from '@nestjs/common'; +import { OpenBaoProvider } from './openbao.provider'; +import { SecretProvider } from './secret-provider.interface'; + +const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']); +const logger = new Logger('SecretsLoader'); + +/** + * Factory function to get the requested secret provider + */ +function getSecretProvider(providerType: string): SecretProvider | null { + switch (providerType.toLowerCase()) { + case 'openbao': + return new OpenBaoProvider(); + + default: + return null; + } +} + +export async function loadConfigSecrets(): Promise { + const providerType = process.env.SECRETS_PROVIDER?.trim(); + + if (!providerType) { + logger.log('SECRETS_PROVIDER not set. Skipping remote secrets fetching.'); + return; + } + + const provider = getSecretProvider(providerType); + + if (!provider) { + logger.warn( + `⚠️ Unsupported SECRETS_PROVIDER value "${providerType}". Falling back to local environment variables.` + ); + return; + } + + logger.log(`Secrets management enabled via [${provider.name}]. Fetching...`); + + try { + const secrets = await provider.loadSecrets(); + + for (const [key, value] of Object.entries(secrets)) { + if (FORBIDDEN_KEYS.has(key)) { + continue; + } + + Object.defineProperty(process.env, key, { + value: String(value), + enumerable: true, + configurable: true, + writable: true + }); + } + + logger.log(`Successfully injected secrets from ${provider.name} into process.env`); + } catch (error) { + // Safely handles instances where error might not be a standard Error object + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Critical Lifecycle Boot Failure (${provider.name}): ${errorMessage}`); + throw new Error(`Failed to load secrets from ${provider.name}`); + } +} From 8e5a3a14bd0870c867fb735f47ab77491fa04b4c Mon Sep 17 00:00:00 2001 From: sujitaw Date: Thu, 25 Jun 2026 17:31:15 +0530 Subject: [PATCH 11/58] wip Signed-off-by: sujitaw --- libs/common/src/common.constant.ts | 4 ++++ libs/common/src/resend-helper-file.ts | 13 ++++++++++--- libs/config/src/index.ts | 2 +- libs/config/src/secret-storage/openbao.provider.ts | 10 +++++----- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 99d23bb4d..44fed668e 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -443,6 +443,10 @@ export enum CommonConstants { OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = 'get-oid4vp-verifier-session-response-id', OIDC_VERIFIER_SESSION_AUTH_RESPONSE_VERIFY = 'verify-oid4vp-verifier-session-auth-response', + // BAO_STORAGE_PATH + CREDEBL_RESEND_API_KEY_PATH = 'credebl_resend_api_key', + + //X509 X509_CREATE_CERTIFICATE = 'create-x509-certificate', X509_IMPORT_CERTIFICATE = 'import-x509-certificate', diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index 1c1170724..b83802f02 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -1,15 +1,22 @@ +import { CommonConstants } from './common.constant'; import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; +import { OpenBaoProvider } from 'libs/config/src/secret-storage/openbao.provider'; import { Resend } from 'resend'; let resend: Resend | null = null; -function getResendClient(): Resend { +async function getResendClient(): Promise { if (resend) { return resend; } + console.log("get resend client called"); + const openBaoProvider = new OpenBaoProvider(); + const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH || ''; - const apiKey = process.env.RESEND_API_KEY; + const secrets = await openBaoProvider.loadSecrets(secretPath); + console.log('🔐 Successfully fetched secrets from OpenBao for Resend API Key', secrets); + const apiKey = secrets.RESEND_API_KEY; if (!apiKey) { throw new Error('Missing RESEND_API_KEY in environment variables.'); @@ -21,7 +28,7 @@ function getResendClient(): Resend { export const sendWithResend = async (emailDto: EmailDto): Promise => { try { - const client = getResendClient(); + const client = await getResendClient(); const response = await client.emails.send({ from: emailDto.emailFrom, diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts index c2451a178..e5e70bc39 100644 --- a/libs/config/src/index.ts +++ b/libs/config/src/index.ts @@ -1,4 +1,4 @@ export * from './config.service'; export * from './config.module'; export * from './global-config.module'; -export { loadBaoSecrets } from './secret-storage/bao-secrets'; +export { loadConfigSecrets } from './secret-storage/secrets-loader'; diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts index 7ec73b072..2b0c63f7a 100644 --- a/libs/config/src/secret-storage/openbao.provider.ts +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -7,12 +7,12 @@ export class OpenBaoProvider implements SecretProvider { return 'true' === process.env.ENABLE_BAO?.trim()?.toLowerCase(); } - async loadSecrets(): Promise> { + async loadSecrets(customPath:string = ''): Promise> { const baoUrl = process.env.BAO_URL; - const secretPath = process.env.BAO_SECRET_PATH; + const secretPath = customPath || process.env.BAO_SECRET_PATH; const roleId = process.env.BAO_ROLE_ID; const secretId = process.env.BAO_SECRET_ID; - + console.log(`🔐 OpenBaoProvider: Fetching secrets from ${baoUrl}/v1/${secretPath} with roleId=${roleId}`); if (!roleId || !secretId) { throw new Error('BAO_ROLE_ID and BAO_SECRET_ID must be set.'); } @@ -24,7 +24,7 @@ export class OpenBaoProvider implements SecretProvider { // eslint-disable-next-line camelcase body: JSON.stringify({ role_id: roleId, secret_id: secretId }) }); - + console.log(`🔐 OpenBaoProvider: Authentication response status: ${JSON.stringify(authResponse)}`); if (!authResponse.ok) { throw new Error(`Authentication failed: Status ${authResponse.status}`); } @@ -44,7 +44,7 @@ export class OpenBaoProvider implements SecretProvider { 'Content-Type': 'application/json' } }); - + console.log(`🔐 OpenBaoProvider: Fetch secrets response status: ${JSON.stringify(response)}`); if (!response.ok) { throw new Error(`Fetch failed: Status ${response.status}`); } From ff0733a875d477401bdaeff6d6303fcbcee48a6e Mon Sep 17 00:00:00 2001 From: sujitaw Date: Fri, 26 Jun 2026 13:13:08 +0530 Subject: [PATCH 12/58] wip Signed-off-by: sujitaw --- libs/common/src/common.constant.ts | 2 +- libs/common/src/resend-helper-file.ts | 4 ++-- .../src/secret-storage/openbao.provider.ts | 17 ++++++++++------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 44fed668e..b00706aec 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -444,7 +444,7 @@ export enum CommonConstants { OIDC_VERIFIER_SESSION_AUTH_RESPONSE_VERIFY = 'verify-oid4vp-verifier-session-auth-response', // BAO_STORAGE_PATH - CREDEBL_RESEND_API_KEY_PATH = 'credebl_resend_api_key', + CREDEBL_RESEND_API_KEY_PATH = 'secret/data/credebl_resend_api_key', //X509 diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index b83802f02..499d24869 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -29,7 +29,7 @@ async function getResendClient(): Promise { export const sendWithResend = async (emailDto: EmailDto): Promise => { try { const client = await getResendClient(); - + console.log('Sending email with Resend:', emailDto); const response = await client.emails.send({ from: emailDto.emailFrom, to: emailDto.emailTo, @@ -38,7 +38,7 @@ export const sendWithResend = async (emailDto: EmailDto): Promise => { html: emailDto.emailHtml, attachments: emailDto.emailAttachments }); - + console.log('Resend email response:', response); return Boolean(response.data?.id); } catch (error) { Logger.error('Error while sending email with Resend', error); diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts index 2b0c63f7a..e1ed90fc8 100644 --- a/libs/config/src/secret-storage/openbao.provider.ts +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -16,26 +16,28 @@ export class OpenBaoProvider implements SecretProvider { if (!roleId || !secretId) { throw new Error('BAO_ROLE_ID and BAO_SECRET_ID must be set.'); } - + console.log(`🔐 OpenBaoProvider: Using roleId=${roleId} and secretId=${secretId}`); + console.log(`${baoUrl}/v1/auth/approle/login`) // --- Authentication --- const authResponse = await fetch(`${baoUrl}/v1/auth/approle/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, // eslint-disable-next-line camelcase body: JSON.stringify({ role_id: roleId, secret_id: secretId }) - }); - console.log(`🔐 OpenBaoProvider: Authentication response status: ${JSON.stringify(authResponse)}`); + }) + console.log(`🔐 OpenBaoProvider: Authentication response status: ${authResponse}`); if (!authResponse.ok) { throw new Error(`Authentication failed: Status ${authResponse.status}`); } - + const authData = await authResponse.json(); + console.log("authresponse", authData); const baoToken = authData.auth?.client_token; if (!baoToken) { throw new Error('Failed to retrieve client token from OpenBao.'); } - + console.log("request url",`${baoUrl}/v1/${secretPath}`) // --- Fetch Secrets --- const response = await fetch(`${baoUrl}/v1/${secretPath}`, { method: 'GET', @@ -44,14 +46,15 @@ export class OpenBaoProvider implements SecretProvider { 'Content-Type': 'application/json' } }); - console.log(`🔐 OpenBaoProvider: Fetch secrets response status: ${JSON.stringify(response)}`); + console.log(`🔐 OpenBaoProvider: Fetch secrets response status: ${response}`); if (!response.ok) { + console.log("inside not ok") throw new Error(`Fetch failed: Status ${response.status}`); } const result = await response.json(); const secrets = result.data?.data; - // console.log('🔐 Successfully fetched secrets from OpenBao', result); + console.log('🔐 Successfully fetched secrets from OpenBao', result); if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { throw new Error('Unexpected secrets payload structure.'); } From 6d63f367f664aed291c2959f5961fb33c8fa0ec6 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Mon, 29 Jun 2026 14:02:06 +0530 Subject: [PATCH 13/58] feat: completed implementation on aws Signed-off-by: sujitaw --- libs/aws/src/aws.service.ts | 76 +++++++++++-------- libs/common/src/common.constant.ts | 2 +- libs/common/src/resend-helper-file.ts | 20 +---- .../src/secret-storage/openbao.provider.ts | 21 +++-- 4 files changed, 57 insertions(+), 62 deletions(-) diff --git a/libs/aws/src/aws.service.ts b/libs/aws/src/aws.service.ts index 1f402a7f4..e125ddcc8 100644 --- a/libs/aws/src/aws.service.ts +++ b/libs/aws/src/aws.service.ts @@ -1,32 +1,36 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CommonConstants } from 'libs/common/src/common.constant'; import { RpcException } from '@nestjs/microservices'; import { S3 } from 'aws-sdk'; -import { promisify } from 'util'; +import { fetchOpenBaoSecrets } from 'libs/common/src/utils/openbao.util'; @Injectable() export class AwsService { - private s3: S3; - private s4: S3; - private s3StoreObject: S3; - - constructor() { - this.s3 = new S3({ - accessKeyId: process.env.AWS_ACCESS_KEY, - secretAccessKey: process.env.AWS_SECRET_KEY, - region: process.env.AWS_REGION + private async getS3Client(): Promise { + const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + return new S3({ + accessKeyId: secrets.AWS_ACCESS_KEY, + secretAccessKey: secrets.AWS_SECRET_KEY, + region: secrets.AWS_REGION }); + } - this.s4 = new S3({ - accessKeyId: process.env.AWS_PUBLIC_ACCESS_KEY, - secretAccessKey: process.env.AWS_PUBLIC_SECRET_KEY, - region: process.env.AWS_PUBLIC_REGION + private async getPublicS3Client(): Promise { + const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + return new S3({ + accessKeyId: secrets.AWS_PUBLIC_ACCESS_KEY, + secretAccessKey: secrets.AWS_PUBLIC_SECRET_KEY, + region: secrets.AWS_PUBLIC_REGION }); + } - this.s3StoreObject = new S3({ - accessKeyId: process.env.AWS_S3_STOREOBJECT_ACCESS_KEY, - secretAccessKey: process.env.AWS_S3_STOREOBJECT_SECRET_KEY, - region: process.env.AWS_S3_STOREOBJECT_REGION + private async getStoreObjectS3Client(): Promise { + const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + return new S3({ + accessKeyId: secrets.AWS_S3_STOREOBJECT_ACCESS_KEY, + secretAccessKey: secrets.AWS_S3_STOREOBJECT_SECRET_KEY, + region: secrets.AWS_S3_STOREOBJECT_REGION }); } @@ -38,17 +42,19 @@ export class AwsService { encoding: string, pathAWS: string = '' ): Promise { + const s4 = await this.getPublicS3Client(); const timestamp = Date.now(); - const putObjectAsync = promisify(this.s4.putObject).bind(this.s4); try { - await putObjectAsync({ - Bucket: `${bucketName}`, - Key: `${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`, - Body: fileBuffer, - ContentEncoding: encoding, - ContentType: `image/png` - }); + await s4 + .putObject({ + Bucket: bucketName, + Key: `${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`, + Body: fileBuffer, + ContentEncoding: encoding, + ContentType: 'image/png' + }) + .promise(); const imageUrl = `https://${bucketName}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`; return imageUrl; @@ -58,44 +64,48 @@ export class AwsService { } async uploadCsvFile(key: string, body: unknown): Promise { + const s3 = await this.getS3Client(); const params: AWS.S3.PutObjectRequest = { - Bucket: process.env.AWS_BUCKET, + Bucket: process.env.FILE_SHARING_BUCKET, Key: key, Body: 'string' === typeof body ? body : body.toString() }; try { - await this.s3.upload(params).promise(); + await s3.upload(params).promise(); } catch (error) { throw new RpcException(error.response ? error.response : error); } } async getFile(key: string): Promise { + const s3 = await this.getS3Client(); const params: AWS.S3.GetObjectRequest = { - Bucket: process.env.AWS_BUCKET, + Bucket: process.env.FILE_SHARING_BUCKET, Key: key }; try { - return this.s3.getObject(params).promise(); + return s3.getObject(params).promise(); } catch (error) { throw new RpcException(error.response ? error.response : error); } } async deleteFile(key: string): Promise { + const s3 = await this.getS3Client(); const params: AWS.S3.DeleteObjectRequest = { - Bucket: process.env.AWS_BUCKET, + Bucket: process.env.FILE_SHARING_BUCKET, Key: key }; try { - await this.s3.deleteObject(params).promise(); + await s3.deleteObject(params).promise(); } catch (error) { throw new RpcException(error.response ? error.response : error); } } async storeObject(persistent: boolean, key: string, body: unknown): Promise { + const s3StoreObject = await this.getStoreObjectS3Client(); const objKey: string = persistent.valueOf() ? `persist/${key}` : `default/${key}`; const buf = Buffer.from(JSON.stringify(body)); const params: AWS.S3.PutObjectRequest = { @@ -107,7 +117,7 @@ export class AwsService { }; try { - const receivedData = await this.s3StoreObject.upload(params).promise(); + const receivedData = await s3StoreObject.upload(params).promise(); return receivedData; } catch (error) { throw new RpcException(error.response ? error.response : error); diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index b00706aec..6eac4ffff 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -445,7 +445,7 @@ export enum CommonConstants { // BAO_STORAGE_PATH CREDEBL_RESEND_API_KEY_PATH = 'secret/data/credebl_resend_api_key', - + CREDEBL_AWS_KEY_PATH = 'secret/data/credebl_aws_keys', //X509 X509_CREATE_CERTIFICATE = 'create-x509-certificate', diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index 499d24869..a19d5fd4f 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -1,35 +1,24 @@ import { CommonConstants } from './common.constant'; import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; -import { OpenBaoProvider } from 'libs/config/src/secret-storage/openbao.provider'; import { Resend } from 'resend'; - -let resend: Resend | null = null; +import { fetchOpenBaoSecrets } from './utils/openbao.util'; async function getResendClient(): Promise { - if (resend) { - return resend; - } - console.log("get resend client called"); - const openBaoProvider = new OpenBaoProvider(); - const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH || ''; - - const secrets = await openBaoProvider.loadSecrets(secretPath); - console.log('🔐 Successfully fetched secrets from OpenBao for Resend API Key', secrets); + const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH; + const secrets = await fetchOpenBaoSecrets(secretPath); const apiKey = secrets.RESEND_API_KEY; if (!apiKey) { throw new Error('Missing RESEND_API_KEY in environment variables.'); } - resend = new Resend(apiKey); - return resend; + return new Resend(apiKey); } export const sendWithResend = async (emailDto: EmailDto): Promise => { try { const client = await getResendClient(); - console.log('Sending email with Resend:', emailDto); const response = await client.emails.send({ from: emailDto.emailFrom, to: emailDto.emailTo, @@ -38,7 +27,6 @@ export const sendWithResend = async (emailDto: EmailDto): Promise => { html: emailDto.emailHtml, attachments: emailDto.emailAttachments }); - console.log('Resend email response:', response); return Boolean(response.data?.id); } catch (error) { Logger.error('Error while sending email with Resend', error); diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts index e1ed90fc8..4aad27300 100644 --- a/libs/config/src/secret-storage/openbao.provider.ts +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -1,43 +1,42 @@ +import { Logger } from '@nestjs/common'; import { SecretProvider } from './secret-provider.interface'; export class OpenBaoProvider implements SecretProvider { readonly name = 'OpenBao'; + private readonly logger = new Logger(OpenBaoProvider.name); isEnabled(): boolean { return 'true' === process.env.ENABLE_BAO?.trim()?.toLowerCase(); } - async loadSecrets(customPath:string = ''): Promise> { + async loadSecrets(customPath: string = ''): Promise> { const baoUrl = process.env.BAO_URL; const secretPath = customPath || process.env.BAO_SECRET_PATH; const roleId = process.env.BAO_ROLE_ID; const secretId = process.env.BAO_SECRET_ID; - console.log(`🔐 OpenBaoProvider: Fetching secrets from ${baoUrl}/v1/${secretPath} with roleId=${roleId}`); + this.logger.log(`Fetching secrets from ${baoUrl}/v1/${secretPath}`); if (!roleId || !secretId) { throw new Error('BAO_ROLE_ID and BAO_SECRET_ID must be set.'); } - console.log(`🔐 OpenBaoProvider: Using roleId=${roleId} and secretId=${secretId}`); - console.log(`${baoUrl}/v1/auth/approle/login`) + this.logger.log(`Authenticating with AppRole at ${baoUrl}/v1/auth/approle/login`); // --- Authentication --- const authResponse = await fetch(`${baoUrl}/v1/auth/approle/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, // eslint-disable-next-line camelcase body: JSON.stringify({ role_id: roleId, secret_id: secretId }) - }) - console.log(`🔐 OpenBaoProvider: Authentication response status: ${authResponse}`); + }); if (!authResponse.ok) { throw new Error(`Authentication failed: Status ${authResponse.status}`); } - + const authData = await authResponse.json(); - console.log("authresponse", authData); const baoToken = authData.auth?.client_token; if (!baoToken) { throw new Error('Failed to retrieve client token from OpenBao.'); } - console.log("request url",`${baoUrl}/v1/${secretPath}`) + this.logger.log(`Fetching secrets from ${baoUrl}/v1/${secretPath}`); // --- Fetch Secrets --- const response = await fetch(`${baoUrl}/v1/${secretPath}`, { method: 'GET', @@ -46,15 +45,13 @@ export class OpenBaoProvider implements SecretProvider { 'Content-Type': 'application/json' } }); - console.log(`🔐 OpenBaoProvider: Fetch secrets response status: ${response}`); if (!response.ok) { - console.log("inside not ok") throw new Error(`Fetch failed: Status ${response.status}`); } const result = await response.json(); const secrets = result.data?.data; - console.log('🔐 Successfully fetched secrets from OpenBao', result); + this.logger.log('Successfully fetched secrets from OpenBao'); if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { throw new Error('Unexpected secrets payload structure.'); } From 2b9b87cb00559d5cbfcb7a4aeb0d845d5d53c43c Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 30 Jun 2026 11:50:49 +0530 Subject: [PATCH 14/58] fix: sonar cloud issues Signed-off-by: sujitaw --- .../src/providers/base-s3-storage.provider.ts | 31 +++-- .../src/providers/rustfs-storage.provider.ts | 31 ++--- .../src/providers/s3-storage.provider.ts | 108 +----------------- 3 files changed, 36 insertions(+), 134 deletions(-) diff --git a/libs/storage/src/providers/base-s3-storage.provider.ts b/libs/storage/src/providers/base-s3-storage.provider.ts index 4403130d3..25c274b7d 100644 --- a/libs/storage/src/providers/base-s3-storage.provider.ts +++ b/libs/storage/src/providers/base-s3-storage.provider.ts @@ -7,19 +7,9 @@ import { S3 } from 'aws-sdk'; import { promisify } from 'node:util'; export abstract class BaseS3StorageService implements IStorageService { - protected s3: S3; - protected s4: S3; - protected s3StoreObject: S3; - - constructor( - s3Config: Record, - s4Config: Record, - storeObjectConfig: Record - ) { - this.s3 = new S3(s3Config); - this.s4 = new S3(s4Config); - this.s3StoreObject = new S3(storeObjectConfig); - } + protected abstract getS3Client(): Promise; + protected abstract getPublicS3Client(): Promise; + protected abstract getStoreObjectS3Client(): Promise; abstract getPublicUrl(bucketName: string, fileKey: string): string; @@ -31,8 +21,9 @@ export abstract class BaseS3StorageService implements IStorageService { encoding: string, pathAWS: string = '' ): Promise { + const s4 = await this.getPublicS3Client(); const timestamp = Date.now(); - const putObjectAsync = promisify(this.s4.putObject).bind(this.s4); + const putObjectAsync = promisify(s4.putObject).bind(s4); const fileKey = `${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`; try { await putObjectAsync({ @@ -49,6 +40,7 @@ export abstract class BaseS3StorageService implements IStorageService { } async uploadCsvFile(key: string, body: unknown): Promise { + const s3 = await this.getS3Client(); let data: string; if ('string' === typeof body) { data = body; @@ -64,37 +56,40 @@ export abstract class BaseS3StorageService implements IStorageService { Body: data }; try { - await this.s3.upload(params).promise(); + await s3.upload(params).promise(); } catch (error) { throw new RpcException(error.response ? error.response : error); } } async getFile(key: string): Promise { + const s3 = await this.getS3Client(); const params: AWS.S3.GetObjectRequest = { Bucket: process.env.FILE_SHARING_BUCKET, Key: key }; try { - return this.s3.getObject(params).promise(); + return s3.getObject(params).promise(); } catch (error) { throw new RpcException(error.response ? error.response : error); } } async deleteFile(key: string): Promise { + const s3 = await this.getS3Client(); const params: AWS.S3.DeleteObjectRequest = { Bucket: process.env.FILE_SHARING_BUCKET, Key: key }; try { - await this.s3.deleteObject(params).promise(); + await s3.deleteObject(params).promise(); } catch (error) { throw new RpcException(error.response ? error.response : error); } } async storeObject(persistent: boolean, key: string, body: unknown): Promise { + const s3StoreObject = await this.getStoreObjectS3Client(); const objKey: string = persistent.valueOf() ? `persist/${key}` : `default/${key}`; const buf = Buffer.from(JSON.stringify(body)); const params: AWS.S3.PutObjectRequest = { @@ -106,7 +101,7 @@ export abstract class BaseS3StorageService implements IStorageService { }; try { - return await this.s3StoreObject.upload(params).promise(); + return await s3StoreObject.upload(params).promise(); } catch (error) { throw new RpcException(error.response ? error.response : error); } diff --git a/libs/storage/src/providers/rustfs-storage.provider.ts b/libs/storage/src/providers/rustfs-storage.provider.ts index c90287b95..9dee2d144 100644 --- a/libs/storage/src/providers/rustfs-storage.provider.ts +++ b/libs/storage/src/providers/rustfs-storage.provider.ts @@ -1,27 +1,30 @@ import { BaseS3StorageService } from './base-s3-storage.provider'; import { Injectable } from '@nestjs/common'; +import { S3 } from 'aws-sdk'; @Injectable() export class RustFsStorageService extends BaseS3StorageService { - constructor() { - const endpoint = process.env.RUSTFS_ENDPOINT || 'http://localhost:9000'; + private readonly endpoint = process.env.RUSTFS_ENDPOINT || 'http://localhost:9000'; + private readonly baseConfig = { + accessKeyId: process.env.RUSTFS_ACCESS_KEY_ID, + secretAccessKey: process.env.RUSTFS_SECRET_ACCESS_KEY, + endpoint: this.endpoint, + s3ForcePathStyle: true + }; - const baseConfig = { - accessKeyId: process.env.RUSTFS_ACCESS_KEY_ID, - secretAccessKey: process.env.RUSTFS_SECRET_ACCESS_KEY, - endpoint, - s3ForcePathStyle: true - }; + protected async getS3Client(): Promise { + return new S3({ ...this.baseConfig, region: process.env.AWS_REGION }); + } - const s3Config = { ...baseConfig, region: process.env.AWS_REGION }; - const s4Config = { ...baseConfig, region: process.env.AWS_PUBLIC_REGION }; - const storeObjectConfig = { ...baseConfig, region: process.env.AWS_S3_STOREOBJECT_REGION }; + protected async getPublicS3Client(): Promise { + return new S3({ ...this.baseConfig, region: process.env.AWS_PUBLIC_REGION }); + } - super(s3Config, s4Config, storeObjectConfig); + protected async getStoreObjectS3Client(): Promise { + return new S3({ ...this.baseConfig, region: process.env.AWS_S3_STOREOBJECT_REGION }); } getPublicUrl(bucketName: string, fileKey: string): string { - const endpoint = process.env.RUSTFS_ENDPOINT || 'http://localhost:9000'; - return `${endpoint}/${bucketName}/${fileKey}`; + return `${this.endpoint}/${bucketName}/${fileKey}`; } } diff --git a/libs/storage/src/providers/s3-storage.provider.ts b/libs/storage/src/providers/s3-storage.provider.ts index d9a75f6ff..be05b4ba3 100644 --- a/libs/storage/src/providers/s3-storage.provider.ts +++ b/libs/storage/src/providers/s3-storage.provider.ts @@ -1,15 +1,13 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; -import { Buffer } from 'node:buffer'; import { CommonConstants } from 'libs/common/src/common.constant'; -import { RpcException } from '@nestjs/microservices'; import { S3 } from 'aws-sdk'; import { fetchOpenBaoSecrets } from 'libs/common/src/utils/openbao.util'; -import { promisify } from 'node:util'; +import { BaseS3StorageService } from './base-s3-storage.provider'; @Injectable() -export class S3StorageService { - private async getS3Client(): Promise { +export class S3StorageService extends BaseS3StorageService { + protected async getS3Client(): Promise { const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); return new S3({ accessKeyId: secrets.AWS_ACCESS_KEY, @@ -18,7 +16,7 @@ export class S3StorageService { }); } - private async getPublicS3Client(): Promise { + protected async getPublicS3Client(): Promise { const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); return new S3({ accessKeyId: secrets.AWS_PUBLIC_ACCESS_KEY, @@ -27,7 +25,7 @@ export class S3StorageService { }); } - private async getStoreObjectS3Client(): Promise { + protected async getStoreObjectS3Client(): Promise { const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); return new S3({ accessKeyId: secrets.AWS_S3_STOREOBJECT_ACCESS_KEY, @@ -39,98 +37,4 @@ export class S3StorageService { getPublicUrl(bucketName: string, fileKey: string): string { return `https://${bucketName}.s3.${process.env.AWS_PUBLIC_REGION}.amazonaws.com/${fileKey}`; } - - async uploadFileToS3Bucket( - fileBuffer: Buffer, - ext: string, - filename: string, - bucketName: string, - encoding: string, - pathAWS: string = '' - ): Promise { - const s4 = await this.getPublicS3Client(); - const timestamp = Date.now(); - const putObjectAsync = promisify(s4.putObject).bind(s4); - const fileKey = `${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`; - try { - await putObjectAsync({ - Bucket: `${bucketName}`, - Key: `${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`, - Body: fileBuffer, - ContentEncoding: encoding, - ContentType: `image/png` - }); - return this.getPublicUrl(bucketName, fileKey); - } catch (error) { - throw new HttpException(error, HttpStatus.SERVICE_UNAVAILABLE); - } - } - - async uploadCsvFile(key: string, body: unknown): Promise { - const s3 = await this.getS3Client(); - let data: string; - if ('string' === typeof body) { - data = body; - } else if (Buffer.isBuffer(body)) { - data = (body as Buffer).toString('utf-8'); - } else { - data = JSON.stringify(body); - } - - const params: AWS.S3.PutObjectRequest = { - Bucket: process.env.FILE_SHARING_BUCKET, - Key: key, - Body: data - }; - try { - await s3.upload(params).promise(); - } catch (error) { - throw new RpcException(error.response ? error.response : error); - } - } - - async getFile(key: string): Promise { - const s3 = await this.getS3Client(); - const params: AWS.S3.GetObjectRequest = { - Bucket: process.env.FILE_SHARING_BUCKET, - Key: key - }; - try { - return s3.getObject(params).promise(); - } catch (error) { - throw new RpcException(error.response ? error.response : error); - } - } - - async deleteFile(key: string): Promise { - const s3 = await this.getS3Client(); - const params: AWS.S3.DeleteObjectRequest = { - Bucket: process.env.FILE_SHARING_BUCKET, - Key: key - }; - try { - await s3.deleteObject(params).promise(); - } catch (error) { - throw new RpcException(error.response ? error.response : error); - } - } - - async storeObject(persistent: boolean, key: string, body: unknown): Promise { - const s3StoreObject = await this.getStoreObjectS3Client(); - const objKey: string = persistent.valueOf() ? `persist/${key}` : `default/${key}`; - const buf = Buffer.from(JSON.stringify(body)); - const params: AWS.S3.PutObjectRequest = { - Bucket: process.env.STOREOBJECT_BUCKET, - Body: buf, - Key: objKey, - ContentEncoding: 'base64', - ContentType: 'application/json' - }; - - try { - return await s3StoreObject.upload(params).promise(); - } catch (error) { - throw new RpcException(error.response ? error.response : error); - } - } } From 1f9c116c53b7a42829af02a8102d98e4aa1f7b6b Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 30 Jun 2026 14:56:12 +0530 Subject: [PATCH 15/58] fix:code rabbit comments for openbao Signed-off-by: sujitaw --- libs/common/src/common.constant.ts | 4 +- libs/common/src/resend-helper-file.ts | 20 ++++-- .../src/secret-storage/openbao.provider.ts | 66 ++++++++++++++----- .../src/providers/base-s3-storage.provider.ts | 24 ++++++- .../src/providers/s3-storage.provider.ts | 11 ++-- 5 files changed, 95 insertions(+), 30 deletions(-) diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 83314982f..db710b93d 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -455,7 +455,9 @@ export enum CommonConstants { // Storage Types STORAGE_TYPE_AWS = 'aws', STORAGE_TYPE_LOCAL = 'local', - STORAGE_TYPE_RUSTFS = 'rustfs' + STORAGE_TYPE_RUSTFS = 'rustfs', + + OPENBAO_REQUEST_TIMEOUT = 10000 } export const MICRO_SERVICE_NAME = Symbol('MICRO_SERVICE_NAME'); export const ATTRIBUTE_NAME_REGEX = /\['(.*?)'\]/; diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index a19d5fd4f..2c207c342 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -4,16 +4,22 @@ import { Logger } from '@nestjs/common'; import { Resend } from 'resend'; import { fetchOpenBaoSecrets } from './utils/openbao.util'; +let resendClientPromise: Promise | undefined; + async function getResendClient(): Promise { - const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH; - const secrets = await fetchOpenBaoSecrets(secretPath); - const apiKey = secrets.RESEND_API_KEY; + resendClientPromise ??= (async (): Promise => { + const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH; + const secrets = await fetchOpenBaoSecrets(secretPath); + const apiKey = secrets.RESEND_API_KEY; - if (!apiKey) { - throw new Error('Missing RESEND_API_KEY in environment variables.'); - } + if (!apiKey) { + throw new Error('Missing RESEND_API_KEY in secret payload.'); + } + + return new Resend(apiKey); + })(); - return new Resend(apiKey); + return resendClientPromise; } export const sendWithResend = async (emailDto: EmailDto): Promise => { diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts index 4aad27300..f9b9a0070 100644 --- a/libs/config/src/secret-storage/openbao.provider.ts +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -1,6 +1,18 @@ +import { CommonConstants } from '@credebl/common/common.constant'; import { Logger } from '@nestjs/common'; import { SecretProvider } from './secret-provider.interface'; +async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(url, { ...options, signal: controller.signal }); + return response; + } finally { + clearTimeout(timeoutId); + } +} + export class OpenBaoProvider implements SecretProvider { readonly name = 'OpenBao'; private readonly logger = new Logger(OpenBaoProvider.name); @@ -14,18 +26,30 @@ export class OpenBaoProvider implements SecretProvider { const secretPath = customPath || process.env.BAO_SECRET_PATH; const roleId = process.env.BAO_ROLE_ID; const secretId = process.env.BAO_SECRET_ID; - this.logger.log(`Fetching secrets from ${baoUrl}/v1/${secretPath}`); - if (!roleId || !secretId) { - throw new Error('BAO_ROLE_ID and BAO_SECRET_ID must be set.'); + if (!baoUrl || !secretPath || !roleId || !secretId) { + throw new Error('BAO_URL, BAO_SECRET_PATH, BAO_ROLE_ID, and BAO_SECRET_ID must be set.'); } + this.logger.log(`Fetching secrets from ${baoUrl}/v1/${secretPath}`); this.logger.log(`Authenticating with AppRole at ${baoUrl}/v1/auth/approle/login`); // --- Authentication --- - const authResponse = await fetch(`${baoUrl}/v1/auth/approle/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - // eslint-disable-next-line camelcase - body: JSON.stringify({ role_id: roleId, secret_id: secretId }) - }); + let authResponse: Response; + try { + authResponse = await fetchWithTimeout( + `${baoUrl}/v1/auth/approle/login`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + // eslint-disable-next-line camelcase + body: JSON.stringify({ role_id: roleId, secret_id: secretId }) + }, + CommonConstants.OPENBAO_REQUEST_TIMEOUT + ); + } catch (error) { + if (error instanceof DOMException && 'AbortError' === error.name) { + throw new Error('OpenBao authentication request timed out.'); + } + throw error; + } if (!authResponse.ok) { throw new Error(`Authentication failed: Status ${authResponse.status}`); } @@ -38,13 +62,25 @@ export class OpenBaoProvider implements SecretProvider { } this.logger.log(`Fetching secrets from ${baoUrl}/v1/${secretPath}`); // --- Fetch Secrets --- - const response = await fetch(`${baoUrl}/v1/${secretPath}`, { - method: 'GET', - headers: { - 'X-Vault-Token': baoToken, - 'Content-Type': 'application/json' + let response: Response; + try { + response = await fetchWithTimeout( + `${baoUrl}/v1/${secretPath}`, + { + method: 'GET', + headers: { + 'X-Vault-Token': baoToken, + 'Content-Type': 'application/json' + } + }, + CommonConstants.OPENBAO_REQUEST_TIMEOUT + ); + } catch (error) { + if (error instanceof DOMException && 'AbortError' === error.name) { + throw new Error('OpenBao secrets fetch request timed out.'); } - }); + throw error; + } if (!response.ok) { throw new Error(`Fetch failed: Status ${response.status}`); } diff --git a/libs/storage/src/providers/base-s3-storage.provider.ts b/libs/storage/src/providers/base-s3-storage.provider.ts index 25c274b7d..fbb55b9b3 100644 --- a/libs/storage/src/providers/base-s3-storage.provider.ts +++ b/libs/storage/src/providers/base-s3-storage.provider.ts @@ -6,6 +6,28 @@ import { RpcException } from '@nestjs/microservices'; import { S3 } from 'aws-sdk'; import { promisify } from 'node:util'; +function extToMimeType(ext: string): string { + const mime: Record = { + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + bmp: 'image/bmp', + pdf: 'application/pdf', + csv: 'text/csv', + json: 'application/json', + txt: 'text/plain', + html: 'text/html', + xml: 'application/xml', + zip: 'application/zip', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }; + return mime[ext.toLowerCase()] ?? 'application/octet-stream'; +} + export abstract class BaseS3StorageService implements IStorageService { protected abstract getS3Client(): Promise; protected abstract getPublicS3Client(): Promise; @@ -31,7 +53,7 @@ export abstract class BaseS3StorageService implements IStorageService { Key: `${pathAWS}/${encodeURIComponent(filename)}-${timestamp}.${ext}`, Body: fileBuffer, ContentEncoding: encoding, - ContentType: `image/png` + ContentType: extToMimeType(ext) }); return this.getPublicUrl(bucketName, fileKey); } catch (error) { diff --git a/libs/storage/src/providers/s3-storage.provider.ts b/libs/storage/src/providers/s3-storage.provider.ts index be05b4ba3..255e49fbf 100644 --- a/libs/storage/src/providers/s3-storage.provider.ts +++ b/libs/storage/src/providers/s3-storage.provider.ts @@ -1,9 +1,8 @@ -import { Injectable } from '@nestjs/common'; - +import { BaseS3StorageService } from './base-s3-storage.provider'; import { CommonConstants } from 'libs/common/src/common.constant'; +import { Injectable } from '@nestjs/common'; import { S3 } from 'aws-sdk'; import { fetchOpenBaoSecrets } from 'libs/common/src/utils/openbao.util'; -import { BaseS3StorageService } from './base-s3-storage.provider'; @Injectable() export class S3StorageService extends BaseS3StorageService { @@ -12,7 +11,7 @@ export class S3StorageService extends BaseS3StorageService { return new S3({ accessKeyId: secrets.AWS_ACCESS_KEY, secretAccessKey: secrets.AWS_SECRET_KEY, - region: secrets.AWS_REGION + region: process.env.AWS_REGION }); } @@ -21,7 +20,7 @@ export class S3StorageService extends BaseS3StorageService { return new S3({ accessKeyId: secrets.AWS_PUBLIC_ACCESS_KEY, secretAccessKey: secrets.AWS_PUBLIC_SECRET_KEY, - region: secrets.AWS_PUBLIC_REGION + region: process.env.AWS_PUBLIC_REGION }); } @@ -30,7 +29,7 @@ export class S3StorageService extends BaseS3StorageService { return new S3({ accessKeyId: secrets.AWS_S3_STOREOBJECT_ACCESS_KEY, secretAccessKey: secrets.AWS_S3_STOREOBJECT_SECRET_KEY, - region: secrets.AWS_S3_STOREOBJECT_REGION + region: process.env.AWS_S3_STOREOBJECT_REGION }); } From c90b5edd9a434b4db72d5d19fd4e82f54d4c761a Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 30 Jun 2026 15:19:11 +0530 Subject: [PATCH 16/58] fix: remove commented code Signed-off-by: sujitaw --- libs/config/src/secret-storage/secrets-loader.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/libs/config/src/secret-storage/secrets-loader.ts b/libs/config/src/secret-storage/secrets-loader.ts index 251a37f4d..951330d80 100644 --- a/libs/config/src/secret-storage/secrets-loader.ts +++ b/libs/config/src/secret-storage/secrets-loader.ts @@ -1,5 +1,3 @@ -// import { AwsSecretsProvider } from './providers/aws-secrets.provider'; -// libs/config/src/secrets-loader.ts import { Logger } from '@nestjs/common'; import { OpenBaoProvider } from './openbao.provider'; import { SecretProvider } from './secret-provider.interface'; @@ -57,7 +55,6 @@ export async function loadConfigSecrets(): Promise { logger.log(`Successfully injected secrets from ${provider.name} into process.env`); } catch (error) { - // Safely handles instances where error might not be a standard Error object const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Critical Lifecycle Boot Failure (${provider.name}): ${errorMessage}`); throw new Error(`Failed to load secrets from ${provider.name}`); From 8c1dfb73f859d4b98be537cd8a3fbd4899179ced Mon Sep 17 00:00:00 2001 From: sujitaw Date: Tue, 30 Jun 2026 15:39:04 +0530 Subject: [PATCH 17/58] feat: util files for open bao Signed-off-by: sujitaw --- libs/common/src/utils/openbao.util.ts | 19 +++++++++++++ libs/common/src/utils/ttl-cache.util.ts | 38 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 libs/common/src/utils/openbao.util.ts create mode 100644 libs/common/src/utils/ttl-cache.util.ts diff --git a/libs/common/src/utils/openbao.util.ts b/libs/common/src/utils/openbao.util.ts new file mode 100644 index 000000000..6f333f15b --- /dev/null +++ b/libs/common/src/utils/openbao.util.ts @@ -0,0 +1,19 @@ +import { OpenBaoProvider } from 'libs/config/src/secret-storage/openbao.provider'; +import { TtlCache } from './ttl-cache.util'; + +const CACHE_TTL_MS = 10 * 60 * 1000; + +const secretCaches = new Map>>(); + +export async function fetchOpenBaoSecrets(secretPath: string): Promise> { + let cache = secretCaches.get(secretPath); + if (!cache) { + cache = new TtlCache>(CACHE_TTL_MS); + secretCaches.set(secretPath, cache); + } + + return cache.get(async () => { + const openBaoProvider = new OpenBaoProvider(); + return openBaoProvider.loadSecrets(secretPath); + }); +} diff --git a/libs/common/src/utils/ttl-cache.util.ts b/libs/common/src/utils/ttl-cache.util.ts new file mode 100644 index 000000000..5574ea441 --- /dev/null +++ b/libs/common/src/utils/ttl-cache.util.ts @@ -0,0 +1,38 @@ +export class TtlCache { + private value: T | null = null; + private timer: ReturnType | null = null; + private readonly ttlMs: number; + + constructor(ttlMs: number) { + this.ttlMs = ttlMs; + } + + async get(fetchFn: () => Promise): Promise { + if (null !== this.value) { + this.refreshTimer(); + return this.value; + } + + this.value = await fetchFn(); + this.refreshTimer(); + return this.value; + } + + clear(): void { + this.value = null; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + private refreshTimer(): void { + if (this.timer) { + clearTimeout(this.timer); + } + this.timer = setTimeout(() => { + this.clear(); + }, this.ttlMs); + this.timer.unref(); + } +} From 6b61197f2e8bc4d2408d953a4ec365d1bcdac5fe Mon Sep 17 00:00:00 2001 From: sujitaw Date: Wed, 1 Jul 2026 10:22:25 +0530 Subject: [PATCH 18/58] feat: add openbao implementation for sendgrid and smtp Signed-off-by: sujitaw --- libs/common/src/common.constant.ts | 2 + libs/common/src/resend-helper-file.ts | 6 +- libs/common/src/send-grid-helper-file.ts | 24 ++++++- libs/common/src/smtp-helper-file.ts | 72 ++++++++++--------- libs/common/src/utils/openbao.util.ts | 19 ----- libs/common/src/utils/secretLoader.util.ts | 28 ++++++++ .../src/secret-storage/openbao.provider.ts | 6 +- .../secret-provider.interface.ts | 2 +- .../src/secret-storage/secrets-loader.ts | 6 +- .../src/providers/s3-storage.provider.ts | 20 +++--- 10 files changed, 110 insertions(+), 75 deletions(-) delete mode 100644 libs/common/src/utils/openbao.util.ts create mode 100644 libs/common/src/utils/secretLoader.util.ts diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index db710b93d..9613ed454 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -445,6 +445,8 @@ export enum CommonConstants { // BAO_STORAGE_PATH CREDEBL_RESEND_API_KEY_PATH = 'secret/data/credebl_resend_api_key', + CREDEBL_SMTP_CONFIG_PATH = 'secret/data/credebl_smtp_config', + CREDEBL_SENDGRID_API_KEY_PATH = 'secret/data/credebl_sendgrid_api_key', CREDEBL_AWS_KEY_PATH = 'secret/data/credebl_aws_keys', //X509 diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index 2c207c342..5c675a380 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -2,15 +2,15 @@ import { CommonConstants } from './common.constant'; import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; import { Resend } from 'resend'; -import { fetchOpenBaoSecrets } from './utils/openbao.util'; +import { fetchSecrets } from './utils/secretLoader.util'; let resendClientPromise: Promise | undefined; async function getResendClient(): Promise { resendClientPromise ??= (async (): Promise => { const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH; - const secrets = await fetchOpenBaoSecrets(secretPath); - const apiKey = secrets.RESEND_API_KEY; + const secrets = await fetchSecrets(secretPath); + const apiKey = secrets.RESEND_API_KEY ?? process.env.RESEND_API_KEY; if (!apiKey) { throw new Error('Missing RESEND_API_KEY in secret payload.'); diff --git a/libs/common/src/send-grid-helper-file.ts b/libs/common/src/send-grid-helper-file.ts index 3ae07faf1..110a71598 100644 --- a/libs/common/src/send-grid-helper-file.ts +++ b/libs/common/src/send-grid-helper-file.ts @@ -1,14 +1,31 @@ -import * as dotenv from 'dotenv'; import * as sendgrid from '@sendgrid/mail'; +import { CommonConstants } from './common.constant'; import { EmailDto } from './dtos/email.dto'; +import { Logger } from '@nestjs/common'; +import { fetchSecrets } from './utils/secretLoader.util'; -dotenv.config(); +let sendgridInitPromise: Promise | undefined; -sendgrid.setApiKey(process.env.SENDGRID_API_KEY); +async function initSendgrid(): Promise { + sendgridInitPromise ??= (async (): Promise => { + const secretPath = CommonConstants.CREDEBL_SENDGRID_API_KEY_PATH; + const secrets = await fetchSecrets(secretPath); + const apiKey = secrets.SENDGRID_API_KEY ?? process.env.SENDGRID_API_KEY; + + if (!apiKey) { + throw new Error('Missing SENDGRID_API_KEY in secret payload.'); + } + + sendgrid.setApiKey(apiKey); + })(); + + return sendgridInitPromise; +} export const sendWithSendGrid = async (EmailDto: EmailDto): Promise => { try { + await initSendgrid(); const msg = { to: EmailDto.emailTo, from: EmailDto.emailFrom, @@ -22,6 +39,7 @@ export const sendWithSendGrid = async (EmailDto: EmailDto): Promise => .then(() => true) .catch(() => false); } catch (error) { + Logger.error('Error while sending email with SendGrid', error); return false; } }; diff --git a/libs/common/src/smtp-helper-file.ts b/libs/common/src/smtp-helper-file.ts index be0559e0a..8222f617f 100644 --- a/libs/common/src/smtp-helper-file.ts +++ b/libs/common/src/smtp-helper-file.ts @@ -1,47 +1,49 @@ -import * as dotenv from 'dotenv'; import * as nodemailer from 'nodemailer'; +import { CommonConstants } from './common.constant'; import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; +import { fetchSecrets } from './utils/secretLoader.util'; + +let transporterPromise: Promise | undefined; + +async function getSmtpTransporter(): Promise { + transporterPromise ??= (async (): Promise => { + const secretPath = CommonConstants.CREDEBL_SMTP_CONFIG_PATH; + const secrets = await fetchSecrets(secretPath); + const smtpHost = secrets.SMTP_HOST ?? process.env.SMTP_HOST; + const smtpPort = secrets.SMTP_PORT ?? process.env.SMTP_PORT; + const smtpUser = secrets.SMTP_USER ?? process.env.SMTP_USER; + const smtpPass = secrets.SMTP_PASS ?? process.env.SMTP_PASS; + + if (!smtpHost || !smtpPort || !smtpUser || !smtpPass) { + throw new Error('Missing SMTP configuration. Required: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS'); + } + + const port = Number(smtpPort); + + if (!Number.isInteger(port) || 0 >= port) { + throw new Error(`Invalid SMTP_PORT value: "${smtpPort}". Must be a valid number.`); + } + + return nodemailer.createTransport({ + host: smtpHost, + port, + secure: 465 === port, + auth: { + user: smtpUser, + pass: smtpPass + }, + requireTLS: 587 === port + }); + })(); -dotenv.config(); - -const emailProvider = process.env.EMAIL_PROVIDER?.toLowerCase(); - -let transporter: nodemailer.Transporter | null = null; - -if ('smtp' === emailProvider) { - const { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS } = process.env; - - if (!SMTP_HOST || !SMTP_PORT || !SMTP_USER || !SMTP_PASS) { - throw new Error('Missing SMTP configuration. Required: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS'); - } - - const port = Number(SMTP_PORT); - - if (!Number.isInteger(port) || 0 >= port) { - throw new Error(`Invalid SMTP_PORT value: "${SMTP_PORT}". Must be a valid number.`); - } - - transporter = nodemailer.createTransport({ - host: SMTP_HOST, - port: Number(SMTP_PORT), - secure: 465 === Number(SMTP_PORT), - auth: { - user: SMTP_USER, - pass: SMTP_PASS - }, - requireTLS: 587 === Number(SMTP_PORT) - }); + return transporterPromise; } export const sendWithSMTP = async (emailDto: EmailDto): Promise => { - if (!transporter) { - Logger.error('SMTP email provider is not initialized'); - return false; - } - try { + const transporter = await getSmtpTransporter(); await transporter.sendMail({ from: emailDto.emailFrom, to: emailDto.emailTo, diff --git a/libs/common/src/utils/openbao.util.ts b/libs/common/src/utils/openbao.util.ts deleted file mode 100644 index 6f333f15b..000000000 --- a/libs/common/src/utils/openbao.util.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { OpenBaoProvider } from 'libs/config/src/secret-storage/openbao.provider'; -import { TtlCache } from './ttl-cache.util'; - -const CACHE_TTL_MS = 10 * 60 * 1000; - -const secretCaches = new Map>>(); - -export async function fetchOpenBaoSecrets(secretPath: string): Promise> { - let cache = secretCaches.get(secretPath); - if (!cache) { - cache = new TtlCache>(CACHE_TTL_MS); - secretCaches.set(secretPath, cache); - } - - return cache.get(async () => { - const openBaoProvider = new OpenBaoProvider(); - return openBaoProvider.loadSecrets(secretPath); - }); -} diff --git a/libs/common/src/utils/secretLoader.util.ts b/libs/common/src/utils/secretLoader.util.ts new file mode 100644 index 000000000..63cd53352 --- /dev/null +++ b/libs/common/src/utils/secretLoader.util.ts @@ -0,0 +1,28 @@ +import { TtlCache } from './ttl-cache.util'; +import { getSecretProvider } from 'libs/config/src/secret-storage/secrets-loader'; + +const CACHE_TTL_MS = 10 * 60 * 1000; + +const secretCaches = new Map>>(); + +export async function fetchSecrets(secretPath: string): Promise> { + if ('true' !== process.env.ENABLE_BAO?.trim()?.toLowerCase()) { + return {}; + } + let cache = secretCaches.get(secretPath); + if (!cache) { + cache = new TtlCache>(CACHE_TTL_MS); + secretCaches.set(secretPath, cache); + } + return cache.get(async () => { + const providerType = process.env.SECRETS_PROVIDER?.trim(); + if (!providerType) { + return {}; + } + const provider = getSecretProvider(providerType); + if (!provider) { + return {}; + } + return provider.loadSecrets({ customPath: secretPath }); + }); +} diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts index f9b9a0070..a4e73e0ae 100644 --- a/libs/config/src/secret-storage/openbao.provider.ts +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -21,9 +21,9 @@ export class OpenBaoProvider implements SecretProvider { return 'true' === process.env.ENABLE_BAO?.trim()?.toLowerCase(); } - async loadSecrets(customPath: string = ''): Promise> { + async loadSecrets(options?: { customPath?: string }): Promise> { const baoUrl = process.env.BAO_URL; - const secretPath = customPath || process.env.BAO_SECRET_PATH; + const secretPath = options?.customPath || process.env.BAO_SECRET_PATH; const roleId = process.env.BAO_ROLE_ID; const secretId = process.env.BAO_SECRET_ID; if (!baoUrl || !secretPath || !roleId || !secretId) { @@ -87,7 +87,7 @@ export class OpenBaoProvider implements SecretProvider { const result = await response.json(); const secrets = result.data?.data; - this.logger.log('Successfully fetched secrets from OpenBao'); + this.logger.log('Successfully fetched secrets from OpenBao', secrets); if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { throw new Error('Unexpected secrets payload structure.'); } diff --git a/libs/config/src/secret-storage/secret-provider.interface.ts b/libs/config/src/secret-storage/secret-provider.interface.ts index 9d734319e..f982c868a 100644 --- a/libs/config/src/secret-storage/secret-provider.interface.ts +++ b/libs/config/src/secret-storage/secret-provider.interface.ts @@ -9,5 +9,5 @@ export interface SecretProvider { * as a flat key-value object of strings. * * @returns A promise resolving to Record */ - loadSecrets(): Promise>; + loadSecrets(options?: Record): Promise>; } diff --git a/libs/config/src/secret-storage/secrets-loader.ts b/libs/config/src/secret-storage/secrets-loader.ts index 951330d80..4458be121 100644 --- a/libs/config/src/secret-storage/secrets-loader.ts +++ b/libs/config/src/secret-storage/secrets-loader.ts @@ -8,7 +8,7 @@ const logger = new Logger('SecretsLoader'); /** * Factory function to get the requested secret provider */ -function getSecretProvider(providerType: string): SecretProvider | null { +export function getSecretProvider(providerType: string): SecretProvider | null { switch (providerType.toLowerCase()) { case 'openbao': return new OpenBaoProvider(); @@ -19,6 +19,10 @@ function getSecretProvider(providerType: string): SecretProvider | null { } export async function loadConfigSecrets(): Promise { + if ('true' !== process.env.ENABLE_BAO?.trim()?.toLowerCase()) { + logger.log('OpenBao secrets management is disabled. Skipping remote secrets fetching.'); + return; + } const providerType = process.env.SECRETS_PROVIDER?.trim(); if (!providerType) { diff --git a/libs/storage/src/providers/s3-storage.provider.ts b/libs/storage/src/providers/s3-storage.provider.ts index 255e49fbf..1fba7d5b0 100644 --- a/libs/storage/src/providers/s3-storage.provider.ts +++ b/libs/storage/src/providers/s3-storage.provider.ts @@ -2,33 +2,33 @@ import { BaseS3StorageService } from './base-s3-storage.provider'; import { CommonConstants } from 'libs/common/src/common.constant'; import { Injectable } from '@nestjs/common'; import { S3 } from 'aws-sdk'; -import { fetchOpenBaoSecrets } from 'libs/common/src/utils/openbao.util'; +import { fetchSecrets } from '@credebl/common/utils/secretLoader.util'; @Injectable() export class S3StorageService extends BaseS3StorageService { protected async getS3Client(): Promise { - const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + const secrets = await fetchSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); return new S3({ - accessKeyId: secrets.AWS_ACCESS_KEY, - secretAccessKey: secrets.AWS_SECRET_KEY, + accessKeyId: secrets.AWS_ACCESS_KEY ?? process.env.AWS_ACCESS_KEY, + secretAccessKey: secrets.AWS_SECRET_KEY ?? process.env.AWS_SECRET_KEY, region: process.env.AWS_REGION }); } protected async getPublicS3Client(): Promise { - const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + const secrets = await fetchSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); return new S3({ - accessKeyId: secrets.AWS_PUBLIC_ACCESS_KEY, - secretAccessKey: secrets.AWS_PUBLIC_SECRET_KEY, + accessKeyId: secrets.AWS_PUBLIC_ACCESS_KEY ?? process.env.AWS_PUBLIC_ACCESS_KEY, + secretAccessKey: secrets.AWS_PUBLIC_SECRET_KEY ?? process.env.AWS_PUBLIC_SECRET_KEY, region: process.env.AWS_PUBLIC_REGION }); } protected async getStoreObjectS3Client(): Promise { - const secrets = await fetchOpenBaoSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + const secrets = await fetchSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); return new S3({ - accessKeyId: secrets.AWS_S3_STOREOBJECT_ACCESS_KEY, - secretAccessKey: secrets.AWS_S3_STOREOBJECT_SECRET_KEY, + accessKeyId: secrets.AWS_S3_STOREOBJECT_ACCESS_KEY ?? process.env.AWS_S3_STOREOBJECT_ACCESS_KEY, + secretAccessKey: secrets.AWS_S3_STOREOBJECT_SECRET_KEY ?? process.env.AWS_S3_STOREOBJECT_SECRET_KEY, region: process.env.AWS_S3_STOREOBJECT_REGION }); } From 8766ac6f003ec7fc781e11fbfeb907a2c42da3b6 Mon Sep 17 00:00:00 2001 From: sujitaw Date: Wed, 1 Jul 2026 12:09:28 +0530 Subject: [PATCH 19/58] fix: code rabbit comment on email data caching Signed-off-by: sujitaw --- libs/common/src/resend-helper-file.ts | 16 +++------------- libs/common/src/send-grid-helper-file.ts | 13 ++----------- libs/common/src/smtp-helper-file.ts | 15 +++------------ .../src/secret-storage/openbao.provider.ts | 2 +- libs/config/src/secret-storage/secrets-loader.ts | 2 +- 5 files changed, 10 insertions(+), 38 deletions(-) diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index 5c675a380..9950ee96a 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -4,10 +4,8 @@ import { Logger } from '@nestjs/common'; import { Resend } from 'resend'; import { fetchSecrets } from './utils/secretLoader.util'; -let resendClientPromise: Promise | undefined; - -async function getResendClient(): Promise { - resendClientPromise ??= (async (): Promise => { +export const sendWithResend = async (emailDto: EmailDto): Promise => { + try { const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH; const secrets = await fetchSecrets(secretPath); const apiKey = secrets.RESEND_API_KEY ?? process.env.RESEND_API_KEY; @@ -16,15 +14,7 @@ async function getResendClient(): Promise { throw new Error('Missing RESEND_API_KEY in secret payload.'); } - return new Resend(apiKey); - })(); - - return resendClientPromise; -} - -export const sendWithResend = async (emailDto: EmailDto): Promise => { - try { - const client = await getResendClient(); + const client = new Resend(apiKey); const response = await client.emails.send({ from: emailDto.emailFrom, to: emailDto.emailTo, diff --git a/libs/common/src/send-grid-helper-file.ts b/libs/common/src/send-grid-helper-file.ts index 110a71598..01e3f666d 100644 --- a/libs/common/src/send-grid-helper-file.ts +++ b/libs/common/src/send-grid-helper-file.ts @@ -5,10 +5,8 @@ import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; import { fetchSecrets } from './utils/secretLoader.util'; -let sendgridInitPromise: Promise | undefined; - -async function initSendgrid(): Promise { - sendgridInitPromise ??= (async (): Promise => { +export const sendWithSendGrid = async (EmailDto: EmailDto): Promise => { + try { const secretPath = CommonConstants.CREDEBL_SENDGRID_API_KEY_PATH; const secrets = await fetchSecrets(secretPath); const apiKey = secrets.SENDGRID_API_KEY ?? process.env.SENDGRID_API_KEY; @@ -18,14 +16,7 @@ async function initSendgrid(): Promise { } sendgrid.setApiKey(apiKey); - })(); - return sendgridInitPromise; -} - -export const sendWithSendGrid = async (EmailDto: EmailDto): Promise => { - try { - await initSendgrid(); const msg = { to: EmailDto.emailTo, from: EmailDto.emailFrom, diff --git a/libs/common/src/smtp-helper-file.ts b/libs/common/src/smtp-helper-file.ts index 8222f617f..1f629ad67 100644 --- a/libs/common/src/smtp-helper-file.ts +++ b/libs/common/src/smtp-helper-file.ts @@ -5,10 +5,8 @@ import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; import { fetchSecrets } from './utils/secretLoader.util'; -let transporterPromise: Promise | undefined; - -async function getSmtpTransporter(): Promise { - transporterPromise ??= (async (): Promise => { +export const sendWithSMTP = async (emailDto: EmailDto): Promise => { + try { const secretPath = CommonConstants.CREDEBL_SMTP_CONFIG_PATH; const secrets = await fetchSecrets(secretPath); const smtpHost = secrets.SMTP_HOST ?? process.env.SMTP_HOST; @@ -26,7 +24,7 @@ async function getSmtpTransporter(): Promise { throw new Error(`Invalid SMTP_PORT value: "${smtpPort}". Must be a valid number.`); } - return nodemailer.createTransport({ + const transporter = nodemailer.createTransport({ host: smtpHost, port, secure: 465 === port, @@ -36,14 +34,7 @@ async function getSmtpTransporter(): Promise { }, requireTLS: 587 === port }); - })(); - return transporterPromise; -} - -export const sendWithSMTP = async (emailDto: EmailDto): Promise => { - try { - const transporter = await getSmtpTransporter(); await transporter.sendMail({ from: emailDto.emailFrom, to: emailDto.emailTo, diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts index a4e73e0ae..2bffbe7fd 100644 --- a/libs/config/src/secret-storage/openbao.provider.ts +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -87,7 +87,7 @@ export class OpenBaoProvider implements SecretProvider { const result = await response.json(); const secrets = result.data?.data; - this.logger.log('Successfully fetched secrets from OpenBao', secrets); + this.logger.log('Successfully fetched secrets from OpenBao'); if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { throw new Error('Unexpected secrets payload structure.'); } diff --git a/libs/config/src/secret-storage/secrets-loader.ts b/libs/config/src/secret-storage/secrets-loader.ts index 4458be121..8c7d6c23b 100644 --- a/libs/config/src/secret-storage/secrets-loader.ts +++ b/libs/config/src/secret-storage/secrets-loader.ts @@ -61,6 +61,6 @@ export async function loadConfigSecrets(): Promise { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Critical Lifecycle Boot Failure (${provider.name}): ${errorMessage}`); - throw new Error(`Failed to load secrets from ${provider.name}`); + logger.error(`Failed to load secrets from ${provider.name}`); } } From 6d33404206ba576aa273c53368f1fe8326870ceb Mon Sep 17 00:00:00 2001 From: M Abdullah Date: Tue, 7 Jul 2026 16:10:16 +0500 Subject: [PATCH 20/58] empty attribute response Signed-off-by: M Abdullah --- .../utilities/dtos/shortening-url.dto.spec.ts | 31 +++++++++ .../src/utilities/dtos/shortening-url.dto.ts | 64 +++++++++---------- 2 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts diff --git a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts new file mode 100644 index 000000000..829480aae --- /dev/null +++ b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts @@ -0,0 +1,31 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { UtilitiesDto } from './shortening-url.dto'; + +describe('UtilitiesDto', () => { + const basePayload = { + credentialId: 'cred-id', + schemaId: 'schema-id', + credDefId: 'cred-def-id', + invitationUrl: 'https://example.com/invite' + }; + + it('should pass validation when attributes contains at least one item', async () => { + const dto = plainToInstance(UtilitiesDto, { + ...basePayload, + attributes: [{ name: 'name', value: 'value' }] + }); + const errors = await validate(dto); + expect(errors.length).toBe(0); + }); + + it('should fail validation when attributes is an empty array', async () => { + const dto = plainToInstance(UtilitiesDto, { + ...basePayload, + attributes: [] + }); + const errors = await validate(dto); + const attributesError = errors.find((error) => 'attributes' === error.property); + expect(attributesError?.constraints?.arrayNotEmpty).toBe('attributes should not be empty'); + }); +}); diff --git a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts index c49e5a788..44bfc0da3 100644 --- a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts +++ b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts @@ -1,47 +1,45 @@ - import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsNotEmpty } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsNotEmpty } from 'class-validator'; @ApiExtraModels() - export class UtilitiesDto { + @ApiProperty() + @IsNotEmpty() + credentialId: string; - @ApiProperty() - @IsNotEmpty() - credentialId: string; - - @ApiProperty() - @IsNotEmpty() - schemaId: string; - - @ApiProperty() - @IsNotEmpty() - credDefId: string; + @ApiProperty() + @IsNotEmpty() + schemaId: string; - @ApiProperty() - @IsNotEmpty() - invitationUrl: string; + @ApiProperty() + @IsNotEmpty() + credDefId: string; - @ApiProperty({ - example: [ - { - name: 'name', - value: 'value' - } - ] - }) - @IsArray({ message: 'attributes must be a valid array' }) - @IsNotEmpty({ message: 'please provide valid attributes' }) - attributes: Attribute[]; + @ApiProperty() + @IsNotEmpty() + invitationUrl: string; + + @ApiProperty({ + example: [ + { + name: 'name', + value: 'value' + } + ] + }) + @IsArray({ message: 'attributes must be a valid array' }) + @ArrayNotEmpty({ message: 'attributes should not be empty' }) + @IsNotEmpty({ message: 'please provide valid attributes' }) + attributes: Attribute[]; } interface Attribute { - name: string; - value: string; - } + name: string; + value: string; +} - export class StoreObjectDto { +export class StoreObjectDto { @ApiProperty() @IsNotEmpty() data: string | object; -} \ No newline at end of file +} From 4f31b8dbbb2ad823b5f7f250b8962479d8304594 Mon Sep 17 00:00:00 2001 From: M Abdullah Date: Tue, 7 Jul 2026 17:16:02 +0500 Subject: [PATCH 21/58] match issuance pattern Signed-off-by: M Abdullah --- .../utilities/dtos/shortening-url.dto.spec.ts | 22 +++++++++++++++++++ .../src/utilities/dtos/shortening-url.dto.ts | 22 ++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts index 829480aae..49df14cae 100644 --- a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts +++ b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.spec.ts @@ -28,4 +28,26 @@ describe('UtilitiesDto', () => { const attributesError = errors.find((error) => 'attributes' === error.property); expect(attributesError?.constraints?.arrayNotEmpty).toBe('attributes should not be empty'); }); + + it('should fail validation when an attribute item is missing required fields', async () => { + const dto = plainToInstance(UtilitiesDto, { + ...basePayload, + attributes: [{}] + }); + const errors = await validate(dto); + const attributesError = errors.find((error) => 'attributes' === error.property); + expect(attributesError?.children?.length).toBeGreaterThan(0); + expect(errors.length).toBeGreaterThan(0); + }); + + it('should fail validation when an attribute name is not a string', async () => { + const dto = plainToInstance(UtilitiesDto, { + ...basePayload, + attributes: [{ name: 123, value: 'value' }] + }); + const errors = await validate(dto); + const attributesError = errors.find((error) => 'attributes' === error.property); + const nameError = attributesError?.children?.[0]?.children?.find((error) => 'name' === error.property); + expect(nameError?.constraints?.isString).toBeDefined(); + }); }); diff --git a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts index 44bfc0da3..29c3ab38f 100644 --- a/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts +++ b/apps/api-gateway/src/utilities/dtos/shortening-url.dto.ts @@ -1,5 +1,18 @@ import { ApiExtraModels, ApiProperty } from '@nestjs/swagger'; -import { ArrayNotEmpty, IsArray, IsNotEmpty } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; + +export class Attribute { + @ApiProperty() + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + value: string; +} @ApiExtraModels() export class UtilitiesDto { @@ -29,15 +42,12 @@ export class UtilitiesDto { }) @IsArray({ message: 'attributes must be a valid array' }) @ArrayNotEmpty({ message: 'attributes should not be empty' }) + @ValidateNested({ each: true }) + @Type(() => Attribute) @IsNotEmpty({ message: 'please provide valid attributes' }) attributes: Attribute[]; } -interface Attribute { - name: string; - value: string; -} - export class StoreObjectDto { @ApiProperty() @IsNotEmpty() From d6a48b5ae8465b15ff67b760268492a14dd54c22 Mon Sep 17 00:00:00 2001 From: Tipu_Singh Date: Wed, 15 Jul 2026 11:58:27 +0530 Subject: [PATCH 22/58] fix: dco issue (#1698) * empty attribute response Signed-off-by: M Abdullah Signed-off-by: Tipu_Singh * match issuance pattern Signed-off-by: M Abdullah Signed-off-by: Tipu_Singh --------- Signed-off-by: M Abdullah Signed-off-by: Tipu_Singh Co-authored-by: M Abdullah From 9c8c50c17cbf3ca2ebeb6fa163b961fc16f24334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:19:29 +0000 Subject: [PATCH 23/58] chore(deps): bump body-parser from 1.20.5 to 1.20.6 Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.5 to 1.20.6. - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.5...1.20.6) --- updated-dependencies: - dependency-name: body-parser dependency-version: 1.20.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 94 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 67 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 79f2dbe33..ca57aebfc 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "axios": "^1.16.0", "bcrypt": "^6.0.0", "blob-stream": "^0.1.3", - "body-parser": "^1.20.4", + "body-parser": "^1.20.6", "buffer": "^6.0.3", "bull": "^4.16.5", "cache-manager": "^5.7.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0008ff7dd..e9c35dc3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -195,8 +195,8 @@ importers: specifier: ^0.1.3 version: 0.1.3 body-parser: - specifier: ^1.20.4 - version: 1.20.5 + specifier: ^1.20.6 + version: 1.20.6 buffer: specifier: ^6.0.3 version: 6.0.3 @@ -2749,12 +2749,12 @@ packages: bn.js@4.12.3: resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} - body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} + body-parser@1.20.6: + resolution: {integrity: sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} engines: {node: '>=18'} bowser@2.14.1: @@ -3035,6 +3035,10 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -4095,8 +4099,8 @@ packages: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} idtoken-verifier@2.2.4: @@ -4921,6 +4925,7 @@ packages: nats@2.29.3: resolution: {integrity: sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==} engines: {node: '>= 14.0.0'} + deprecated: Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5469,6 +5474,10 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + qs@6.5.5: resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} engines: {node: '>=0.6'} @@ -5750,6 +5759,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -6193,6 +6206,10 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -7457,7 +7474,7 @@ snapshots: '@inquirer/external-editor@1.0.3(@types/node@20.19.39)': dependencies: chardet: 2.1.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 optionalDependencies: '@types/node': 20.19.39 @@ -9596,7 +9613,7 @@ snapshots: bn.js@4.12.3: {} - body-parser@1.20.5: + body-parser@1.20.6: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -9606,24 +9623,24 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.1 + qs: 6.15.3 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: - supports-color - body-parser@2.2.2: + body-parser@2.3.0: dependencies: bytes: 3.1.2 - content-type: 1.0.5 + content-type: 2.0.0 debug: 4.4.3 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.1 + qs: 6.15.3 raw-body: 3.0.2 - type-is: 2.0.1 + type-is: 2.1.0 transitivePeerDependencies: - supports-color @@ -9907,6 +9924,8 @@ snapshots: content-type@1.0.5: {} + content-type@2.0.0: {} + convert-source-map@2.0.0: {} cookie-parser@1.4.7: @@ -10066,7 +10085,7 @@ snapshots: object-keys: 1.1.1 object.assign: 4.1.7 regexp.prototype.flags: 1.5.4 - side-channel: 1.1.0 + side-channel: 1.1.1 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 which-typed-array: 1.1.20 @@ -10598,7 +10617,7 @@ snapshots: express@5.2.1: dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.3.0 content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 @@ -10642,7 +10661,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.3.4 + debug: 4.4.3 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -11004,7 +11023,7 @@ snapshots: dependencies: basic-ftp: 5.3.1 data-uri-to-buffer: 6.0.2 - debug: 4.3.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11230,7 +11249,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11270,7 +11289,7 @@ snapshots: dependencies: safer-buffer: 2.1.2 - iconv-lite@0.7.2: + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -11321,7 +11340,7 @@ snapshots: dependencies: es-errors: 1.3.0 hasown: 2.0.3 - side-channel: 1.1.0 + side-channel: 1.1.1 ioredis@5.10.1: dependencies: @@ -12504,7 +12523,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -12761,7 +12780,7 @@ snapshots: proxy-agent@6.3.0: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -12774,7 +12793,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -12884,6 +12903,11 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + qs@6.5.5: optional: true @@ -12906,7 +12930,7 @@ snapshots: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.7.2 + iconv-lite: 0.7.3 unpipe: 1.0.0 react-is@18.3.1: {} @@ -13234,6 +13258,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -13293,7 +13325,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 socks: 2.8.8 transitivePeerDependencies: - supports-color @@ -13739,6 +13771,12 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 From 5fee42ccdd396404c39143cf7acf181663decc42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:49:42 +0000 Subject: [PATCH 24/58] chore(deps): bump typeorm from 0.3.28 to 0.3.31 Bumps [typeorm](https://github.com/typeorm/typeorm) from 0.3.28 to 0.3.31. - [Release notes](https://github.com/typeorm/typeorm/releases) - [Changelog](https://github.com/typeorm/typeorm/blob/0.3.31/CHANGELOG.md) - [Commits](https://github.com/typeorm/typeorm/compare/0.3.28...0.3.31) --- updated-dependencies: - dependency-name: typeorm dependency-version: 0.3.31 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 88 +++++++++++++++++++++++++++++++------------------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 79f2dbe33..d35f0086e 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "rxjs": "^7.8.2", "socket.io-client": "^4.8.3", "swagger-ui-express": "^5.0.1", - "typeorm": "^0.3.28", + "typeorm": "^0.3.31", "unzipper": "^0.12.3", "uuid": "^9.0.1", "validator": "^13.15.35", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0008ff7dd..2aacf3a20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,7 +121,7 @@ importers: version: 11.4.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) '@nestjs/typeorm': specifier: 'catalog:' - version: 11.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) + version: 11.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) '@nestjs/websockets': specifier: 'catalog:' version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -295,7 +295,7 @@ importers: version: 1.0.9 nestjs-typeorm-paginate: specifier: 'catalog:' - version: 4.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) + version: 4.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) node-html-to-image: specifier: ^4.0.0 version: 4.0.0 @@ -363,8 +363,8 @@ importers: specifier: ^5.0.1 version: 5.0.1(express@5.2.1) typeorm: - specifier: ^0.3.28 - version: 0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) + specifier: ^0.3.31 + version: 0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) unzipper: specifier: ^0.12.3 version: 0.12.3 @@ -2506,8 +2506,8 @@ packages: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} engines: {node: '>=14'} anymatch@3.1.3: @@ -2766,6 +2766,9 @@ packages: brace-expansion@2.1.0: resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + brace-expansion@2.1.2: + resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -3142,8 +3145,8 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -4921,6 +4924,7 @@ packages: nats@2.29.3: resolution: {integrity: sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==} engines: {node: '>= 14.0.0'} + deprecated: Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -6212,8 +6216,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typeorm@0.3.28: - resolution: {integrity: sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==} + typeorm@0.3.31: + resolution: {integrity: sha512-6u9EFtdLBgHjnPm78NStVeM+I/1MolTzKykDDcydzKUkh6E++YS6XViU/fePJbvDvEGU4Xq34KOM/CLeer9I2A==} engines: {node: '>=16.13.0'} hasBin: true peerDependencies: @@ -6224,13 +6228,13 @@ packages: mongodb: ^5.8.0 || ^6.0.0 mssql: ^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0 mysql2: ^2.2.5 || ^3.0.1 - oracledb: ^6.3.0 + oracledb: ^6.3.0 || ^7.0.0 pg: ^8.5.1 pg-native: ^3.0.0 pg-query-stream: ^4.0.0 redis: ^3.1.1 || ^4.0.0 || ^5.0.14 sql.js: ^1.4.0 - sqlite3: ^5.0.3 + sqlite3: ^5.0.3 || ^6.0.0 ts-node: ^10.7.0 typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 peerDependenciesMeta: @@ -6621,6 +6625,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yauzl@2.10.0: resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} @@ -7384,7 +7392,7 @@ snapshots: lodash.camelcase: 4.3.0 long: 5.3.2 protobufjs: 7.5.6 - yargs: 17.7.2 + yargs: 17.7.3 optional: true '@grpc/proto-loader@0.8.0': @@ -7392,7 +7400,7 @@ snapshots: lodash.camelcase: 4.3.0 long: 5.3.2 protobufjs: 7.5.6 - yargs: 17.7.2 + yargs: 17.7.3 '@humanwhocodes/config-array@0.13.0': dependencies: @@ -7993,13 +8001,13 @@ snapshots: '@nestjs/microservices': 11.1.19(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) - '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))': dependencies: '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 rxjs: 7.8.2 - typeorm: 0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) + typeorm: 0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) '@nestjs/websockets@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: @@ -9304,7 +9312,7 @@ snapshots: ansis@4.1.0: {} - ansis@4.2.0: {} + ansis@4.3.1: {} anymatch@3.1.3: dependencies: @@ -9638,6 +9646,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.1.2: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -10028,7 +10040,7 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - dayjs@1.11.20: {} + dayjs@1.11.21: {} debug@2.6.9: dependencies: @@ -10642,7 +10654,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.3.4 + debug: 4.4.3 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -11004,7 +11016,7 @@ snapshots: dependencies: basic-ftp: 5.3.1 data-uri-to-buffer: 6.0.2 - debug: 4.3.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11230,7 +11242,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11586,7 +11598,7 @@ snapshots: jest-config: 29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) jest-util: 29.7.0 jest-validate: 29.7.0 - yargs: 17.7.2 + yargs: 17.7.3 transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -12209,7 +12221,7 @@ snapshots: minimatch@9.0.9: dependencies: - brace-expansion: 2.1.0 + brace-expansion: 2.1.2 minimist@1.2.8: {} @@ -12298,10 +12310,10 @@ snapshots: nestjs-supabase-auth@1.0.9: {} - nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))): + nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))): dependencies: '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - typeorm: 0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) + typeorm: 0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) netmask@2.1.1: {} @@ -12504,7 +12516,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -12761,7 +12773,7 @@ snapshots: proxy-agent@6.3.0: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -12774,7 +12786,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -13293,7 +13305,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.3.4 + debug: 4.4.3 socks: 2.8.8 transitivePeerDependencies: - supports-color @@ -13774,13 +13786,13 @@ snapshots: typedarray@0.0.6: {} - typeorm@0.3.28(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): + typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)): dependencies: '@sqltools/formatter': 1.2.5 - ansis: 4.2.0 + ansis: 4.3.1 app-root-path: 3.1.0 buffer: 6.0.3 - dayjs: 1.11.20 + dayjs: 1.11.21 debug: 4.4.3 dedent: 1.7.2 dotenv: 16.6.1 @@ -13790,7 +13802,7 @@ snapshots: sql-highlight: 6.1.0 tslib: 2.8.1 uuid: 11.1.1 - yargs: 17.7.2 + yargs: 17.7.3 optionalDependencies: ioredis: 5.10.1 pg: 8.20.0 @@ -14176,6 +14188,16 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 From 6fbc9b186a0c0f06d0914368b55a20133d0026ef Mon Sep 17 00:00:00 2001 From: sujitaw Date: Mon, 27 Jul 2026 16:59:33 +0530 Subject: [PATCH 25/58] feat/docker files for open bao Signed-off-by: sujitaw --- config.hcl | 10 ++++++++++ docker-compose.openbao.yml | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 config.hcl create mode 100644 docker-compose.openbao.yml diff --git a/config.hcl b/config.hcl new file mode 100644 index 000000000..db00e3ebb --- /dev/null +++ b/config.hcl @@ -0,0 +1,10 @@ +storage "file" { + path = "/bao/data" +} + +listener "tcp" { + address = "0.0.0.0:8200" + tls_disable = 1 # In true production, configure actual TLS certificates here +} + +ui = true diff --git a/docker-compose.openbao.yml b/docker-compose.openbao.yml new file mode 100644 index 000000000..a2b119a98 --- /dev/null +++ b/docker-compose.openbao.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + openbao: + image: openbao/openbao:latest + container_name: openbao + # Runs as openbao user (UID 100) instead of root + user: "100:100" + ports: + - "8200:8200" + environment: + - BAO_ADDR=http://127.0.0.1:8200 + volumes: + - openbao_data:/bao/data + - ./config/bao.hcl:/bao/config/bao.hcl:ro + cap_add: + - IPC_LOCK + # Expressly point to the HCL config file + command: ["server", "-config=/bao/config/bao.hcl"] + restart: unless-stopped + +volumes: + openbao_data: \ No newline at end of file From f9ee32d174d0182b706b3a9fa41b75c95050d547 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 03:34:56 +0000 Subject: [PATCH 26/58] build: bump redis from 6.2-alpine to 8.0-alpine Bumps redis from 6.2-alpine to 8.0-alpine. --- updated-dependencies: - dependency-name: redis dependency-version: 8.0-alpine dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- docker-compose-dev.yml | 2 +- docker-compose.redis.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 59d392828..2aa4b0478 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -12,7 +12,7 @@ services: # volumes: # - ./nats-server.conf:/nats-server.conf # Mount the config file redis: - image: redis:6.2-alpine + image: redis:8.0-alpine restart: always ports: - '6379:6379' diff --git a/docker-compose.redis.yml b/docker-compose.redis.yml index c17394ea1..9b2389a0d 100644 --- a/docker-compose.redis.yml +++ b/docker-compose.redis.yml @@ -1,7 +1,7 @@ version: '3' services: redis: - image: redis:6.2-alpine + image: redis:8.0-alpine restart: always ports: - '6379:6379' diff --git a/docker-compose.yml b/docker-compose.yml index 991af677e..38996d977 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: - ./nats-server.conf:/nats-server.conf:ro redis: container_name: credebl-redis - image: redis:6.2-alpine + image: redis:8.0-alpine restart: always ports: - '6379:6379' From 74ef5f1983e1f7e966512e590b412a109eb50596 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:52:15 +0000 Subject: [PATCH 27/58] build: bump protobufjs from 7.5.6 to 7.6.5 Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.5.6 to 7.6.5. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.6.5/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.5.6...protobufjs-v7.6.5) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 7.6.5 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 55 +++++++++++++++++++++++--------------------------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 697d33844..19c36a1dd 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "winston": "^3.19.0", "winston-daily-rotate-file": "^5.0.0", "xml-js": "^1.6.11", - "protobufjs": "^7.5.6", + "protobufjs": "^7.6.5", "handlebars": "^4.7.9", "form-data": "^4.0.5" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c15e52875..98500dea4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -330,8 +330,8 @@ importers: specifier: ^8.18.0 version: 8.20.0 protobufjs: - specifier: ^7.5.6 - version: 7.5.6 + specifier: ^7.6.5 + version: 7.6.5 puppeteer: specifier: ^21.11.0 version: 21.11.0(typescript@5.9.3) @@ -1778,26 +1778,23 @@ packages: '@protobufjs/codegen@2.0.5': resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - '@protobufjs/inquire@1.1.1': - resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} - '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} '@protobufjs/pool@1.1.0': resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} '@puppeteer/browsers@1.5.0': resolution: {integrity: sha512-za318PweGINh5LnHSph7C4xhs0tmRjCD8EPpzcKlw4nzSPhnULj+LTG3+TGefZvW1ti5gjw2JkdQvQsivBeZlg==} @@ -3106,6 +3103,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cron@4.4.0: resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} @@ -3120,6 +3118,7 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained. crypto-random-string@5.0.0: resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} @@ -5405,8 +5404,8 @@ packages: resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} engines: {node: '>=14.0.0'} - protobufjs@7.5.6: - resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -7358,7 +7357,7 @@ snapshots: fast-deep-equal: 3.1.3 functional-red-black-tree: 1.0.1 google-gax: 4.6.1 - protobufjs: 7.5.6 + protobufjs: 7.6.5 transitivePeerDependencies: - encoding - supports-color @@ -7407,7 +7406,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.6 + protobufjs: 7.6.5 yargs: 17.7.3 optional: true @@ -7415,7 +7414,7 @@ snapshots: dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.6 + protobufjs: 7.6.5 yargs: 17.7.3 '@humanwhocodes/config-array@0.13.0': @@ -8241,7 +8240,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) - protobufjs: 7.5.6 + protobufjs: 7.6.5 '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.1)': dependencies: @@ -8360,22 +8359,19 @@ snapshots: '@protobufjs/codegen@2.0.5': {} - '@protobufjs/eventemitter@1.1.0': {} + '@protobufjs/eventemitter@1.1.1': {} - '@protobufjs/fetch@1.1.0': + '@protobufjs/fetch@1.1.1': dependencies: '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.1 '@protobufjs/float@1.0.2': {} - '@protobufjs/inquire@1.1.1': {} - '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} - '@protobufjs/utf8@1.1.1': {} + '@protobufjs/utf8@1.1.2': {} '@puppeteer/browsers@1.5.0': dependencies: @@ -11140,7 +11136,7 @@ snapshots: node-fetch: 2.7.0 object-hash: 3.0.0 proto3-json-serializer: 2.0.2 - protobufjs: 7.5.6 + protobufjs: 7.6.5 retry-request: 7.0.2 uuid: 9.0.1 transitivePeerDependencies: @@ -12765,21 +12761,20 @@ snapshots: proto3-json-serializer@2.0.2: dependencies: - protobufjs: 7.5.6 + protobufjs: 7.6.5 optional: true - protobufjs@7.5.6: + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.1 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 + '@protobufjs/utf8': 1.1.2 '@types/node': 20.19.39 long: 5.3.2 From f501cf5be342ccf8bc3ad446e767f6d871d12d93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:52:30 +0000 Subject: [PATCH 28/58] build: bump axios from 1.16.0 to 1.18.0 Bumps [axios](https://github.com/axios/axios) from 1.16.0 to 1.18.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.16.0...v1.18.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 43 +++++++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 697d33844..17c7cfb64 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "async-retry": "^1.3.3", "auth0-js": "^9.30.1", "aws-sdk": "^2.1693.0", - "axios": "^1.16.0", + "axios": "^1.18.0", "bcrypt": "^6.0.0", "blob-stream": "^0.1.3", "body-parser": "^1.20.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c15e52875..3d8071789 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,7 +82,7 @@ importers: version: 3.1044.0 '@nestjs/axios': specifier: 'catalog:' - version: 4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2) + version: 4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2) '@nestjs/bull': specifier: 'catalog:' version: 11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(bull@4.16.5) @@ -186,8 +186,8 @@ importers: specifier: ^2.1693.0 version: 2.1693.0 axios: - specifier: ^1.16.0 - version: 1.16.0 + specifier: ^1.18.0 + version: 1.18.0 bcrypt: specifier: ^6.0.0 version: 6.0.0 @@ -488,7 +488,7 @@ importers: dependencies: '@nestjs/axios': specifier: 'catalog:' - version: 4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2) + version: 4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2) '@nestjs/cache-manager': specifier: 'catalog:' version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) @@ -2616,8 +2616,8 @@ packages: axios@0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} - axios@1.16.0: - resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + axios@1.18.0: + resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==} b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} @@ -3106,6 +3106,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cron@4.4.0: resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} @@ -3120,6 +3121,7 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained. crypto-random-string@5.0.0: resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} @@ -3773,8 +3775,8 @@ packages: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} engines: {node: '>= 0.12'} - form-data@2.5.5: - resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + form-data@2.5.6: + resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==} engines: {node: '>= 0.12'} form-data@4.0.5: @@ -4031,6 +4033,10 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + helmet@8.1.0: resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} engines: {node: '>=18.0.0'} @@ -7808,10 +7814,10 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true - '@nestjs/axios@4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.16.0)(rxjs@7.8.2)': + '@nestjs/axios@4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - axios: 1.16.0 + axios: 1.18.0 rxjs: 7.8.2 '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)': @@ -8949,7 +8955,7 @@ snapshots: '@types/caseless': 0.12.5 '@types/node': 20.19.39 '@types/tough-cookie': 4.0.5 - form-data: 2.5.5 + form-data: 2.5.6 optional: true '@types/retry@0.12.5': {} @@ -9251,7 +9257,6 @@ snapshots: debug: 4.4.3 transitivePeerDependencies: - supports-color - optional: true agent-base@7.1.4: {} @@ -9477,13 +9482,15 @@ snapshots: transitivePeerDependencies: - debug - axios@1.16.0: + axios@1.18.0: dependencies: follow-redirects: 1.16.0 form-data: 4.0.5 + https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: - debug + - supports-color b4a@1.8.1: {} @@ -10872,12 +10879,12 @@ snapshots: mime-types: 2.1.35 optional: true - form-data@2.5.5: + form-data@2.5.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.3 + hasown: 2.0.4 mime-types: 2.1.35 safe-buffer: 5.2.1 optional: true @@ -11223,6 +11230,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + optional: true + helmet@8.1.0: {} html-entities@2.6.0: @@ -11279,7 +11291,6 @@ snapshots: debug: 4.4.3 transitivePeerDependencies: - supports-color - optional: true https-proxy-agent@7.0.6: dependencies: From ab6ee20851b0077789ed89fea20724506d966bcd Mon Sep 17 00:00:00 2001 From: Ajay Jadhav Date: Fri, 7 Aug 2026 01:37:35 +0530 Subject: [PATCH 29/58] test/docs: add OpenBao tests, provisioning script, and docs - add unit tests for OpenBao provider, secrets loader, secret loader util, TTL cache, and email/S3 credential sourcing (50 tests) - add openbao-init.sh: idempotent provisioning (init/unseal, KV v2, AppRole, credebl role) - fix docker-compose.openbao.yml + config.hcl storage volume ownership - add ENABLE_BAO/SECRETS_PROVIDER/BAO_* vars to .env.demo - add README 'OpenBao Secret Storage' guide and openspec Signed-off-by: Ajay Jadhav --- .env.demo | 18 +- README.md | 48 ++++ config.hcl | 2 +- docker-compose.openbao.yml | 9 +- libs/common/src/resend-helper-file.spec.ts | 81 ++++++ libs/common/src/send-grid-helper-file.spec.ts | 73 ++++++ libs/common/src/smtp-helper-file.spec.ts | 122 +++++++++ .../src/utils/secretLoader.util.spec.ts | 86 +++++++ libs/common/src/utils/ttl-cache.util.spec.ts | 68 +++++ .../secret-storage/openbao.provider.spec.ts | 130 ++++++++++ .../src/secret-storage/secrets-loader.spec.ts | 104 ++++++++ .../src/providers/s3-storage.provider.spec.ts | 108 ++++++++ openbao-init.sh | 164 ++++++++++++ .../openbao-secret-storage.spec.md | 235 ++++++++++++++++++ package.json | 6 +- 15 files changed, 1240 insertions(+), 14 deletions(-) create mode 100644 libs/common/src/resend-helper-file.spec.ts create mode 100644 libs/common/src/send-grid-helper-file.spec.ts create mode 100644 libs/common/src/smtp-helper-file.spec.ts create mode 100644 libs/common/src/utils/secretLoader.util.spec.ts create mode 100644 libs/common/src/utils/ttl-cache.util.spec.ts create mode 100644 libs/config/src/secret-storage/openbao.provider.spec.ts create mode 100644 libs/config/src/secret-storage/secrets-loader.spec.ts create mode 100644 libs/storage/src/providers/s3-storage.provider.spec.ts create mode 100755 openbao-init.sh create mode 100644 openspec/specs/openbao-secret-storage/openbao-secret-storage.spec.md diff --git a/.env.demo b/.env.demo index 83d2fe9e4..102d61508 100644 --- a/.env.demo +++ b/.env.demo @@ -382,8 +382,18 @@ ADMIN_KEYCLOAK_SECRET= # Add in case the keycloak id of the old platform client is changed and want to update all the users in database with the new admin client PLATFORM_ADMIN_OLD_CLIENT_ID= -BAO_URL= -BAO_SECRET_PATH= -BAO_ROLE_ID= -BAO_SECRET_ID= +# --- OpenBao secret storage (PR #1679) --- +# Enable remote secrets fetching via OpenBao. Set to false to fall back to +# plain environment variables (default local behaviour). +ENABLE_BAO=true +# Secret provider: only 'openbao' is supported today +SECRETS_PROVIDER=openbao +# OpenBao server URL (docker-compose.openbao.yml maps 8200) +BAO_URL=http://127.0.0.1:8200 +# Default KV v2 path fetched at boot and injected into process.env +BAO_SECRET_PATH=secret/data/credebl_resend_api_key +# AppRole credentials. Generate your own via: ./openbao-init.sh +# (values below are for the local 'openbao' container created by docker-compose.openbao.yml) +BAO_ROLE_ID=ca2a350b-1871-0851-8557-42b36aa9c623 +BAO_SECRET_ID=3f0d8d45-c232-f5c1-2984-fe58e5144be8 HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=false diff --git a/README.md b/README.md index 20f15ecff..acf763699 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,54 @@ The `docker-compose.yml` file is available in the root folder. docker-compose up ``` +## OpenBao Secret Storage (Optional) + +The platform can source the credentials used by email and file-storage integrations from an [OpenBao](https://openbao.org) server instead of plain environment variables. This is opt-in and disabled by default. + +### • Run OpenBao using Docker Compose + +```bash +docker compose -f docker-compose.openbao.yml up -d +``` + +This starts an OpenBao server on `http://127.0.0.1:8200` with file-backed storage persisted on a named volume. Server settings live in `config.hcl`. + +### • Provision the server + +OpenBao starts sealed and uninitialized. Run the one-shot provisioning script (idempotent — safe to re-run): + +```bash +./openbao-init.sh +``` + +The script initializes and unseals the server, enables the KV v2 secrets engine at `secret/`, enables the AppRole auth method, creates a `credebl` role scoped to the `credebl_*` secret paths, stores the secret values found in the environment (e.g. `RESEND_API_KEY`, `SMTP_HOST`, `AWS_ACCESS_KEY`), and prints the values to copy into your env file: + +```bash +BAO_URL=http://127.0.0.1:8200 +BAO_SECRET_PATH=secret/data/credebl_resend_api_key +BAO_ROLE_ID= +BAO_SECRET_ID= +``` + +Keep the printed `BAO_UNSEAL_KEY` and `BAO_ROOT_TOKEN` safe — they are shown only once. On later runs, pass `BAO_ROOT_TOKEN` (and `BAO_SECRET_ID` to re-print it). + +### • Configure the platform + +Add the following to your `.env` (values are already present as placeholders in `.env.demo`): + +```bash +ENABLE_BAO=true +SECRETS_PROVIDER=openbao +BAO_URL=http://127.0.0.1:8200 +BAO_SECRET_PATH=secret/data/credebl_resend_api_key +BAO_ROLE_ID= +BAO_SECRET_ID= +``` + +At startup every microservice authenticates to OpenBao via AppRole, fetches the secrets at `BAO_SECRET_PATH`, and injects them into `process.env` before the NATS listener starts. Email and S3 integrations re-fetch their own credential paths on demand (e.g. `secret/data/credebl_smtp_config`, `secret/data/credebl_aws_keys`) with a 10-minute TTL cache. + +Set `ENABLE_BAO=false` (or omit it) to fall back to local environment variables. + ## Run CREDEBL Microservices ### • Install Dependencies diff --git a/config.hcl b/config.hcl index db00e3ebb..4f0774dfd 100644 --- a/config.hcl +++ b/config.hcl @@ -1,5 +1,5 @@ storage "file" { - path = "/bao/data" + path = "/openbao/file" } listener "tcp" { diff --git a/docker-compose.openbao.yml b/docker-compose.openbao.yml index a2b119a98..f9e44b5f1 100644 --- a/docker-compose.openbao.yml +++ b/docker-compose.openbao.yml @@ -1,18 +1,15 @@ -version: '3.8' - services: openbao: image: openbao/openbao:latest container_name: openbao - # Runs as openbao user (UID 100) instead of root - user: "100:100" + # Entrypoint runs as root, chowns the data volume, then drops to openbao user ports: - "8200:8200" environment: - BAO_ADDR=http://127.0.0.1:8200 volumes: - - openbao_data:/bao/data - - ./config/bao.hcl:/bao/config/bao.hcl:ro + - openbao_data:/openbao/file + - ./config.hcl:/bao/config/bao.hcl:ro cap_add: - IPC_LOCK # Expressly point to the HCL config file diff --git a/libs/common/src/resend-helper-file.spec.ts b/libs/common/src/resend-helper-file.spec.ts new file mode 100644 index 000000000..0eb374dde --- /dev/null +++ b/libs/common/src/resend-helper-file.spec.ts @@ -0,0 +1,81 @@ +import { sendWithResend } from './resend-helper-file'; +import { fetchSecrets } from './utils/secretLoader.util'; + +jest.mock('resend', () => ({ + Resend: jest.fn() +})); +jest.mock('./utils/secretLoader.util', () => ({ + fetchSecrets: jest.fn() +})); + +import { Resend } from 'resend'; +import { EmailDto } from './dtos/email.dto'; + +const MockedResend = Resend as unknown as jest.Mock; +const mockedFetchSecrets = fetchSecrets as jest.MockedFunction; + +const emailDto: EmailDto = { + emailFrom: 'from@credebl.id', + emailTo: ['to@example.com'], + emailSubject: 'subject', + emailText: 'text', + emailHtml: '

html

' +}; + +describe('sendWithResend', () => { + const originalEnv = { ...process.env }; + let sendMock: jest.Mock; + + beforeEach(() => { + delete process.env.RESEND_API_KEY; + sendMock = jest.fn().mockResolvedValue({ data: { id: 'email-1' } }); + MockedResend.mockImplementation(() => ({ emails: { send: sendMock } })); + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('sends an email using the provider-sourced API key', async () => { + mockedFetchSecrets.mockResolvedValue({ RESEND_API_KEY: 'resend-key' }); + + await expect(sendWithResend(emailDto)).resolves.toBe(true); + + expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_resend_api_key'); + expect(MockedResend).toHaveBeenCalledWith('resend-key'); + expect(sendMock).toHaveBeenCalledWith( + expect.objectContaining({ to: emailDto.emailTo, subject: emailDto.emailSubject }) + ); + }); + + it('falls back to the environment variable when no secret is returned', async () => { + mockedFetchSecrets.mockResolvedValue({}); + process.env.RESEND_API_KEY = 'env-key'; + + await expect(sendWithResend(emailDto)).resolves.toBe(true); + + expect(MockedResend).toHaveBeenCalledWith('env-key'); + }); + + it('returns false when the API key is missing', async () => { + mockedFetchSecrets.mockResolvedValue({}); + + await expect(sendWithResend(emailDto)).resolves.toBe(false); + expect(MockedResend).not.toHaveBeenCalled(); + }); + + it('returns false when the send response has no id', async () => { + mockedFetchSecrets.mockResolvedValue({ RESEND_API_KEY: 'resend-key' }); + sendMock.mockResolvedValue({ data: {} }); + + await expect(sendWithResend(emailDto)).resolves.toBe(false); + }); + + it('returns false when the send rejects', async () => { + mockedFetchSecrets.mockResolvedValue({ RESEND_API_KEY: 'resend-key' }); + sendMock.mockRejectedValue(new Error('rate limited')); + + await expect(sendWithResend(emailDto)).resolves.toBe(false); + }); +}); diff --git a/libs/common/src/send-grid-helper-file.spec.ts b/libs/common/src/send-grid-helper-file.spec.ts new file mode 100644 index 000000000..cbfb52789 --- /dev/null +++ b/libs/common/src/send-grid-helper-file.spec.ts @@ -0,0 +1,73 @@ +import { sendWithSendGrid } from './send-grid-helper-file'; +import { fetchSecrets } from './utils/secretLoader.util'; + +jest.mock('@sendgrid/mail', () => ({ + setApiKey: jest.fn(), + send: jest.fn() +})); +jest.mock('./utils/secretLoader.util', () => ({ + fetchSecrets: jest.fn() +})); + +import * as sendgrid from '@sendgrid/mail'; +import { EmailDto } from './dtos/email.dto'; + +const mockedSetApiKey = sendgrid.setApiKey as jest.Mock; +const mockedSend = sendgrid.send as jest.Mock; +const mockedFetchSecrets = fetchSecrets as jest.MockedFunction; + +const emailDto: EmailDto = { + emailFrom: 'from@credebl.id', + emailTo: ['to@example.com'], + emailSubject: 'subject', + emailText: 'text', + emailHtml: '

html

' +}; + +describe('sendWithSendGrid', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + delete process.env.SENDGRID_API_KEY; + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('sends an email using the provider-sourced API key', async () => { + mockedFetchSecrets.mockResolvedValue({ SENDGRID_API_KEY: 'sg-key' }); + mockedSend.mockResolvedValue(true); + + await expect(sendWithSendGrid(emailDto)).resolves.toBe(true); + + expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_sendgrid_api_key'); + expect(mockedSetApiKey).toHaveBeenCalledWith('sg-key'); + expect(mockedSend).toHaveBeenCalledWith(expect.objectContaining({ to: emailDto.emailTo })); + }); + + it('falls back to the environment variable when no secret is returned', async () => { + mockedFetchSecrets.mockResolvedValue({}); + process.env.SENDGRID_API_KEY = 'env-key'; + mockedSend.mockResolvedValue(true); + + await expect(sendWithSendGrid(emailDto)).resolves.toBe(true); + + expect(mockedSetApiKey).toHaveBeenCalledWith('env-key'); + }); + + it('returns false when the API key is missing', async () => { + mockedFetchSecrets.mockResolvedValue({}); + + await expect(sendWithSendGrid(emailDto)).resolves.toBe(false); + expect(mockedSetApiKey).not.toHaveBeenCalled(); + }); + + it('returns false when send rejects', async () => { + mockedFetchSecrets.mockResolvedValue({ SENDGRID_API_KEY: 'sg-key' }); + mockedSend.mockRejectedValue(new Error('rate limited')); + + await expect(sendWithSendGrid(emailDto)).resolves.toBe(false); + }); +}); diff --git a/libs/common/src/smtp-helper-file.spec.ts b/libs/common/src/smtp-helper-file.spec.ts new file mode 100644 index 000000000..a89b948d9 --- /dev/null +++ b/libs/common/src/smtp-helper-file.spec.ts @@ -0,0 +1,122 @@ +import { sendWithSMTP } from './smtp-helper-file'; +import { fetchSecrets } from './utils/secretLoader.util'; + +jest.mock('nodemailer', () => ({ + createTransport: jest.fn() +})); +jest.mock('./utils/secretLoader.util', () => ({ + fetchSecrets: jest.fn() +})); + +import * as nodemailer from 'nodemailer'; +import { EmailDto } from './dtos/email.dto'; + +const mockedCreateTransport = nodemailer.createTransport as jest.Mock; +const mockedFetchSecrets = fetchSecrets as jest.MockedFunction; + +const emailDto: EmailDto = { + emailFrom: 'from@credebl.id', + emailTo: ['to@example.com'], + emailSubject: 'subject', + emailText: 'text', + emailHtml: '

html

' +}; + +describe('sendWithSMTP', () => { + const originalEnv = { ...process.env }; + let sendMail: jest.Mock; + + beforeEach(() => { + delete process.env.SMTP_HOST; + delete process.env.SMTP_PORT; + delete process.env.SMTP_USER; + delete process.env.SMTP_PASS; + sendMail = jest.fn().mockResolvedValue({ messageId: 'm1' }); + mockedCreateTransport.mockReturnValue({ sendMail }); + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('sends an email using provider-sourced SMTP configuration', async () => { + mockedFetchSecrets.mockResolvedValue({ + SMTP_HOST: 'smtp.example.com', + SMTP_PORT: '587', + SMTP_USER: 'user', + SMTP_PASS: 'pass' + }); + + await expect(sendWithSMTP(emailDto)).resolves.toBe(true); + + expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_smtp_config'); + expect(mockedCreateTransport).toHaveBeenCalledWith( + expect.objectContaining({ + host: 'smtp.example.com', + port: 587, + secure: false, + requireTLS: true, + auth: { user: 'user', pass: 'pass' } + }) + ); + expect(sendMail).toHaveBeenCalledWith(expect.objectContaining({ from: emailDto.emailFrom })); + }); + + it('sets secure mode for port 465', async () => { + mockedFetchSecrets.mockResolvedValue({ + SMTP_HOST: 'smtp.example.com', + SMTP_PORT: '465', + SMTP_USER: 'user', + SMTP_PASS: 'pass' + }); + + await sendWithSMTP(emailDto); + + expect(mockedCreateTransport).toHaveBeenCalledWith(expect.objectContaining({ secure: true, requireTLS: false })); + }); + + it('falls back to environment variables when no secrets are returned', async () => { + mockedFetchSecrets.mockResolvedValue({}); + process.env.SMTP_HOST = 'smtp.env.com'; + process.env.SMTP_PORT = '587'; + process.env.SMTP_USER = 'env-user'; + process.env.SMTP_PASS = 'env-pass'; + + await expect(sendWithSMTP(emailDto)).resolves.toBe(true); + + expect(mockedCreateTransport).toHaveBeenCalledWith( + expect.objectContaining({ host: 'smtp.env.com', auth: { user: 'env-user', pass: 'env-pass' } }) + ); + }); + + it('returns false when required SMTP configuration is missing', async () => { + mockedFetchSecrets.mockResolvedValue({}); + + await expect(sendWithSMTP(emailDto)).resolves.toBe(false); + expect(sendMail).not.toHaveBeenCalled(); + }); + + it('returns false when the SMTP port is invalid', async () => { + mockedFetchSecrets.mockResolvedValue({ + SMTP_HOST: 'smtp.example.com', + SMTP_PORT: 'not-a-number', + SMTP_USER: 'user', + SMTP_PASS: 'pass' + }); + + await expect(sendWithSMTP(emailDto)).resolves.toBe(false); + }); + + it('returns false when sendMail rejects', async () => { + mockedFetchSecrets.mockResolvedValue({ + SMTP_HOST: 'smtp.example.com', + SMTP_PORT: '587', + SMTP_USER: 'user', + SMTP_PASS: 'pass' + }); + sendMail.mockRejectedValue(new Error('connection refused')); + + await expect(sendWithSMTP(emailDto)).resolves.toBe(false); + }); +}); diff --git a/libs/common/src/utils/secretLoader.util.spec.ts b/libs/common/src/utils/secretLoader.util.spec.ts new file mode 100644 index 000000000..7a9e0293c --- /dev/null +++ b/libs/common/src/utils/secretLoader.util.spec.ts @@ -0,0 +1,86 @@ +import { fetchSecrets } from './secretLoader.util'; +import { getSecretProvider } from 'libs/config/src/secret-storage/secrets-loader'; + +jest.mock('libs/config/src/secret-storage/secrets-loader', () => ({ + getSecretProvider: jest.fn() +})); + +const mockedGetSecretProvider = getSecretProvider as jest.MockedFunction; + +describe('fetchSecrets', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + delete process.env.ENABLE_BAO; + delete process.env.SECRETS_PROVIDER; + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('returns an empty object when OpenBao is disabled', async () => { + process.env.ENABLE_BAO = 'false'; + + await expect(fetchSecrets('secret/data/credebl_smtp_config')).resolves.toEqual({}); + expect(mockedGetSecretProvider).not.toHaveBeenCalled(); + }); + + it('returns an empty object when SECRETS_PROVIDER is not set', async () => { + process.env.ENABLE_BAO = 'true'; + delete process.env.SECRETS_PROVIDER; + + await expect(fetchSecrets('secret/data/credebl_smtp_config_2')).resolves.toEqual({}); + expect(mockedGetSecretProvider).not.toHaveBeenCalled(); + }); + + it('returns an empty object when the provider is unsupported', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'consul'; + mockedGetSecretProvider.mockReturnValue(null); + + await expect(fetchSecrets('secret/data/credebl_smtp_config_3')).resolves.toEqual({}); + expect(mockedGetSecretProvider).toHaveBeenCalledWith('consul'); + }); + + it('fetches the secrets for the requested path via the active provider', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'openbao'; + const loadSecrets = jest.fn().mockResolvedValue({ SMTP_HOST: 'smtp.example.com', SMTP_PORT: '587' }); + mockedGetSecretProvider.mockReturnValue({ name: 'OpenBao', loadSecrets }); + + await expect(fetchSecrets('secret/data/credebl_smtp_config_4')).resolves.toEqual({ + SMTP_HOST: 'smtp.example.com', + SMTP_PORT: '587' + }); + + expect(loadSecrets).toHaveBeenCalledWith({ customPath: 'secret/data/credebl_smtp_config_4' }); + }); + + it('caches the result per path and does not re-invoke the provider', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'openbao'; + const loadSecrets = jest.fn().mockResolvedValue({ RESEND_API_KEY: 'k' }); + mockedGetSecretProvider.mockReturnValue({ name: 'OpenBao', loadSecrets }); + + await fetchSecrets('secret/data/credebl_resend_api_key'); + await fetchSecrets('secret/data/credebl_resend_api_key'); + + expect(loadSecrets).toHaveBeenCalledTimes(1); + }); + + it('uses an independent cache per secret path', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'openbao'; + const loadSecrets = jest.fn().mockResolvedValue({ SOME_KEY: 'v' }); + mockedGetSecretProvider.mockReturnValue({ name: 'OpenBao', loadSecrets }); + + await fetchSecrets('secret/data/path_a'); + await fetchSecrets('secret/data/path_b'); + + expect(loadSecrets).toHaveBeenCalledTimes(2); + expect(loadSecrets).toHaveBeenCalledWith({ customPath: 'secret/data/path_a' }); + expect(loadSecrets).toHaveBeenCalledWith({ customPath: 'secret/data/path_b' }); + }); +}); diff --git a/libs/common/src/utils/ttl-cache.util.spec.ts b/libs/common/src/utils/ttl-cache.util.spec.ts new file mode 100644 index 000000000..055155740 --- /dev/null +++ b/libs/common/src/utils/ttl-cache.util.spec.ts @@ -0,0 +1,68 @@ +import { TtlCache } from './ttl-cache.util'; + +describe('TtlCache', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('calls the fetch function only once and returns the cached value on subsequent gets', async () => { + const cache = new TtlCache(1000); + const fetchFn = jest.fn().mockResolvedValue(42); + + await expect(cache.get(fetchFn)).resolves.toBe(42); + await expect(cache.get(fetchFn)).resolves.toBe(42); + + expect(fetchFn).toHaveBeenCalledTimes(1); + }); + + it('re-fetches after the ttl has elapsed', async () => { + const cache = new TtlCache(1000); + const fetchFn = jest.fn().mockResolvedValue(1); + + await cache.get(fetchFn); + expect(fetchFn).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(1001); + fetchFn.mockResolvedValue(2); + + await expect(cache.get(fetchFn)).resolves.toBe(2); + expect(fetchFn).toHaveBeenCalledTimes(2); + }); + + it('treats the ttl as a sliding window while the value is read frequently', async () => { + const cache = new TtlCache(1000); + const fetchFn = jest.fn().mockResolvedValue(1); + + await cache.get(fetchFn); + + for (let i = 0; 5 > i; i += 1) { + jest.advanceTimersByTime(900); + await expect(cache.get(fetchFn)).resolves.toBe(1); + } + + expect(fetchFn).toHaveBeenCalledTimes(1); + }); + + it('clears the cached value and cancels the pending timer on clear()', async () => { + const cache = new TtlCache(1000); + const fetchFn = jest.fn().mockResolvedValue(1); + + await cache.get(fetchFn); + cache.clear(); + fetchFn.mockResolvedValue(2); + + await expect(cache.get(fetchFn)).resolves.toBe(2); + expect(fetchFn).toHaveBeenCalledTimes(2); + }); + + it('propagates a rejection from the fetch function', async () => { + const cache = new TtlCache(1000); + const fetchFn = jest.fn().mockRejectedValue(new Error('boom')); + + await expect(cache.get(fetchFn)).rejects.toThrow('boom'); + }); +}); diff --git a/libs/config/src/secret-storage/openbao.provider.spec.ts b/libs/config/src/secret-storage/openbao.provider.spec.ts new file mode 100644 index 000000000..1bd8f0daa --- /dev/null +++ b/libs/config/src/secret-storage/openbao.provider.spec.ts @@ -0,0 +1,130 @@ +import { OpenBaoProvider } from './openbao.provider'; + +describe('OpenBaoProvider', () => { + const originalEnv = { ...process.env }; + + let provider: OpenBaoProvider; + let fetchMock: jest.Mock; + + function jsonResponse(body: unknown, ok = true, status = 200): Response { + return { + ok, + status, + json: jest.fn().mockResolvedValue(body) + } as unknown as Response; + } + + beforeEach(() => { + provider = new OpenBaoProvider(); + process.env.BAO_URL = 'http://bao:8200'; + process.env.BAO_SECRET_PATH = 'secret/data/credebl_resend_api_key'; + process.env.BAO_ROLE_ID = 'role-id'; + process.env.BAO_SECRET_ID = 'secret-id'; + + fetchMock = jest.fn(); + global.fetch = fetchMock as unknown as typeof fetch; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + jest.restoreAllMocks(); + }); + + it('rejects when any required environment variable is missing', async () => { + delete process.env.BAO_ROLE_ID; + + await expect(provider.loadSecrets()).rejects.toThrow( + 'BAO_URL, BAO_SECRET_PATH, BAO_ROLE_ID, and BAO_SECRET_ID must be set.' + ); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('authenticates with AppRole and fetches secrets from the configured path', async () => { + fetchMock + .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) + .mockResolvedValueOnce(jsonResponse({ data: { data: { RESEND_API_KEY: 'resend-key' } } })); + + await expect(provider.loadSecrets()).resolves.toEqual({ RESEND_API_KEY: 'resend-key' }); + + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'http://bao:8200/v1/auth/approle/login', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ role_id: 'role-id', secret_id: 'secret-id' }) + }) + ); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://bao:8200/v1/secret/data/credebl_resend_api_key', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ 'X-Vault-Token': 'client-token' }) + }) + ); + }); + + it('fetches from a custom path when one is supplied', async () => { + fetchMock + .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) + .mockResolvedValueOnce(jsonResponse({ data: { data: { KEY: 'value' } } })); + + await provider.loadSecrets({ customPath: 'secret/data/credebl_smtp_config' }); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'http://bao:8200/v1/secret/data/credebl_smtp_config', + expect.anything() + ); + }); + + it('rejects when the AppRole login response is not ok', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({}, false, 500)); + + await expect(provider.loadSecrets()).rejects.toThrow('Authentication failed: Status 500'); + }); + + it('rejects when the login response carries no client token', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ auth: {} })); + + await expect(provider.loadSecrets()).rejects.toThrow('Failed to retrieve client token from OpenBao.'); + }); + + it('rejects when the secrets fetch response is not ok', async () => { + fetchMock + .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) + .mockResolvedValueOnce(jsonResponse({}, false, 404)); + + await expect(provider.loadSecrets()).rejects.toThrow('Fetch failed: Status 404'); + }); + + it('rejects when the payload has an unexpected structure', async () => { + fetchMock + .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) + .mockResolvedValueOnce(jsonResponse({ data: {} })); + + await expect(provider.loadSecrets()).rejects.toThrow('Unexpected secrets payload structure.'); + }); + + it('rejects when the payload data is an array', async () => { + fetchMock + .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) + .mockResolvedValueOnce(jsonResponse({ data: { data: [] } })); + + await expect(provider.loadSecrets()).rejects.toThrow('Unexpected secrets payload structure.'); + }); + + it('rejects with a timeout error when the login request is aborted', async () => { + fetchMock.mockRejectedValueOnce(new DOMException('The operation was aborted.', 'AbortError')); + + await expect(provider.loadSecrets()).rejects.toThrow('OpenBao authentication request timed out.'); + }); + + it('rejects with a timeout error when the secrets fetch is aborted', async () => { + fetchMock + .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) + .mockRejectedValueOnce(new DOMException('The operation was aborted.', 'AbortError')); + + await expect(provider.loadSecrets()).rejects.toThrow('OpenBao secrets fetch request timed out.'); + }); +}); diff --git a/libs/config/src/secret-storage/secrets-loader.spec.ts b/libs/config/src/secret-storage/secrets-loader.spec.ts new file mode 100644 index 000000000..47c091210 --- /dev/null +++ b/libs/config/src/secret-storage/secrets-loader.spec.ts @@ -0,0 +1,104 @@ +import { OpenBaoProvider } from './openbao.provider'; +import { getSecretProvider, loadConfigSecrets } from './secrets-loader'; + +jest.mock('./openbao.provider', () => ({ + OpenBaoProvider: jest.fn() +})); + +const MockedOpenBaoProvider = OpenBaoProvider as unknown as jest.Mock; + +describe('getSecretProvider', () => { + it('returns an OpenBaoProvider for the "openbao" value', () => { + expect(getSecretProvider('openbao')).toBeInstanceOf(OpenBaoProvider); + }); + + it('matches the provider type case-insensitively', () => { + expect(getSecretProvider('OpenBao')).toBeInstanceOf(OpenBaoProvider); + expect(getSecretProvider('OPENBAO')).toBeInstanceOf(OpenBaoProvider); + }); + + it('returns null for unsupported provider types', () => { + expect(getSecretProvider('consul')).toBeNull(); + expect(getSecretProvider('aws')).toBeNull(); + expect(getSecretProvider('')).toBeNull(); + }); +}); + +describe('loadConfigSecrets', () => { + const originalEnv = { ...process.env }; + let loadSecrets: jest.Mock; + + beforeEach(() => { + delete process.env.ENABLE_BAO; + delete process.env.SECRETS_PROVIDER; + loadSecrets = jest.fn(); + MockedOpenBaoProvider.mockImplementation(() => ({ + name: 'OpenBao', + loadSecrets + })); + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('skips remote fetching when ENABLE_BAO is not "true"', async () => { + process.env.ENABLE_BAO = 'false'; + process.env.SECRETS_PROVIDER = 'openbao'; + + await loadConfigSecrets(); + + expect(loadSecrets).not.toHaveBeenCalled(); + }); + + it('skips remote fetching when SECRETS_PROVIDER is not set', async () => { + process.env.ENABLE_BAO = 'true'; + delete process.env.SECRETS_PROVIDER; + + await loadConfigSecrets(); + + expect(loadSecrets).not.toHaveBeenCalled(); + }); + + it('logs a warning and continues for an unsupported provider', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'consul'; + + await expect(loadConfigSecrets()).resolves.toBeUndefined(); + + expect(loadSecrets).not.toHaveBeenCalled(); + }); + + it('injects fetched secrets into process.env', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'openbao'; + loadSecrets.mockResolvedValue({ SMTP_HOST: 'smtp.example.com', SMTP_PASS: 'secret' }); + + await loadConfigSecrets(); + + expect(process.env.SMTP_HOST).toBe('smtp.example.com'); + expect(process.env.SMTP_PASS).toBe('secret'); + }); + + it('skips forbidden prototype-pollution keys during injection', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'openbao'; + loadSecrets.mockResolvedValue({ __proto__: { polluted: true }, constructor: 'x', SAFE: 'y' }); + + await loadConfigSecrets(); + + expect((process.env as Record & { polluted?: boolean }).polluted).toBeUndefined(); + expect(process.env.SAFE).toBe('y'); + }); + + it('logs a critical boot failure and resolves when the provider rejects', async () => { + process.env.ENABLE_BAO = 'true'; + process.env.SECRETS_PROVIDER = 'openbao'; + loadSecrets.mockRejectedValue(new Error('BAO is down')); + + await expect(loadConfigSecrets()).resolves.toBeUndefined(); + + expect(process.env.SMTP_HOST).toBeUndefined(); + }); +}); diff --git a/libs/storage/src/providers/s3-storage.provider.spec.ts b/libs/storage/src/providers/s3-storage.provider.spec.ts new file mode 100644 index 000000000..5ff6c4c6c --- /dev/null +++ b/libs/storage/src/providers/s3-storage.provider.spec.ts @@ -0,0 +1,108 @@ +import { S3StorageService } from './s3-storage.provider'; +import { fetchSecrets } from '@credebl/common/utils/secretLoader.util'; + +jest.mock('aws-sdk', () => ({ + S3: jest.fn() +})); +jest.mock('@credebl/common/utils/secretLoader.util', () => ({ + fetchSecrets: jest.fn() +})); + +import { S3 } from 'aws-sdk'; + +const MockedS3 = S3 as unknown as jest.Mock; +const mockedFetchSecrets = fetchSecrets as jest.MockedFunction; + +type ClientGetter = () => Promise; + +describe('S3StorageService', () => { + const originalEnv = { ...process.env }; + let service: S3StorageService; + + beforeEach(() => { + delete process.env.AWS_REGION; + delete process.env.AWS_ACCESS_KEY; + delete process.env.AWS_SECRET_KEY; + delete process.env.AWS_PUBLIC_REGION; + delete process.env.AWS_PUBLIC_ACCESS_KEY; + delete process.env.AWS_PUBLIC_SECRET_KEY; + delete process.env.AWS_S3_STOREOBJECT_REGION; + delete process.env.AWS_S3_STOREOBJECT_ACCESS_KEY; + delete process.env.AWS_S3_STOREOBJECT_SECRET_KEY; + service = new S3StorageService(); + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('builds the internal S3 client from provider-sourced credentials', async () => { + mockedFetchSecrets.mockResolvedValue({ AWS_ACCESS_KEY: 'secret-key', AWS_SECRET_KEY: 'secret-secret' }); + process.env.AWS_REGION = 'us-east-1'; + + await (service as unknown as { getS3Client: ClientGetter }).getS3Client(); + + expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_aws_keys'); + expect(MockedS3).toHaveBeenCalledWith({ + accessKeyId: 'secret-key', + secretAccessKey: 'secret-secret', + region: 'us-east-1' + }); + }); + + it('falls back to environment variables for the internal client', async () => { + mockedFetchSecrets.mockResolvedValue({}); + process.env.AWS_REGION = 'us-east-1'; + process.env.AWS_ACCESS_KEY = 'env-key'; + process.env.AWS_SECRET_KEY = 'env-secret'; + + await (service as unknown as { getS3Client: ClientGetter }).getS3Client(); + + expect(MockedS3).toHaveBeenCalledWith({ + accessKeyId: 'env-key', + secretAccessKey: 'env-secret', + region: 'us-east-1' + }); + }); + + it('builds the public S3 client from the public credential pair', async () => { + mockedFetchSecrets.mockResolvedValue({ + AWS_PUBLIC_ACCESS_KEY: 'public-key', + AWS_PUBLIC_SECRET_KEY: 'public-secret' + }); + process.env.AWS_PUBLIC_REGION = 'ap-south-1'; + + await (service as unknown as { getPublicS3Client: ClientGetter }).getPublicS3Client(); + + expect(MockedS3).toHaveBeenCalledWith({ + accessKeyId: 'public-key', + secretAccessKey: 'public-secret', + region: 'ap-south-1' + }); + }); + + it('builds the store-object S3 client from the store-object credential pair', async () => { + mockedFetchSecrets.mockResolvedValue({ + AWS_S3_STOREOBJECT_ACCESS_KEY: 'store-key', + AWS_S3_STOREOBJECT_SECRET_KEY: 'store-secret' + }); + process.env.AWS_S3_STOREOBJECT_REGION = 'eu-west-1'; + + await (service as unknown as { getStoreObjectS3Client: ClientGetter }).getStoreObjectS3Client(); + + expect(MockedS3).toHaveBeenCalledWith({ + accessKeyId: 'store-key', + secretAccessKey: 'store-secret', + region: 'eu-west-1' + }); + }); + + it('builds the public URL from the bucket and public region', () => { + process.env.AWS_PUBLIC_REGION = 'ap-south-1'; + + expect(service.getPublicUrl('my-bucket', 'logos/logo.png')).toBe( + 'https://my-bucket.s3.ap-south-1.amazonaws.com/logos/logo.png' + ); + }); +}); diff --git a/openbao-init.sh b/openbao-init.sh new file mode 100755 index 000000000..3e823bd77 --- /dev/null +++ b/openbao-init.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# +# Bootstrap OpenBao (docker-compose.openbao.yml) for the CREDEBL platform demo. +# +# Provisions, idempotently: +# - OpenBao initialization + unseal (if not already initialized) +# - KV v2 secrets engine at secret/ +# - AppRole auth method +# - a "credebl" AppRole role + policy scoped to the credebl_* secret paths +# - the platform's secret data, sourced from the matching environment variables +# +# Usage: +# ./openbao-init.sh +# +# On the first run the script prints the unseal key, root token, role_id and +# secret_id. Copy BAO_ROLE_ID and BAO_SECRET_ID into .env.demo. +# On later runs it is idempotent: it re-prints role_id and re-uses an existing +# secret_id instead of generating a new one. +# +# Optional inputs (read from the environment): +# BAO_UNSEAL_KEY - unseal key from a previous run (needed if already +# initialized but sealed) +# BAO_ROOT_TOKEN - root token from a previous run (avoids re-printing) +# CONTAINER_NAME - docker container name (default: openbao) +# RESEND_API_KEY, SENDGRID_API_KEY, SMTP_HOST/PORT/USER/PASS, +# AWS_* - secret values to store; only the ones that are set +# (non-empty) are written +# +# NOTE: for demo only. The unseal key and root token are secrets; do not commit +# them to the repository. + +set -euo pipefail + +CONTAINER_NAME="${CONTAINER_NAME:-openbao}" +BAO_ADDR_IN_CONTAINER="${BAO_ADDR_IN_CONTAINER:-http://127.0.0.1:8200}" + +run_bao() { + docker exec -e BAO_TOKEN="${BAO_TOKEN:-}" "$CONTAINER_NAME" bao "$@" +} + +echo_step() { + printf '\n==> %s\n' "$1" +} + +echo "Connecting to OpenBao in container '${CONTAINER_NAME}' (${BAO_ADDR_IN_CONTAINER})..." + +status="$(docker exec "$CONTAINER_NAME" bao status 2>&1 || true)" +initialized="$(grep -E '^Initialized' <<<"$status" | awk '{print $2}')" +sealed="$(grep -E '^Sealed' <<<"$status" | awk '{print $2}')" + +if [[ "$initialized" != "true" ]]; then + echo_step "OpenBao is not initialized. Running 'bao operator init' (1 unseal key)..." + init_out="$(docker exec "$CONTAINER_NAME" bao operator init -key-shares=1 -key-threshold=1)" + BAO_UNSEAL_KEY="${BAO_UNSEAL_KEY:-$(grep 'Unseal Key 1:' <<<"$init_out" | awk '{print $NF}')}" + BAO_ROOT_TOKEN="${BAO_ROOT_TOKEN:-$(grep 'Initial Root Token:' <<<"$init_out" | awk '{print $NF}')}" +else + BAO_UNSEAL_KEY="${BAO_UNSEAL_KEY:-}" + BAO_ROOT_TOKEN="${BAO_ROOT_TOKEN:-}" +fi + +if [[ "$sealed" == "true" ]]; then + if [[ -z "$BAO_UNSEAL_KEY" ]]; then + echo "ERROR: OpenBao is sealed. Re-run with BAO_UNSEAL_KEY= (from the first run)." >&2 + exit 1 + fi + echo_step "Unsealing OpenBao..." + docker exec "$CONTAINER_NAME" bao operator unseal "$BAO_UNSEAL_KEY" >/dev/null +fi + +if [[ -z "$BAO_ROOT_TOKEN" ]]; then + echo "ERROR: no root token available. Re-run with BAO_ROOT_TOKEN= (from the first run)." >&2 + exit 1 +fi +BAO_TOKEN="$BAO_ROOT_TOKEN" + +if ! run_bao secrets list 2>/dev/null | grep -q '^secret/'; then + echo_step "Enabling KV v2 secrets engine at secret/..." + run_bao secrets enable -path=secret kv-v2 +fi + +if ! run_bao auth list 2>/dev/null | grep -q '^approle/'; then + echo_step "Enabling AppRole auth method..." + run_bao auth enable approle +fi + +if ! run_bao policy list 2>/dev/null | grep -qx 'credebl-secrets'; then + echo_step "Writing 'credebl-secrets' policy..." + docker exec -i -e BAO_TOKEN="$BAO_TOKEN" "$CONTAINER_NAME" bao policy write credebl-secrets - <<'POLICY' +path "secret/data/credebl_*" { + capabilities = ["read"] +} +path "secret/metadata/credebl_*" { + capabilities = ["read", "list"] +} +POLICY +fi + +role_exists="false" +if run_bao read auth/approle/role/credebl >/dev/null 2>&1; then + role_exists="true" +fi + +if [[ "$role_exists" == "false" ]]; then + echo_step "Creating AppRole role 'credebl' (non-expiring secret_id, read-only policy)..." + run_bao write auth/approle/role/credebl \ + token_policies=credebl-secrets \ + secret_id_ttl=0 \ + token_ttl=30m \ + token_max_ttl=1h +fi + +role_id="$(run_bao read -field=role_id auth/approle/role/credebl/role-id)" + +if [[ "$role_exists" == "false" ]]; then + echo_step "Generating secret_id for the 'credebl' role..." + secret_id="$(run_bao write -f -field=secret_id auth/approle/role/credebl/secret-id)" +else + secret_id="${BAO_SECRET_ID:-}" +fi + +echo_step "Storing platform secrets..." + +kv_put() { + local path="$1" + shift + if [[ "$#" -gt 0 ]]; then + docker exec -e BAO_TOKEN="$BAO_TOKEN" "$CONTAINER_NAME" bao kv put "$path" "$@" >/dev/null + echo " wrote ${path}" + else + echo " skipped ${path} (no matching environment variables set)" + fi +} + +kv_put secret/credebl_resend_api_key \ + $( [[ -n "${RESEND_API_KEY:-}" ]] && printf 'RESEND_API_KEY=%s' "$RESEND_API_KEY" ) + +kv_put secret/credebl_sendgrid_api_key \ + $( [[ -n "${SENDGRID_API_KEY:-}" ]] && printf 'SENDGRID_API_KEY=%s' "$SENDGRID_API_KEY" ) + +kv_put secret/credebl_smtp_config \ + $( [[ -n "${SMTP_HOST:-}" ]] && printf 'SMTP_HOST=%s' "$SMTP_HOST" ) \ + $( [[ -n "${SMTP_PORT:-}" ]] && printf 'SMTP_PORT=%s' "$SMTP_PORT" ) \ + $( [[ -n "${SMTP_USER:-}" ]] && printf 'SMTP_USER=%s' "$SMTP_USER" ) \ + $( [[ -n "${SMTP_PASS:-}" ]] && printf 'SMTP_PASS=%s' "$SMTP_PASS" ) + +kv_put secret/credebl_aws_keys \ + $( [[ -n "${AWS_ACCESS_KEY:-}" ]] && printf 'AWS_ACCESS_KEY=%s' "$AWS_ACCESS_KEY" ) \ + $( [[ -n "${AWS_SECRET_KEY:-}" ]] && printf 'AWS_SECRET_KEY=%s' "$AWS_SECRET_KEY" ) \ + $( [[ -n "${AWS_PUBLIC_ACCESS_KEY:-}" ]] && printf 'AWS_PUBLIC_ACCESS_KEY=%s' "$AWS_PUBLIC_ACCESS_KEY" ) \ + $( [[ -n "${AWS_PUBLIC_SECRET_KEY:-}" ]] && printf 'AWS_PUBLIC_SECRET_KEY=%s' "$AWS_PUBLIC_SECRET_KEY" ) \ + $( [[ -n "${AWS_S3_STOREOBJECT_ACCESS_KEY:-}" ]] && printf 'AWS_S3_STOREOBJECT_ACCESS_KEY=%s' "$AWS_S3_STOREOBJECT_ACCESS_KEY" ) \ + $( [[ -n "${AWS_S3_STOREOBJECT_SECRET_KEY:-}" ]] && printf 'AWS_S3_STOREOBJECT_SECRET_KEY=%s' "$AWS_S3_STOREOBJECT_SECRET_KEY" ) + +echo_step "Done. Add the following to .env.demo:" +cat <)` method returning a flat `Record` of key/value secrets. +- **Must** expose a `getSecretProvider(providerType)` factory that returns a provider for the value `openbao` (case-insensitive) and `null` for any other value. +- **Will** support future providers (e.g. AWS Secrets Manager) via the same factory without call-site changes. + +#### Scenario: unknown provider value + +- **When** `getSecretProvider` is called with a value other than `openbao` +- **Then** it returns `null`. + +#### Scenario: case-insensitive provider lookup + +- **When** `getSecretProvider` is called with `OpenBao` or `OPENBAO` +- **Then** it returns an `OpenBaoProvider` instance. + +### openbao-provider-configuration + +The OpenBao provider reads its connection settings from the process environment. + +- **Must** read `BAO_URL`, `BAO_SECRET_PATH`, `BAO_ROLE_ID`, and `BAO_SECRET_ID` from `process.env`. +- **Must** throw an error listing all required variables when any of them is missing or empty. +- **Must** allow the secret path to be overridden per call via a `customPath` option passed to `loadSecrets`. +- **Must** enforce a request timeout of `CommonConstants.OPENBAO_REQUEST_TIMEOUT` (10 seconds) on every OpenBao HTTP request. +- **Will** treat a request aborted by the timeout as a failure and throw a `...timed out` error. + +#### Scenario: missing environment configuration + +- **When** any of `BAO_URL`, `BAO_SECRET_PATH`, `BAO_ROLE_ID`, `BAO_SECRET_ID` is unset +- **Then** `loadSecrets` rejects with an error naming the missing variables. + +#### Scenario: custom secret path + +- **When** `loadSecrets` is called with `{ customPath: 'secret/data/custom' }` +- **Then** the provider fetches `BAO_URL/v1/secret/data/custom` instead of the `BAO_SECRET_PATH` value. + +### openbao-approle-authentication + +The provider authenticates to OpenBao using the AppRole auth method and exchanges role credentials for a client token. + +- **Must** POST `{ role_id, secret_id }` as a JSON body to `{BAO_URL}/v1/auth/approle/login`. +- **Must** throw an `Authentication failed: Status ` error when the login response is not ok. +- **Must** throw a `Failed to retrieve client token from OpenBao.` error when the login succeeds but the response contains no `auth.client_token`. +- **Must** send the obtained client token on subsequent secret requests via the `X-Vault-Token` header. +- **Will** reject with `OpenBao authentication request timed out.` when the login request is aborted by the timeout. + +#### Scenario: rejected AppRole credentials + +- **When** OpenBao answers the login request with a non-2xx status +- **Then** the provider throws `Authentication failed: Status `. + +#### Scenario: login response without a token + +- **When** the login response is ok but carries no `auth.client_token` +- **Then** the provider throws `Failed to retrieve client token from OpenBao.`. + +### openbao-kv-secret-retrieval + +The provider reads a KV secrets engine path using the authenticated token. + +- **Must** GET `{BAO_URL}/v1/{secretPath}` with the `X-Vault-Token` header. +- **Must** throw `Fetch failed: Status ` when the read response is not ok. +- **Must** read the secret data from the KV v2 payload shape `result.data.data`. +- **Must** throw `Unexpected secrets payload structure.` when `data.data` is missing, not an object, or an array. +- **Must** return the secret map as a flat `Record`. +- **Will** reject with `OpenBao secrets fetch request timed out.` when the read request is aborted by the timeout. + +#### Scenario: unexpected payload shape + +- **When** OpenBao returns a payload without a `data.data` object +- **Then** the provider throws `Unexpected secrets payload structure.`. + +#### Scenario: successful secret retrieval + +- **When** authentication succeeds and the KV path returns `{ data: { data: { RESEND_API_KEY: 'x' } } }` +- **Then** `loadSecrets` resolves to `{ RESEND_API_KEY: 'x' }`. + +### startup-secret-injection + +Every microservice loads remote secrets into `process.env` before its NATS microservice is created and starts listening. + +- **Must** gate startup injection on `ENABLE_BAO` being exactly `true` after trimming and lowercasing. +- **Must** skip injection without error when `ENABLE_BAO` is not `true`. +- **Must** skip injection without error when `SECRETS_PROVIDER` is not set. +- **Must** log a warning and continue with local environment variables when `SECRETS_PROVIDER` names an unsupported provider. +- **Must** inject every fetched secret into `process.env` as an enumerable, configurable, writable string property. +- **Must** skip keys named `__proto__`, `constructor`, or `prototype` during injection. +- **Must** log a `Critical Lifecycle Boot Failure` error and continue startup when the provider throws, rather than crashing the bootstrap. +- **Must** run `await loadConfigSecrets()` before `NestFactory.createMicroservice(...)` in every `apps/*/src/main.ts` bootstrap. +- **Will** be invoked through the `@credebl/config` package root export. + +#### Scenario: secrets management disabled + +- **When** `ENABLE_BAO` is unset, `false`, or `FALSE` +- **Then** `loadConfigSecrets` returns without reading from any provider. + +#### Scenario: unsupported provider value + +- **When** `SECRETS_PROVIDER=consul` +- **Then** `loadConfigSecrets` logs a warning and returns without throwing. + +#### Scenario: malicious secret keys are filtered + +- **When** the provider returns a secret whose key is `__proto__` +- **Then** that entry is skipped and `process.env` is not polluted. + +#### Scenario: provider failure does not block boot + +- **When** the provider rejects while loading secrets +- **Then** the error is logged and `loadConfigSecrets` resolves instead of rejecting. + +### runtime-secret-loading-with-cache + +Email and storage integrations fetch their credential sets on demand from the configured provider, cached per secret path. + +- **Must** expose a `fetchSecrets(secretPath)` helper in `libs/common/src/utils/secretLoader.util.ts`. +- **Must** return an empty object without calling the provider when `ENABLE_BAO` is not `true`. +- **Must** return an empty object when `SECRETS_PROVIDER` is unset or names an unsupported provider. +- **Must** call `provider.loadSecrets({ customPath: secretPath })` when a provider is active. +- **Must** cache the result per secret path for a TTL of 10 minutes. +- **Must** share the cache module-wide across all consumers (email + storage) for a given path. + +#### Scenario: runtime fetch for a specific path + +- **When** `fetchSecrets('secret/data/credebl_smtp_config')` is called with OpenBao enabled +- **Then** the provider is invoked with `customPath` set to that path and its result is returned. + +#### Scenario: subsequent reads are served from cache + +- **When** `fetchSecrets` is called twice with the same path while the entry is unexpired +- **Then** the provider is only invoked once. + +### ttl-cache-semantics + +The TTL cache preserves a fetched value for a duration and re-fetches once it lapses. + +- **Must** invoke the fetch function on first read and store the result. +- **Must** return the stored value on subsequent reads without re-invoking the fetch function. +- **Must** treat the TTL as sliding: any read refreshes the expiry timer. +- **Must** clear the stored value once `ttlMs` of inactivity elapses. +- **Must** provide a `clear()` method that drops the stored value and cancels the pending expiry timer. +- **Will** schedule expiry so that an idle process is not kept alive by the pending timer. + +#### Scenario: expiry triggers a re-fetch + +- **When** a value is read again after `ttlMs` of inactivity has elapsed +- **Then** the fetch function runs again and the fresh value is returned. + +#### Scenario: sliding window keeps a hot entry alive + +- **When** a value is read repeatedly at intervals shorter than `ttlMs` +- **Then** the fetch function is never re-invoked and the same value is returned. + +### email-credential-sourcing + +Email providers resolve their credentials at runtime, preferring provider-sourced secrets over environment variables. + +- **Must** resolve the Resend API key from `secret/data/credebl_resend_api_key` (`RESEND_API_KEY`) or fall back to `process.env.RESEND_API_KEY`. +- **Must** resolve the SendGrid API key from `secret/data/credebl_sendgrid_api_key` (`SENDGRID_API_KEY`) or fall back to `process.env.SENDGRID_API_KEY`. +- **Must** resolve SMTP settings (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`) from `secret/data/credebl_smtp_config` or fall back to the matching environment variables. +- **Must** treat a missing/invalid SMTP port as a failure. +- **Must** return `false` and log the error when a required credential is missing or a send fails, rather than throwing to the caller. +- **Must** configure the SMTP transport with `secure: true` for port 465 and `requireTLS: true` for port 587. + +#### Scenario: missing email credential + +- **When** the provider returns no Resend key and `RESEND_API_KEY` is unset +- **Then** `sendWithResend` logs an error and returns `false`. + +#### Scenario: invalid SMTP port + +- **When** `SMTP_PORT` is not a positive integer +- **Then** `sendWithSMTP` logs an error and returns `false`. + +#### Scenario: provider secrets take precedence + +- **When** the provider returns `RESEND_API_KEY` and the same env var is set to a different value +- **Then** the client is constructed with the provider-sourced key. + +### storage-credential-sourcing + +S3 storage clients resolve their AWS credentials at runtime from the shared `secret/data/credebl_aws_keys` path. + +- **Must** resolve internal S3 credentials from `AWS_ACCESS_KEY`/`AWS_SECRET_KEY` or fall back to the matching env vars. +- **Must** resolve public (logo/URL) S3 credentials from `AWS_PUBLIC_ACCESS_KEY`/`AWS_PUBLIC_SECRET_KEY` or fall back to the matching env vars. +- **Must** resolve store-object S3 credentials from `AWS_S3_STOREOBJECT_ACCESS_KEY`/`AWS_S3_STOREOBJECT_SECRET_KEY` or fall back to the matching env vars. +- **Must** read the region for each client from `AWS_REGION`, `AWS_PUBLIC_REGION`, and `AWS_S3_STOREOBJECT_REGION` respectively. +- **Will** leave the RustFS/local storage providers reading credentials exclusively from the environment. + +#### Scenario: provider-sourced AWS keys + +- **When** OpenBao returns `AWS_ACCESS_KEY`/`AWS_SECRET_KEY` at the AWS key path +- **Then** the internal S3 client is constructed with those values. + +### deployment-artifacts + +The repository ships demo configuration and container artifacts for running OpenBao. + +- **Must** document the `ENABLE_BAO` and `SECRETS_PROVIDER` gating variables and the `BAO_URL`, `BAO_SECRET_PATH`, `BAO_ROLE_ID`, and `BAO_SECRET_ID` placeholders in `.env.demo`. +- **Must** provide a `docker-compose.openbao.yml` that runs an `openbao/openbao` server exposing port 8200 with file-backed storage persisted on a named volume. +- **Must** provide an HCL server configuration (`config.hcl`) declaring file storage at `/openbao/file`, a TCP listener on `0.0.0.0:8200` with TLS disabled, and the UI enabled. +- **Must** provide a `openbao-init.sh` script that idempotently initializes and unseals the server, enables KV v2 at `secret/`, enables AppRole auth, creates the `credebl` role scoped to the `credebl_*` paths, generates the `role_id`/`secret_id`, and stores the secret data from the matching environment variables. +- **Will** require the operator to run `openbao-init.sh` once and copy the printed `BAO_ROLE_ID`/`BAO_SECRET_ID` into `.env.demo` before the platform can use the provider. + +#### Scenario: demo environment placeholders + +- **When** an operator follows the demo setup +- **Then** `.env.demo` exposes the two gating variables and the four `BAO_*` variables to configure the provider. + +#### Scenario: local OpenBao server + +- **When** `docker compose -f docker-compose.openbao.yml up` is run +- **Then** an OpenBao server is available on `127.0.0.1:8200` and its data survives restarts via the named volume. + +#### Scenario: one-shot provisioning via the init script + +- **When** `./openbao-init.sh` is run against a freshly started server +- **Then** the server is initialized and unsealed, KV v2 and AppRole are enabled, the `credebl` role exists, and the script prints `BAO_ROLE_ID`/`BAO_SECRET_ID` ready for `.env.demo`. +- **And** re-running the script does not re-initialize the server or regenerate the `role_id`/`secret_id`. diff --git a/package.json b/package.json index 79f2dbe33..348906944 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "express-useragent": "^1.0.15", "firebase-admin": "^13.6.1", "fs": "0.0.1-security", - "generate-password": "^1.7.1", + "generate-password": "^1.7.1", "helmet": "^8.1.0", "html-pdf": "^3.0.1", "html-to-image": "^1.11.13", @@ -201,11 +201,11 @@ "^@credebl/user-org-roles(|/.*)$": "/libs/user-org-roles/src/$1", "^y/user-activity(|/.*)$": "/libs/user-activity/src/$1", "^@app/supabase(|/.*)$": "/libs/supabase/src/$1", - "^credebl/utility(|/.*)$": "/libs/utility/src/$1", "^@credebl/config(|/.*)$": "/libs/config/src/$1", "^@credebl/context(|/.*)$": "/libs/context/src/$1", - "^@credebl/logger(|/.*)$": "/libs/logger/src/$1" + "^@credebl/logger(|/.*)$": "/libs/logger/src/$1", + "^libs/(.*)$": "/libs/$1" } }, "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a", From cf3f979d6904fbb6d66835a40468b1e4112f99d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 05:06:49 +0000 Subject: [PATCH 30/58] build: bump nanoid from 4.0.2 to 5.1.16 Bumps [nanoid](https://github.com/ai/nanoid) from 4.0.2 to 5.1.16. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/4.0.2...5.1.16) --- updated-dependencies: - dependency-name: nanoid dependency-version: 5.1.16 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 697d33844..42930f5c2 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "moment": "^2.30.1", "morgan": "^1.10.1", "multer": "^2.0.2", - "nanoid": "^4.0.2", + "nanoid": "^5.1.16", "nats": "^2.29.3", "nestjs-cls": "catalog:", "nestjs-rate-limiter": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c15e52875..01e538fdb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,8 +279,8 @@ importers: specifier: ^2.0.2 version: 2.1.1 nanoid: - specifier: ^4.0.2 - version: 4.0.2 + specifier: ^5.1.16 + version: 5.1.16 nats: specifier: ^2.29.3 version: 2.29.3 @@ -3106,6 +3106,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cron@4.4.0: resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} @@ -3120,6 +3121,7 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained. crypto-random-string@5.0.0: resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} @@ -4920,9 +4922,9 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - nanoid@4.0.2: - resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} - engines: {node: ^14 || ^16 || >=18} + nanoid@5.1.16: + resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} + engines: {node: ^18 || >=20} hasBin: true nats@2.29.3: @@ -12301,7 +12303,7 @@ snapshots: mute-stream@2.0.0: {} - nanoid@4.0.2: {} + nanoid@5.1.16: {} nats@2.29.3: dependencies: From f026ceba5faa1bcd63ff4ce5cc4b0e2da283b9aa Mon Sep 17 00:00:00 2001 From: Ajay Jadhav Date: Thu, 13 Aug 2026 18:12:59 +0530 Subject: [PATCH 31/58] fix: address SonarCloud code smells - assign positional parameter to local var in openbao-init.sh (shelldre:S7679) - replace switch with if in getSecretProvider (typescript:S1301) Signed-off-by: Ajay Jadhav --- libs/config/src/secret-storage/secrets-loader.ts | 10 ++++------ openbao-init.sh | 3 ++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libs/config/src/secret-storage/secrets-loader.ts b/libs/config/src/secret-storage/secrets-loader.ts index 8c7d6c23b..1d9c546ad 100644 --- a/libs/config/src/secret-storage/secrets-loader.ts +++ b/libs/config/src/secret-storage/secrets-loader.ts @@ -9,13 +9,11 @@ const logger = new Logger('SecretsLoader'); * Factory function to get the requested secret provider */ export function getSecretProvider(providerType: string): SecretProvider | null { - switch (providerType.toLowerCase()) { - case 'openbao': - return new OpenBaoProvider(); - - default: - return null; + if ('openbao' === providerType.toLowerCase()) { + return new OpenBaoProvider(); } + + return null; } export async function loadConfigSecrets(): Promise { diff --git a/openbao-init.sh b/openbao-init.sh index 3e823bd77..7ab376dad 100755 --- a/openbao-init.sh +++ b/openbao-init.sh @@ -39,7 +39,8 @@ run_bao() { } echo_step() { - printf '\n==> %s\n' "$1" + local message="$1" + printf '\n==> %s\n' "$message" } echo "Connecting to OpenBao in container '${CONTAINER_NAME}' (${BAO_ADDR_IN_CONTAINER})..." From 860d462dc69eec8ea1023daec0855c47ae47bbde Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:23:07 +0000 Subject: [PATCH 32/58] build: bump qs from 6.15.1 to 6.15.2 Bumps [qs](https://github.com/ljharb/qs) from 6.15.1 to 6.15.2. - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.15.1...v6.15.2) --- updated-dependencies: - dependency-name: qs dependency-version: 6.15.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 41 ++++++++++------------------------------- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 47ea3c9c0..7104e0451 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "pg": "^8.18.0", "puppeteer": "^21.11.0", "qrcode": "^1.5.4", - "qs": "^6.15.0", + "qs": "^6.15.2", "reflect-metadata": "^0.1.14", "resend": "^6.9.2", "rimraf": "^3.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b866d285..0e9df95ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -339,8 +339,8 @@ importers: specifier: ^1.5.4 version: 1.5.4 qs: - specifier: ^6.15.0 - version: 6.15.1 + specifier: ^6.15.2 + version: 6.15.2 reflect-metadata: specifier: ^0.1.14 version: 0.1.14 @@ -5476,12 +5476,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} - - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} engines: {node: '>=0.6'} qs@6.5.5: @@ -5761,10 +5757,6 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - side-channel@1.1.1: resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} @@ -9440,7 +9432,7 @@ snapshots: idtoken-verifier: 2.2.4 js-cookie: 2.2.1 minimist: 1.2.8 - qs: 6.15.1 + qs: 6.15.2 superagent: 10.3.0 url-join: 4.0.1 winchan: 0.2.2 @@ -9631,7 +9623,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.15.2 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 @@ -9646,7 +9638,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.3 + qs: 6.15.2 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -10648,7 +10640,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.1 + qs: 6.15.2 range-parser: 1.2.1 router: 2.2.0 send: 1.2.1 @@ -12914,13 +12906,8 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qs@6.15.1: + qs@6.15.2: dependencies: - side-channel: 1.1.0 - - qs@6.15.3: - dependencies: - es-define-property: 1.0.1 side-channel: 1.1.1 qs@6.5.5: @@ -13265,14 +13252,6 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - side-channel@1.1.1: dependencies: es-errors: 1.3.0 @@ -13511,7 +13490,7 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.15.1 + qs: 6.15.2 transitivePeerDependencies: - supports-color From 588653ffeef7f6ae838f14199b036d97faf968ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:23:09 +0000 Subject: [PATCH 33/58] build: bump form-data from 4.0.5 to 4.0.6 Bumps [form-data](https://github.com/form-data/form-data) from 4.0.5 to 4.0.6. - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6) --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.6 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 45 ++++++++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 47ea3c9c0..0cd103130 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "xml-js": "^1.6.11", "protobufjs": "^7.6.5", "handlebars": "^4.7.9", - "form-data": "^4.0.5" + "form-data": "^4.0.6" }, "devDependencies": { "@nestjs/cli": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b866d285..ced4ef6f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,8 +237,8 @@ importers: specifier: ^13.6.1 version: 13.8.0 form-data: - specifier: ^4.0.5 - version: 4.0.5 + specifier: ^4.0.6 + version: 4.0.6 fs: specifier: 0.0.1-security version: 0.0.1-security @@ -3383,6 +3383,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} @@ -3776,8 +3780,8 @@ packages: resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==} engines: {node: '>= 0.12'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} formdata-polyfill@4.0.10: @@ -8978,7 +8982,7 @@ snapshots: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 '@types/node': 20.19.39 - form-data: 4.0.5 + form-data: 4.0.6 '@types/supertest@7.2.0': dependencies: @@ -9479,7 +9483,7 @@ snapshots: axios@1.18.0: dependencies: follow-redirects: 1.16.0 - form-data: 4.0.5 + form-data: 4.0.6 https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -10292,7 +10296,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -10347,16 +10351,20 @@ snapshots: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 es-shim-unscopables@1.1.0: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 es-to-primitive@1.3.0: dependencies: @@ -10883,12 +10891,12 @@ snapshots: safe-buffer: 5.2.1 optional: true - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.3 + hasown: 2.0.4 mime-types: 2.1.35 formdata-polyfill@4.0.10: @@ -10943,7 +10951,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 - hasown: 2.0.3 + hasown: 2.0.4 is-callable: 1.2.7 functional-red-black-tree@1.0.1: @@ -11002,12 +11010,12 @@ snapshots: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 es-errors: 1.3.0 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 function-bind: 1.1.2 get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 math-intrinsics: 1.1.0 get-package-type@0.1.0: {} @@ -11227,7 +11235,6 @@ snapshots: hasown@2.0.4: dependencies: function-bind: 1.1.2 - optional: true helmet@8.1.0: {} @@ -11355,7 +11362,7 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.3 + hasown: 2.0.4 side-channel: 1.1.1 ioredis@5.10.1: @@ -11469,7 +11476,7 @@ snapshots: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 is-set@2.0.3: {} @@ -12442,7 +12449,7 @@ snapshots: call-bind: 1.0.9 call-bound: 1.0.4 define-properties: 1.2.1 - es-object-atoms: 1.1.1 + es-object-atoms: 1.1.2 object.fromentries@2.0.8: dependencies: @@ -13507,7 +13514,7 @@ snapshots: cookiejar: 2.1.4 debug: 4.4.3 fast-safe-stringify: 2.1.1 - form-data: 4.0.5 + form-data: 4.0.6 formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 From 4a000e38fc0985c6671ee3106dad1747f2fabfa3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:23:09 +0000 Subject: [PATCH 34/58] build: bump @opentelemetry/sdk-node from 0.202.0 to 0.217.0 Bumps [@opentelemetry/sdk-node](https://github.com/open-telemetry/opentelemetry-js) from 0.202.0 to 0.217.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/experimental/v0.202.0...experimental/v0.217.0) --- updated-dependencies: - dependency-name: "@opentelemetry/sdk-node" dependency-version: 0.217.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- pnpm-lock.yaml | 490 +++++++++++++++++++++++++++++++++++-------------- 2 files changed, 357 insertions(+), 135 deletions(-) diff --git a/package.json b/package.json index 47ea3c9c0..ae0773658 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@opentelemetry/instrumentation-nestjs-core": "^0.48.1", "@opentelemetry/resources": "^2.5.1", "@opentelemetry/sdk-logs": "^0.202.0", - "@opentelemetry/sdk-node": "^0.202.0", + "@opentelemetry/sdk-node": "^0.217.0", "@opentelemetry/semantic-conventions": "^1.39.0", "@prisma/client": "^5.22.0", "@sendgrid/mail": "^7.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b866d285..fd2886367 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,7 +103,7 @@ importers: version: 11.0.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.19(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/passport': specifier: 'catalog:' version: 11.0.5(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0) @@ -150,8 +150,8 @@ importers: specifier: ^0.202.0 version: 0.202.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-node': - specifier: ^0.202.0 - version: 0.202.0(@opentelemetry/api@1.9.1) + specifier: ^0.217.0 + version: 0.217.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': specifier: ^1.39.0 version: 1.40.0 @@ -500,7 +500,7 @@ importers: version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.19(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/swagger': specifier: 'catalog:' version: 11.4.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) @@ -555,7 +555,7 @@ importers: version: 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.19(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/testing': specifier: 'catalog:' version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19) @@ -991,6 +991,10 @@ packages: resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} engines: {node: '>=12.10.0'} + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + '@grpc/proto-loader@0.7.15': resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} engines: {node: '>=6'} @@ -1001,6 +1005,11 @@ packages: engines: {node: '>=6'} hasBin: true + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1550,12 +1559,22 @@ packages: resolution: {integrity: sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw==} engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.217.0': + resolution: {integrity: sha512-Cdq0jW2lknrNfrAm92MyEAvpe2cRsKjdnQLHUL6xRA4IVUnsWx6P65E7NcUO0Y+L4w1Aee5iV8FvjSwd+lrs9A==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/context-async-hooks@2.0.1': - resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} + '@opentelemetry/configuration@0.217.0': + resolution: {integrity: sha512-xCtrYOhBqdy6ZOMfe0Oa73ZKF+2LMhoOv4L5vmwAHVvOXUg+V3fvKuEIr9ZyD0Ow+vxllEjWO6PV1wd0DOtyvw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/context-async-hooks@2.7.1': + resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1572,8 +1591,8 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.202.0': - resolution: {integrity: sha512-Y84L8Yja/A2qjGEzC/To0yrMUXHrtwJzHtZ2za1/ulZplRe5QFsLNyHixIS42ZYUKuNyWMDgOFhnN2Pz5uThtg==} + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0': + resolution: {integrity: sha512-vC5S0Dc+noxD86CVtNu1+awCHPA5Kewi1Sg23ps+9lh4YifwsKXh3pe4XTNEKtUJiAcjpJ5dqStGakLbrSE+YQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -1584,38 +1603,44 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.202.0': - resolution: {integrity: sha512-qYwbmNWPkP7AbzX8o4DRu5bb/a0TWYNcpZc1NEAOhuV7pgBpAUPEClxRWPN94ulIia+PfQjzFGMaRwmLGmNP6g==} + '@opentelemetry/exporter-logs-otlp-http@0.217.0': + resolution: {integrity: sha512-KfLAdt1uilVE+3FxbgVnp2ZrzqbIawzcesnRoi+Kh9ckB5Ld5D8btUgoBvwTbdmuNx1j6b132Wsh72azq+pPNQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0': - resolution: {integrity: sha512-/dq/rf4KCkTYoP+NyPXTE+5wjvfhAHSqK62vRsJ/IalG61VPQvwaL18yWcavbI+44ImQwtMeZxfIJSox7oQL0w==} + '@opentelemetry/exporter-logs-otlp-proto@0.217.0': + resolution: {integrity: sha512-Se0GG/ZO24mQTlQj7zprR4pNI0nKe4lPDPBsuJmi6508b9TlZEuUd3EfyuHk6oJxzL7fGyDFYAbxNigQvRP2ZQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.202.0': - resolution: {integrity: sha512-ooYcrf/m9ZuVGpQnER7WRH+JZbDPD389HG7VS/EnvIEF5WpNYEqf+NdmtaAcs51d81QrytTYAubc5bVWi//28w==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0': + resolution: {integrity: sha512-0GpJKnCoVaVA1rKBMVPHziznfOQlXgH72S9ktjBAF1AnAVPzX7vVEBGrhwiSxxHDAiefXk+J8znApsMb/K6Z3w==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.202.0': - resolution: {integrity: sha512-X0RpPpPjyCAmIq9tySZm0Hk3Ltw8KWsqeNq5I7gS9AR9RzbVHb/l+eiMI1CqSRvW9R47HXcUu/epmEzY8ebFAg==} + '@opentelemetry/exporter-metrics-otlp-http@0.217.0': + resolution: {integrity: sha512-1zkMzzhiNJdVmLxuwkltqWGw4fOOam47bqRxmuQNjyKJe/9NmY5cIrZ4kiQV7sVGxoOgT0ZvGUfLcjvtpC/b9Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.202.0': - resolution: {integrity: sha512-6RvQqZHAPFiwL1OKRJe4ta6SgJx/g8or41B+OovVVEie3HeCDhDGL9S1VJNkBozUz6wTY8a47fQwdMrCOUdMhQ==} + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0': + resolution: {integrity: sha512-nfxt/KxVGFkjkO/M+58y1ugHu/dwPtxG4eYq0KApcQ7xk5CHzhdn+IuLZfDSvNDrJ3Uy5q++Fj/wbK7i8yryfQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.202.0': - resolution: {integrity: sha512-d5wLdbNA3ahpSeD0I34vbDFMTh4vPsXemH0bKDXLeCVULCAjOJXuZmEiuRammiDgVvvX7CAb/IGLDz8d2QHvoA==} + '@opentelemetry/exporter-prometheus@0.217.0': + resolution: {integrity: sha512-U9MCXxJu0sBCh5aEkylYRR4xVIL8D1CW6dGwvYXbfFr0qveSorfD0XJchCAWoW6QfAAIcY/yxjf4Dj8OgkHBPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0': + resolution: {integrity: sha512-fPZs2fw7veLH3pEKu8vSepUa2fQpAE2P7al6qU10aH9GrEJJ8YaPgsd5xON7by5rbcEVS71FOU2aWyK6nzB7VQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -1626,14 +1651,20 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.202.0': - resolution: {integrity: sha512-z3vzdMclCETGIn8uUBgpz7w651ftCiH2qh3cewhBk+rF0EYPNQ3mJvyxktLnKIBZ/ci0zUknAzzYC7LIIZmggQ==} + '@opentelemetry/exporter-trace-otlp-http@0.217.0': + resolution: {integrity: sha512-38YQoqtYjglz2GV94LGUN/djLvxtvGIQO68o6qAFPVshjmwSdX1F2i0c7vn3lEl1L5B/YqjB/bgKXaVx7KO+RQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.0.1': - resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} + '@opentelemetry/exporter-trace-otlp-proto@0.217.0': + resolution: {integrity: sha512-nPV8gKHUiSuTZpQcnZU3/pBlK7crSyEGpZuh5MtWySB0vv6NNG0QvvfKitQt+Fc2Mc6qfyU54KlZcurwoTbrVg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@2.7.1': + resolution: {integrity: sha512-mfsD9bKAxcKrh5+y08TPodvClBO0CznBE3p79YAGnO81WI4LrdsGA65T53e4iTSbCalW4WaUpkbeJcbpyIUHfg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -1662,14 +1693,26 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.217.0': + resolution: {integrity: sha512-24ucQMjz7Y34Kw3trbxL2ZrssbtgWnR+Clpaa+YdeWuuyH3Cvk23Q03PcQvqiZrDvt8AmQmjgg9v6Y9PHoxG7w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.202.0': resolution: {integrity: sha512-nMEOzel+pUFYuBJg2znGmHJWbmvMbdX5/RhoKNKowguMbURhz0fwik5tUKplLcUtl8wKPL1y9zPnPxeBn65N0Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.202.0': - resolution: {integrity: sha512-yIEHVxFA5dmYif7lZbbB66qulLLhrklj6mI2X3cuGW5hYPyUErztEmbroM+6teu/XobBi9bLHid2VT4NIaRuGg==} + '@opentelemetry/otlp-exporter-base@0.217.0': + resolution: {integrity: sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.217.0': + resolution: {integrity: sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -1680,14 +1723,20 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.0.1': - resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} + '@opentelemetry/otlp-transformer@0.217.0': + resolution: {integrity: sha512-MKK8UHKFUOGAvbZRWh90MhwHG+Fxm6OROBdjKPCF+HQobjuJ/Kuf8Chs8CR45X1aqotxrMj7OxTdsXe8sXuGVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@2.7.1': + resolution: {integrity: sha512-RJid6E2CKyeGfKBzXKF21ejabGMHypFkPAh3qZ+NvI+SGjuIye79t3PmiqcDgtRzdKH6ynXzbfslQ8DfpRUg2A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.0.1': - resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} + '@opentelemetry/propagator-jaeger@2.7.1': + resolution: {integrity: sha512-KMjVBHzP4N60bOzxja76M1F1hZZ43lGPga5ix+mkv9+kk1nx9SbkxSvJsMbuVUxdPQmsPTqGShmhN8ulrMOg6Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1710,14 +1759,26 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.217.0': + resolution: {integrity: sha512-BB+PcHItcZDL63dPMW+mJvwN9rk37wuIDjRxbVlg6pPDvDR/7GL7UJHbGsllgoggOoTimsKgENaWPoGch/oE1A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.0.1': resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.202.0': - resolution: {integrity: sha512-SF9vXWVd9I5CZ69mW3GfwfLI2SHgyvEqntcg0en5y8kRp5+2PPoa3Mkgj0WzFLrbSgTw4PsXn7c7H6eSdrtV0w==} + '@opentelemetry/sdk-metrics@2.7.1': + resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.217.0': + resolution: {integrity: sha512-K/60pSv42+NQiZKy1pAH18nYDkxltsDV4O3SJ233J0E9raU1ksyL9gsKuS8p30bYBb4AMPCfDuutHQaHYpcv0Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -1728,8 +1789,14 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.0.1': - resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} + '@opentelemetry/sdk-trace-base@2.7.1': + resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.7.1': + resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -1787,6 +1854,9 @@ packages: '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -2904,6 +2974,9 @@ packages: cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + cjs-module-lexer@2.2.1: + resolution: {integrity: sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==} + class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -3379,6 +3452,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -4129,6 +4205,10 @@ packages: import-in-the-middle@1.15.0: resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-in-the-middle@3.3.3: + resolution: {integrity: sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==} + engines: {node: '>=18'} + import-local@3.2.0: resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} engines: {node: '>=8'} @@ -5412,6 +5492,10 @@ packages: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} + protobufjs@8.0.1: + resolution: {integrity: sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5571,6 +5655,10 @@ packages: resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} @@ -6624,6 +6712,11 @@ packages: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -7405,6 +7498,12 @@ snapshots: dependencies: '@grpc/proto-loader': 0.8.0 '@js-sdsl/ordered-map': 4.4.2 + optional: true + + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 '@grpc/proto-loader@0.7.15': dependencies: @@ -7420,6 +7519,14 @@ snapshots: long: 5.3.2 protobufjs: 7.6.5 yargs: 17.7.3 + optional: true + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 '@humanwhocodes/config-array@0.13.0': dependencies: @@ -7902,7 +8009,7 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/microservices': 11.1.19(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@nestjs/websockets': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -7920,7 +8027,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.4 - '@nestjs/microservices@11.1.19(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/microservices@11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) @@ -7929,7 +8036,7 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.14.4 '@nestjs/websockets': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) cache-manager: 5.7.6 ioredis: 5.10.1 @@ -8017,7 +8124,7 @@ snapshots: '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/microservices': 11.1.19(@grpc/grpc-js@1.14.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))': @@ -8064,9 +8171,19 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs@0.217.0': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api@1.9.1': {} - '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/configuration@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + yaml: 2.9.0 + + '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -8080,15 +8197,15 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-grpc@0.217.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.3 + '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) '@opentelemetry/exporter-logs-otlp-http@0.202.0(@opentelemetry/api@1.9.1)': dependencies: @@ -8099,65 +8216,75 @@ snapshots: '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-http@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-proto@0.217.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.217.0(@opentelemetry/api@1.9.1)': dependencies: + '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-http@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-proto@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-grpc@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-prometheus@0.217.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/exporter-trace-otlp-http@0.202.0(@opentelemetry/api@1.9.1)': dependencies: @@ -8168,21 +8295,30 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-http@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/exporter-zipkin@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 '@opentelemetry/instrumentation-express@0.51.1(@opentelemetry/api@1.9.1)': @@ -8221,19 +8357,34 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.217.0 + import-in-the-middle: 3.3.3 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + '@opentelemetry/otlp-exporter-base@0.202.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-exporter-base@0.217.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.3 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-grpc-exporter-base@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@grpc/grpc-js': 1.14.4 + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.217.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.1)': dependencies: @@ -8246,15 +8397,26 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) protobufjs: 7.6.5 - '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-transformer@0.217.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + protobufjs: 8.0.1 - '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/propagator-b3@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/propagator-jaeger@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.1)': dependencies: @@ -8275,36 +8437,53 @@ snapshots: '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-node@0.202.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.202.0 - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-http': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-node@0.217.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.217.0 + '@opentelemetry/configuration': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-proto': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-proto': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-prometheus': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-proto': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-zipkin': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.217.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 transitivePeerDependencies: - supports-color @@ -8316,12 +8495,19 @@ snapshots: '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.40.0 - '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions@1.40.0': {} @@ -8371,6 +8557,8 @@ snapshots: '@protobufjs/float@1.0.2': {} + '@protobufjs/inquire@1.1.2': {} + '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} @@ -9818,6 +10006,8 @@ snapshots: cjs-module-lexer@1.4.3: {} + cjs-module-lexer@2.2.1: {} + class-transformer@0.5.1: {} class-validator@0.14.4: @@ -10343,6 +10533,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -11336,6 +11528,12 @@ snapshots: cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 + import-in-the-middle@3.3.3: + dependencies: + cjs-module-lexer: 2.2.1 + es-module-lexer: 2.3.1 + module-details-from-path: 1.0.4 + import-local@3.2.0: dependencies: pkg-dir: 4.2.0 @@ -12787,6 +12985,21 @@ snapshots: '@types/node': 20.19.39 long: 5.3.2 + protobufjs@8.0.1: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 20.19.39 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -13041,6 +13254,13 @@ snapshots: transitivePeerDependencies: - supports-color + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + require-main-filename@2.0.0: {} resend@6.12.3: @@ -14188,6 +14408,8 @@ snapshots: yaml@2.3.1: {} + yaml@2.9.0: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 From 333944c91dc959a27c338a3320eb72bce0fca368 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:26:28 +0530 Subject: [PATCH 35/58] build: bump protobufjs from 8.0.1 to 8.7.2 (#1714) Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 8.0.1 to 8.7.2. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v8.0.1...protobufjs-v8.7.2) --- updated-dependencies: - dependency-name: protobufjs dependency-version: 8.7.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba1b13de9..1ce203204 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -331,7 +331,7 @@ importers: version: 8.20.0 protobufjs: specifier: ^7.6.5 - version: 7.6.5 + version: 8.7.2 puppeteer: specifier: ^21.11.0 version: 21.11.0(typescript@5.9.3) @@ -5500,6 +5500,10 @@ packages: resolution: {integrity: sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w==} engines: {node: '>=12.0.0'} + protobufjs@8.7.2: + resolution: {integrity: sha512-oTVHV+oelUBtiu5iTuTNNZ0eLYsXSMxry4cgr30mayNkgIZL6qZ0IOQVPuSWGcyAaXKl/XgqwWHIC3a0khYVBA==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -12999,6 +13003,10 @@ snapshots: '@types/node': 20.19.39 long: 5.3.2 + protobufjs@8.7.2: + dependencies: + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 From fb541d7ca6c3a270ec011d32cde77b464d16bef8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:26:48 +0530 Subject: [PATCH 36/58] chore(deps): bump morgan from 1.10.1 to 1.11.0 (#1697) Bumps [morgan](https://github.com/expressjs/morgan) from 1.10.1 to 1.11.0. - [Release notes](https://github.com/expressjs/morgan/releases) - [Changelog](https://github.com/expressjs/morgan/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/morgan/compare/1.10.1...1.11.0) --- updated-dependencies: - dependency-name: morgan dependency-version: 1.11.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 20 ++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 82fcc75c9..09e7838a2 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "jwks-rsa": "^3.2.2", "linebyline": "^1.3.0", "moment": "^2.30.1", - "morgan": "^1.10.1", + "morgan": "^1.11.0", "multer": "^2.0.2", "nanoid": "^5.1.16", "nats": "^2.29.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ce203204..889c37ee5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,8 +273,8 @@ importers: specifier: ^2.30.1 version: 2.30.1 morgan: - specifier: ^1.10.1 - version: 1.10.1 + specifier: ^1.11.0 + version: 1.11.0 multer: specifier: ^2.0.2 version: 2.1.1 @@ -4979,8 +4979,8 @@ packages: moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} - morgan@1.10.1: - resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} + morgan@1.11.0: + resolution: {integrity: sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==} engines: {node: '>= 0.8.0'} ms@2.0.0: @@ -5176,10 +5176,6 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - on-finished@2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -12466,12 +12462,12 @@ snapshots: moment@2.30.1: {} - morgan@1.10.1: + morgan@1.11.0: dependencies: basic-auth: 2.0.1 debug: 2.6.9 depd: 2.0.0 - on-finished: 2.3.0 + on-finished: 2.4.1 on-headers: 1.1.0 transitivePeerDependencies: - supports-color @@ -12665,10 +12661,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - on-finished@2.3.0: - dependencies: - ee-first: 1.1.1 - on-finished@2.4.1: dependencies: ee-first: 1.1.1 From 9eae1f2f4409c0668157d447f24537906068c653 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:27:37 +0530 Subject: [PATCH 37/58] ci: bump ossf/scorecard-action (#1662) Bumps the minor-actions group with 1 update in the / directory: [ossf/scorecard-action](https://github.com/ossf/scorecard-action). Updates `ossf/scorecard-action` from 2.3.1 to 2.4.4 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/0864cf19026789058feabb7e87baa5f140aac736...2d1146689b8cda280b9bc96326124645441f03bc) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-version: 2.4.3 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 971dc081f..35a432884 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: results_file: results.sarif results_format: sarif From f6630318a94a9ef64b08fe75183b198d2bf34ab9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:27:51 +0530 Subject: [PATCH 38/58] build: bump redis in the minor-docker-compose group across 1 directory (#1709) Bumps the minor-docker-compose group with 1 update in the / directory: redis. Updates `redis` from 8.0-alpine to 8.10-alpine --- updated-dependencies: - dependency-name: redis dependency-version: 8.10-alpine dependency-type: direct:production dependency-group: minor-docker-compose ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docker-compose-dev.yml | 2 +- docker-compose.redis.yml | 2 +- docker-compose.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 2aa4b0478..b4ef82954 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -12,7 +12,7 @@ services: # volumes: # - ./nats-server.conf:/nats-server.conf # Mount the config file redis: - image: redis:8.0-alpine + image: redis:8.10-alpine restart: always ports: - '6379:6379' diff --git a/docker-compose.redis.yml b/docker-compose.redis.yml index 9b2389a0d..f37765bb3 100644 --- a/docker-compose.redis.yml +++ b/docker-compose.redis.yml @@ -1,7 +1,7 @@ version: '3' services: redis: - image: redis:8.0-alpine + image: redis:8.10-alpine restart: always ports: - '6379:6379' diff --git a/docker-compose.yml b/docker-compose.yml index 38996d977..1a7358709 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: - ./nats-server.conf:/nats-server.conf:ro redis: container_name: credebl-redis - image: redis:8.0-alpine + image: redis:8.10-alpine restart: always ports: - '6379:6379' From 85139dc674e2f2e8a273b9a40fa3a1e0fb527bdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:28:10 +0530 Subject: [PATCH 39/58] chore(deps): bump uuid from 9.0.1 to 14.0.0 (#1658) Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.1 to 14.0.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v9.0.1...v14.0.0) --- updated-dependencies: - dependency-name: uuid dependency-version: 14.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/common/package.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/libs/common/package.json b/libs/common/package.json index 7d396649a..3737b9f13 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -28,7 +28,7 @@ "dotenv": "^16.0.3", "nats": "^2.15.1", "rxjs": "^7.8.1", - "uuid": "^9.0.0" + "uuid": "^14.0.0" }, "devDependencies": { "@types/crypto-js": "^4.1.1", diff --git a/package.json b/package.json index 09e7838a2..7e6b9f730 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.31", "unzipper": "^0.12.3", - "uuid": "^9.0.1", + "uuid": "^14.0.0", "validator": "^13.15.35", "web-push": "^3.6.7", "winston": "^3.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 889c37ee5..0cd244812 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -369,8 +369,8 @@ importers: specifier: ^0.12.3 version: 0.12.3 uuid: - specifier: ^9.0.1 - version: 9.0.1 + specifier: ^14.0.0 + version: 14.0.0 validator: specifier: ^13.15.35 version: 13.15.35 @@ -532,8 +532,8 @@ importers: specifier: ^7.8.1 version: 7.8.2 uuid: - specifier: ^9.0.0 - version: 9.0.1 + specifier: ^14.0.0 + version: 14.0.0 devDependencies: '@types/crypto-js': specifier: ^4.1.1 @@ -6462,6 +6462,10 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -14146,6 +14150,8 @@ snapshots: uuid@11.1.1: {} + uuid@14.0.0: {} + uuid@3.4.0: optional: true @@ -14153,7 +14159,8 @@ snapshots: uuid@8.3.2: {} - uuid@9.0.1: {} + uuid@9.0.1: + optional: true v8-compile-cache-lib@3.0.1: {} From e1a0112eed532a7c03c567d83ed7f4ca4ad458a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:32:06 +0530 Subject: [PATCH 40/58] chore(deps): bump multer from 2.1.1 to 2.2.0 (#1686) Bumps [multer](https://github.com/expressjs/multer) from 2.1.1 to 2.2.0. - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.1.1...v2.2.0) --- updated-dependencies: - dependency-name: multer dependency-version: 2.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: Ajay Jadhav Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7e6b9f730..c25e3a4f1 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "linebyline": "^1.3.0", "moment": "^2.30.1", "morgan": "^1.11.0", - "multer": "^2.0.2", + "multer": "^2.2.0", "nanoid": "^5.1.16", "nats": "^2.29.3", "nestjs-cls": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cd244812..5c5db3a28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -276,8 +276,8 @@ importers: specifier: ^1.11.0 version: 1.11.0 multer: - specifier: ^2.0.2 - version: 2.1.1 + specifier: ^2.2.0 + version: 2.2.0 nanoid: specifier: ^5.1.16 version: 5.1.16 @@ -5003,6 +5003,10 @@ packages: resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} engines: {node: '>= 10.16.0'} + multer@2.2.0: + resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + engines: {node: '>= 10.16.0'} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -12505,6 +12509,13 @@ snapshots: concat-stream: 2.0.0 type-is: 1.6.18 + multer@2.2.0: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + mute-stream@2.0.0: {} nanoid@5.1.16: {} From 7dcc5d4fff22fbdc64a4cd25cd598dce23247240 Mon Sep 17 00:00:00 2001 From: Ajay Jadhav Date: Fri, 14 Aug 2026 14:10:16 +0530 Subject: [PATCH 41/58] revert: bump uuid from 9.0.1 to 14.0.0 (#1658) (#1716) uuid 14 is ESM-only and cannot be loaded by the repo's CommonJS jest runtime (SyntaxError: Unexpected token 'export'), breaking 8+ suites that import uuid transitively via NATSClient. The bump carried no security fix (uuid 9.0.1 has no advisories), so revert to keep the test suite green while the ESM migration is handled separately. Verified: test results on this revert match the pre-bump baseline exactly (19 pre-existing broken suites, 5 failed tests, 56 passed). Signed-off-by: Ajay Jadhav --- libs/common/package.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 17 +++++------------ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/libs/common/package.json b/libs/common/package.json index 3737b9f13..7d396649a 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -28,7 +28,7 @@ "dotenv": "^16.0.3", "nats": "^2.15.1", "rxjs": "^7.8.1", - "uuid": "^14.0.0" + "uuid": "^9.0.0" }, "devDependencies": { "@types/crypto-js": "^4.1.1", diff --git a/package.json b/package.json index c25e3a4f1..f13223a5c 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.31", "unzipper": "^0.12.3", - "uuid": "^14.0.0", + "uuid": "^9.0.1", "validator": "^13.15.35", "web-push": "^3.6.7", "winston": "^3.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c5db3a28..9ec07afb6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -369,8 +369,8 @@ importers: specifier: ^0.12.3 version: 0.12.3 uuid: - specifier: ^14.0.0 - version: 14.0.0 + specifier: ^9.0.1 + version: 9.0.1 validator: specifier: ^13.15.35 version: 13.15.35 @@ -532,8 +532,8 @@ importers: specifier: ^7.8.1 version: 7.8.2 uuid: - specifier: ^14.0.0 - version: 14.0.0 + specifier: ^9.0.0 + version: 9.0.1 devDependencies: '@types/crypto-js': specifier: ^4.1.1 @@ -6466,10 +6466,6 @@ packages: resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} hasBin: true - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} - hasBin: true - uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). @@ -14161,8 +14157,6 @@ snapshots: uuid@11.1.1: {} - uuid@14.0.0: {} - uuid@3.4.0: optional: true @@ -14170,8 +14164,7 @@ snapshots: uuid@8.3.2: {} - uuid@9.0.1: - optional: true + uuid@9.0.1: {} v8-compile-cache-lib@3.0.1: {} From fec49105afaed5cf5f54cc78f5b255d0e07f3e61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:47:52 +0530 Subject: [PATCH 42/58] chore(deps): bump the patch-updates group across 1 directory with 44 updates (#1718) Bumps the patch-updates group with 20 updates in the / directory: | Package | From | To | | --- | --- | --- | | [nodemailer](https://github.com/nodemailer/nodemailer) | `9.0.1` | `9.0.5` | | [papaparse](https://github.com/mholt/PapaParse) | `5.5.3` | `5.5.4` | | [qs](https://github.com/ljharb/qs) | `6.15.2` | `6.15.3` | | [unzipper](https://github.com/ZJONSSON/node-unzipper) | `0.12.3` | `0.12.5` | | [uuid](https://github.com/uuidjs/uuid) | `14.0.0` | `14.0.1` | | [@types/supertest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/supertest) | `7.2.0` | `7.2.1` | | [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.4.9` | `29.4.12` | | [@nestjs/bull](https://github.com/nestjs/bull) | `11.0.4` | `11.0.5` | | [@nestjs/cache-manager](https://github.com/nestjs/cache-manager) | `3.1.2` | `3.1.3` | | [@nestjs/cli](https://github.com/nestjs/nest-cli) | `11.0.10` | `11.0.24` | | [@nestjs/common](https://github.com/nestjs/nest/tree/HEAD/packages/common) | `11.1.19` | `11.1.29` | | [@nestjs/core](https://github.com/nestjs/nest/tree/HEAD/packages/core) | `11.1.19` | `11.1.29` | | [@nestjs/microservices](https://github.com/nestjs/nest/tree/HEAD/packages/microservices) | `11.1.19` | `11.1.29` | | [@nestjs/platform-express](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express) | `11.1.19` | `11.1.29` | | [@nestjs/platform-socket.io](https://github.com/nestjs/nest/tree/HEAD/packages/platform-socket.io) | `11.1.19` | `11.1.29` | | [@nestjs/swagger](https://github.com/nestjs/swagger) | `11.4.2` | `11.4.6` | | [@nestjs/testing](https://github.com/nestjs/nest/tree/HEAD/packages/testing) | `11.1.19` | `11.1.29` | | [@nestjs/typeorm](https://github.com/nestjs/typeorm) | `11.0.1` | `11.0.3` | | [@nestjs/websockets](https://github.com/nestjs/nest/tree/HEAD/packages/websockets) | `11.1.19` | `11.1.29` | | [nestjs-cls](https://github.com/Papooch/nestjs-cls) | `6.2.0` | `6.2.1` | Updates `nodemailer` from 9.0.1 to 9.0.5 - [Release notes](https://github.com/nodemailer/nodemailer/releases) - [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.5) Updates `papaparse` from 5.5.3 to 5.5.4 - [Release notes](https://github.com/mholt/PapaParse/releases) - [Changelog](https://github.com/mholt/PapaParse/blob/master/CHANGELOG.md) - [Commits](https://github.com/mholt/PapaParse/compare/5.5.3...5.5.4) Updates `qs` from 6.15.2 to 6.15.3 - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.15.2...v6.15.3) Updates `unzipper` from 0.12.3 to 0.12.5 - [Release notes](https://github.com/ZJONSSON/node-unzipper/releases) - [Commits](https://github.com/ZJONSSON/node-unzipper/commits) Updates `uuid` from 14.0.0 to 14.0.1 - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v14.0.0...v14.0.1) Updates `protobufjs` from 8.7.2 to 7.6.5 - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/protobufjs-v7.6.5/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v8.7.2...protobufjs-v7.6.5) Updates `@types/supertest` from 7.2.0 to 7.2.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/supertest) Updates `ts-jest` from 29.4.9 to 29.4.12 - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.4.9...v29.4.12) Updates `@nestjs/bull` from 11.0.4 to 11.0.5 - [Release notes](https://github.com/nestjs/bull/releases) - [Commits](https://github.com/nestjs/bull/compare/@nestjs/bull@11.0.4...@nestjs/bull@11.0.5) Updates `@nestjs/cache-manager` from 3.1.2 to 3.1.3 - [Release notes](https://github.com/nestjs/cache-manager/releases) - [Commits](https://github.com/nestjs/cache-manager/compare/3.1.2...3.1.3) Updates `@nestjs/cli` from 11.0.10 to 11.0.24 - [Release notes](https://github.com/nestjs/nest-cli/releases) - [Commits](https://github.com/nestjs/nest-cli/compare/11.0.10...11.0.24) Updates `@nestjs/common` from 11.1.19 to 11.1.29 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/common) Updates `@nestjs/core` from 11.1.19 to 11.1.29 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/core) Updates `@nestjs/microservices` from 11.1.19 to 11.1.29 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/microservices) Updates `@nestjs/platform-express` from 11.1.19 to 11.1.29 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/platform-express) Updates `@nestjs/platform-socket.io` from 11.1.19 to 11.1.29 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/platform-socket.io) Updates `@nestjs/swagger` from 11.4.2 to 11.4.6 - [Release notes](https://github.com/nestjs/swagger/releases) - [Commits](https://github.com/nestjs/swagger/compare/11.4.2...11.4.6) Updates `@nestjs/testing` from 11.1.19 to 11.1.29 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/testing) Updates `@nestjs/typeorm` from 11.0.1 to 11.0.3 - [Release notes](https://github.com/nestjs/typeorm/releases) - [Commits](https://github.com/nestjs/typeorm/compare/11.0.1...11.0.3) Updates `@nestjs/websockets` from 11.1.19 to 11.1.29 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.29/packages/websockets) Updates `nestjs-cls` from 6.2.0 to 6.2.1 - [Release notes](https://github.com/Papooch/nestjs-cls/releases) - [Commits](https://github.com/Papooch/nestjs-cls/compare/nestjs-cls@6.2.0...nestjs-cls@6.2.1) Updates `@angular-devkit/core` from 19.2.15 to 19.2.24 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/19.2.15...v19.2.24) Updates `@angular-devkit/schematics-cli` from 19.2.15 to 19.2.27 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/19.2.15...v19.2.27) Updates `@angular-devkit/schematics` from 19.2.15 to 19.2.24 - [Release notes](https://github.com/angular/angular-cli/releases) - [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md) - [Commits](https://github.com/angular/angular-cli/compare/19.2.15...v19.2.24) Updates `@nestjs/bull-shared` from 11.0.4 to 11.0.5 - [Release notes](https://github.com/nestjs/bull/releases) - [Commits](https://github.com/nestjs/bull/compare/@nestjs/bull-shared@11.0.4...@nestjs/bull-shared@11.0.5) Updates `@types/superagent` from 8.1.9 to 8.1.11 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/superagent) Updates `ansis` from 4.1.0 to 4.2.0 - [Release notes](https://github.com/webdiscus/ansis/releases) - [Changelog](https://github.com/webdiscus/ansis/blob/master/CHANGELOG.md) - [Commits](https://github.com/webdiscus/ansis/compare/v4.1.0...v4.2.0) Updates `baseline-browser-mapping` from 2.10.27 to 2.11.14 - [Release notes](https://github.com/web-platform-dx/baseline-browser-mapping/releases) - [Commits](https://github.com/web-platform-dx/baseline-browser-mapping/compare/v2.10.27...v2.11.14) Updates `browserslist` from 4.28.2 to 4.28.8 - [Release notes](https://github.com/browserslist/browserslist/releases) - [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md) - [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8) Updates `caniuse-lite` from 1.0.30001792 to 1.0.30001809 - [Commits](https://github.com/browserslist/caniuse-lite/compare/1.0.30001792...1.0.30001809) Updates `chardet` from 2.1.1 to 2.2.0 - [Release notes](https://github.com/runk/node-chardet/releases) - [Commits](https://github.com/runk/node-chardet/compare/v2.1.1...v2.2.0) Updates `electron-to-chromium` from 1.5.352 to 1.5.406 - [Changelog](https://github.com/Kilian/electron-to-chromium/blob/main/CHANGELOG.md) - [Commits](https://github.com/Kilian/electron-to-chromium/compare/v1.5.352...v1.5.406) Updates `engine.io` from 6.6.7 to 6.6.9 - [Release notes](https://github.com/socketio/socket.io/releases) - [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io/compare/engine.io@6.6.7...engine.io@6.6.9) Updates `es-module-lexer` from 1.7.0 to 2.3.1 - [Release notes](https://github.com/guybedford/es-module-lexer/releases) - [Commits](https://github.com/guybedford/es-module-lexer/compare/1.7.0...2.3.1) Updates `js-yaml` from 3.14.2 to 3.15.1 - [Changelog](https://github.com/nodeca/js-yaml/blob/3.15.1/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.1) Updates `node-releases` from 2.0.38 to 2.0.53 - [Commits](https://github.com/chicoxyzzy/node-releases/compare/v2.0.38...v2.0.53) Updates `socket.io-adapter` from 2.5.6 to 2.5.8 - [Release notes](https://github.com/socketio/socket.io/releases) - [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io/compare/socket.io-adapter@2.5.6...socket.io-adapter@2.5.8) Updates `swagger-ui-dist` from 5.32.4 to 5.32.5 - [Release notes](https://github.com/swagger-api/swagger-ui/releases) - [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.32.4...v5.32.5) Updates `terser-webpack-plugin` from 5.5.0 to 5.6.1 - [Release notes](https://github.com/webpack/minimizer-webpack-plugin/releases) - [Changelog](https://github.com/webpack/minimizer-webpack-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/minimizer-webpack-plugin/compare/v5.5.0...v5.6.1) Updates `terser` from 5.47.1 to 5.50.0 - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/compare/v5.47.1...v5.50.0) Updates `update-browserslist-db` from 1.2.3 to 1.3.1 - [Release notes](https://github.com/browserslist/update-db/releases) - [Changelog](https://github.com/browserslist/update-db/blob/main/CHANGELOG.md) - [Commits](https://github.com/browserslist/update-db/compare/1.2.3...1.3.1) Updates `watchpack` from 2.5.1 to 2.5.2 - [Release notes](https://github.com/webpack/watchpack/releases) - [Changelog](https://github.com/webpack/watchpack/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/watchpack/compare/v2.5.1...v2.5.2) Updates `webpack-sources` from 3.4.1 to 3.5.1 - [Release notes](https://github.com/webpack/webpack-sources/releases) - [Changelog](https://github.com/webpack/webpack-sources/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-sources/compare/v3.4.1...v3.5.1) Updates `webpack` from 5.100.2 to 5.106.2 - [Release notes](https://github.com/webpack/webpack/releases) - [Changelog](https://github.com/webpack/webpack/blob/main/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack/compare/v5.100.2...v5.106.2) --- updated-dependencies: - dependency-name: nodemailer dependency-version: 9.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: papaparse dependency-version: 5.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: qs dependency-version: 6.15.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: unzipper dependency-version: 0.12.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: uuid dependency-version: 14.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: protobufjs dependency-version: 7.6.5 dependency-type: direct:production update-type: version-update:semver-major dependency-group: patch-updates - dependency-name: "@types/supertest" dependency-version: 7.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: ts-jest dependency-version: 29.4.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/bull" dependency-version: 11.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/cache-manager" dependency-version: 3.1.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/cli" dependency-version: 11.0.24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/common" dependency-version: 11.1.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/core" dependency-version: 11.1.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/microservices" dependency-version: 11.1.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/platform-express" dependency-version: 11.1.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/platform-socket.io" dependency-version: 11.1.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/swagger" dependency-version: 11.4.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/testing" dependency-version: 11.1.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/typeorm" dependency-version: 11.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/websockets" dependency-version: 11.1.29 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: nestjs-cls dependency-version: 6.2.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@angular-devkit/core" dependency-version: 19.2.24 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@angular-devkit/schematics-cli" dependency-version: 19.2.27 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@angular-devkit/schematics" dependency-version: 19.2.24 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@nestjs/bull-shared" dependency-version: 11.0.5 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: "@types/superagent" dependency-version: 8.1.11 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: ansis dependency-version: 4.2.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: baseline-browser-mapping dependency-version: 2.11.14 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: browserslist dependency-version: 4.28.8 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: caniuse-lite dependency-version: 1.0.30001809 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: chardet dependency-version: 2.2.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: electron-to-chromium dependency-version: 1.5.406 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: engine.io dependency-version: 6.6.9 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: es-module-lexer dependency-version: 2.3.1 dependency-type: indirect update-type: version-update:semver-major dependency-group: patch-updates - dependency-name: js-yaml dependency-version: 3.15.1 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: node-releases dependency-version: 2.0.53 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: socket.io-adapter dependency-version: 2.5.8 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: swagger-ui-dist dependency-version: 5.32.5 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: terser-webpack-plugin dependency-version: 5.6.1 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: terser dependency-version: 5.50.0 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: update-browserslist-db dependency-version: 1.3.1 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: watchpack dependency-version: 2.5.2 dependency-type: indirect update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: webpack-sources dependency-version: 3.5.1 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates - dependency-name: webpack dependency-version: 5.106.2 dependency-type: indirect update-type: version-update:semver-minor dependency-group: patch-updates ... Signed-off-by: dependabot[bot] Signed-off-by: Ajay Jadhav Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 12 +- pnpm-lock.yaml | 978 +++++++++++++++++++++++--------------------- pnpm-workspace.yaml | 26 +- 3 files changed, 520 insertions(+), 496 deletions(-) diff --git a/package.json b/package.json index f13223a5c..a627a5cc3 100644 --- a/package.json +++ b/package.json @@ -99,9 +99,9 @@ "nestjs-typeorm-paginate": "catalog:", "node-html-to-image": "^4.0.0", "node-qpdf2": "^2.0.0", - "nodemailer": "^9.0.1", + "nodemailer": "^9.0.5", "p-limit": "^3.1.0", - "papaparse": "^5.5.3", + "papaparse": "^5.5.4", "passport": "^0.6.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", @@ -110,7 +110,7 @@ "pg": "^8.18.0", "puppeteer": "^21.11.0", "qrcode": "^1.5.4", - "qs": "^6.15.2", + "qs": "^6.15.3", "reflect-metadata": "^0.1.14", "resend": "^6.9.2", "rimraf": "^3.0.2", @@ -119,7 +119,7 @@ "socket.io-client": "^4.8.3", "swagger-ui-express": "^5.0.1", "typeorm": "^0.3.31", - "unzipper": "^0.12.3", + "unzipper": "^0.12.5", "uuid": "^9.0.1", "validator": "^13.15.35", "web-push": "^3.6.7", @@ -140,7 +140,7 @@ "@types/node": "^20.19.33", "@types/passport-jwt": "3.0.9", "@types/passport-local": "^1.0.38", - "@types/supertest": "^7.2.0", + "@types/supertest": "^7.2.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.57.1", @@ -156,7 +156,7 @@ "prettier": "^3.8.1", "prisma": "^5.22.0", "supertest": "^7.2.2", - "ts-jest": "^29.4.6", + "ts-jest": "^29.4.12", "ts-loader": "^9.5.4", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ec07afb6..409d365ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,38 +10,38 @@ catalogs: specifier: ^4.0.1 version: 4.0.1 '@nestjs/bull': - specifier: ^11.0.3 - version: 11.0.4 + specifier: ^11.0.5 + version: 11.0.5 '@nestjs/cache-manager': - specifier: ^3.0.1 - version: 3.1.2 + specifier: ^3.1.3 + version: 3.1.3 '@nestjs/cli': - specifier: 11.0.10 - version: 11.0.10 + specifier: 11.0.24 + version: 11.0.24 '@nestjs/common': - specifier: ^11.1.6 - version: 11.1.19 + specifier: ^11.1.29 + version: 11.1.29 '@nestjs/config': specifier: ^4.0.2 version: 4.0.4 '@nestjs/core': - specifier: ^11.1.6 - version: 11.1.19 + specifier: ^11.1.29 + version: 11.1.29 '@nestjs/jwt': specifier: ^11.0.0 version: 11.0.2 '@nestjs/microservices': - specifier: ^11.1.6 - version: 11.1.19 + specifier: ^11.1.29 + version: 11.1.29 '@nestjs/passport': specifier: ^11.0.5 version: 11.0.5 '@nestjs/platform-express': - specifier: ^11.1.6 - version: 11.1.19 + specifier: ^11.1.29 + version: 11.1.29 '@nestjs/platform-socket.io': - specifier: ^11.1.6 - version: 11.1.19 + specifier: ^11.1.29 + version: 11.1.29 '@nestjs/schedule': specifier: ^6.0.1 version: 6.1.3 @@ -49,20 +49,20 @@ catalogs: specifier: ^11.0.7 version: 11.1.0 '@nestjs/swagger': - specifier: ^11.2.0 - version: 11.4.2 + specifier: ^11.4.6 + version: 11.4.6 '@nestjs/testing': - specifier: ^11.1.6 - version: 11.1.19 + specifier: ^11.1.29 + version: 11.1.29 '@nestjs/typeorm': - specifier: ^11.0.0 - version: 11.0.1 + specifier: ^11.0.3 + version: 11.0.3 '@nestjs/websockets': - specifier: ^11.1.6 - version: 11.1.19 + specifier: ^11.1.29 + version: 11.1.29 nestjs-cls: - specifier: ^6.0.1 - version: 6.2.0 + specifier: ^6.2.1 + version: 6.2.1 nestjs-rate-limiter: specifier: ^3.1.0 version: 3.1.0 @@ -82,49 +82,49 @@ importers: version: 3.1044.0 '@nestjs/axios': specifier: 'catalog:' - version: 4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2) + version: 4.0.1(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2) '@nestjs/bull': specifier: 'catalog:' - version: 11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(bull@4.16.5) + version: 11.0.5(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(bull@4.16.5) '@nestjs/cache-manager': specifier: 'catalog:' - version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) + version: 3.1.3(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: 'catalog:' - version: 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/config': specifier: 'catalog:' - version: 4.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.4(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/jwt': specifier: 'catalog:' - version: 11.0.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2)) + version: 11.0.2(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2)) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/websockets@11.1.29)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/passport': specifier: 'catalog:' - version: 11.0.5(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0) + version: 11.0.5(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0) '@nestjs/platform-express': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29) '@nestjs/platform-socket.io': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(rxjs@7.8.2) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.29)(rxjs@7.8.2) '@nestjs/schedule': specifier: 'catalog:' - version: 6.1.3(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) + version: 6.1.3(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29) '@nestjs/swagger': specifier: 'catalog:' - version: 11.4.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) + version: 11.4.6(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) '@nestjs/typeorm': specifier: 'catalog:' - version: 11.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) + version: 11.0.3(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) '@nestjs/websockets': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/platform-socket.io@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.1 @@ -286,7 +286,7 @@ importers: version: 2.29.3 nestjs-cls: specifier: 'catalog:' - version: 6.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 6.2.1(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) nestjs-rate-limiter: specifier: 'catalog:' version: 3.1.0 @@ -295,7 +295,7 @@ importers: version: 1.0.9 nestjs-typeorm-paginate: specifier: 'catalog:' - version: 4.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) + version: 4.1.0(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))) node-html-to-image: specifier: ^4.0.0 version: 4.0.0 @@ -303,14 +303,14 @@ importers: specifier: ^2.0.0 version: 2.0.0 nodemailer: - specifier: ^9.0.1 - version: 9.0.1 + specifier: ^9.0.5 + version: 9.0.5 p-limit: specifier: ^3.1.0 version: 3.1.0 papaparse: - specifier: ^5.5.3 - version: 5.5.3 + specifier: ^5.5.4 + version: 5.5.4 passport: specifier: ^0.6.0 version: 0.6.0 @@ -331,7 +331,7 @@ importers: version: 8.20.0 protobufjs: specifier: ^7.6.5 - version: 8.7.2 + version: 7.6.5 puppeteer: specifier: ^21.11.0 version: 21.11.0(typescript@5.9.3) @@ -339,8 +339,8 @@ importers: specifier: ^1.5.4 version: 1.5.4 qs: - specifier: ^6.15.2 - version: 6.15.2 + specifier: ^6.15.3 + version: 6.15.3 reflect-metadata: specifier: ^0.1.14 version: 0.1.14 @@ -366,8 +366,8 @@ importers: specifier: ^0.3.31 version: 0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) unzipper: - specifier: ^0.12.3 - version: 0.12.3 + specifier: ^0.12.5 + version: 0.12.5 uuid: specifier: ^9.0.1 version: 9.0.1 @@ -389,13 +389,13 @@ importers: devDependencies: '@nestjs/cli': specifier: 'catalog:' - version: 11.0.10(@types/node@20.19.39)(prettier@3.8.3) + version: 11.0.24(@types/node@20.19.39)(prettier@3.8.3) '@nestjs/schematics': specifier: 'catalog:' version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3) '@nestjs/testing': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29) '@types/express': specifier: ^4.17.25 version: 4.17.25 @@ -415,8 +415,8 @@ importers: specifier: ^1.0.38 version: 1.0.38 '@types/supertest': - specifier: ^7.2.0 - version: 7.2.0 + specifier: ^7.2.1 + version: 7.2.1 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -463,11 +463,11 @@ importers: specifier: ^7.2.2 version: 7.2.2 ts-jest: - specifier: ^29.4.6 - version: 29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))(typescript@5.9.3) + specifier: ^29.4.12 + version: 29.4.12(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.4 - version: 9.5.7(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.7(typescript@5.9.3)(webpack@5.106.2) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.19.39)(typescript@5.9.3) @@ -488,25 +488,25 @@ importers: dependencies: '@nestjs/axios': specifier: 'catalog:' - version: 4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2) + version: 4.0.1(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2) '@nestjs/cache-manager': specifier: 'catalog:' - version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) + version: 3.1.3(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2) '@nestjs/common': specifier: 'catalog:' - version: 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/core': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/websockets@11.1.29)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/swagger': specifier: 'catalog:' - version: 11.4.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) + version: 11.4.6(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) '@nestjs/testing': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29) '@prisma/client': specifier: ^5.1.1 version: 5.22.0(prisma@5.22.0) @@ -552,13 +552,13 @@ importers: dependencies: '@nestjs/common': specifier: 'catalog:' - version: 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/microservices': specifier: 'catalog:' - version: 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + version: 11.1.29(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/websockets@11.1.29)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@nestjs/testing': specifier: 'catalog:' - version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19) + version: 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29) aws-sdk: specifier: ^2.1510.0 version: 2.1693.0 @@ -575,8 +575,8 @@ importers: packages: - '@angular-devkit/core@19.2.15': - resolution: {integrity: sha512-pU2RZYX6vhd7uLSdLwPnuBcr0mXJSjp3EgOXKsrlQFQZevc+Qs+2JdXgIElnOT/aDqtRtriDmLlSbtdE8n3ZbA==} + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^4.0.0 @@ -584,8 +584,8 @@ packages: chokidar: optional: true - '@angular-devkit/core@19.2.24': - resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + '@angular-devkit/core@19.2.27': + resolution: {integrity: sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^4.0.0 @@ -593,19 +593,19 @@ packages: chokidar: optional: true - '@angular-devkit/schematics-cli@19.2.15': - resolution: {integrity: sha512-1ESFmFGMpGQmalDB3t2EtmWDGv6gOFYBMxmHO2f1KI/UDl8UmZnCGL4mD3EWo8Hv0YIsZ9wOH9Q7ZHNYjeSpzg==} + '@angular-devkit/schematics-cli@19.2.27': + resolution: {integrity: sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/schematics@19.2.15': - resolution: {integrity: sha512-kNOJ+3vekJJCQKWihNmxBkarJzNW09kP5a9E1SRNiQVNOUEeSwcRR0qYotM65nx821gNzjjhJXnAZ8OazWldrg==} - engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@angular-devkit/schematics@19.2.24': resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics@19.2.27': + resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -735,6 +735,10 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.3': resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} engines: {node: '>=6.9.0'} @@ -777,6 +781,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -1112,8 +1120,8 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.3.2': - resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1121,8 +1129,8 @@ packages: '@types/node': optional: true - '@inquirer/prompts@7.8.0': - resolution: {integrity: sha512-JHwGbQ6wjf1dxxnalDYpZwZxUEosT+6CPGD9Zh4sm9WXdtUp9XODCQD3NjSTmu+0OAyxWXNOqf0spjIymJa2Tw==} + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' @@ -1173,10 +1181,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/cliui@9.0.0': - resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} - engines: {node: '>=18'} - '@istanbuljs/load-nyc-config@1.1.0': resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -1320,21 +1324,21 @@ packages: axios: ^1.3.1 rxjs: ^7.0.0 - '@nestjs/bull-shared@11.0.4': - resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} + '@nestjs/bull-shared@11.0.5': + resolution: {integrity: sha512-QI7GHPk4oePBwszAZn/Kz5hgB79KijB1RDoo+E/BrSt33sHUrvRZXVyO0+knZleyloRYy0LT2VwsqSP/aQvg4Q==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - '@nestjs/bull@11.0.4': - resolution: {integrity: sha512-QVz2PR/rJF/isy7otVnMTSqLf/O71p9Ka7lBZt9Gm+NQFv8fcH2L11GL7TA0whyCcw/kAX5iRepUXz/wed4JoA==} + '@nestjs/bull@11.0.5': + resolution: {integrity: sha512-z1TKz7NHlsZv5ss27Lp32jzPe8ynw/COWoywMu32k8VXWsDSAy7mvDSBEX3Go0ekO7CzhAzqPWrl//GU3QuMFA==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 bull: ^3.3 || ^4.0.0 - '@nestjs/cache-manager@3.1.2': - resolution: {integrity: sha512-Eglt8lUzC3Q3OZ2hFt4vLZ190M94YSJXUiKo67K/zlUgZQGtvxL0AYeKbG96x8+1gJTF7QhFpYw/RkQ28416Mw==} + '@nestjs/cache-manager@3.1.3': + resolution: {integrity: sha512-HMtiOfHz75NZX7mJn1VnZGLSachVI04TnUc5wvEogIaKwk5BDQHgtP5htxreizjv7oxKalJbuTyxtiF6bE+bgQ==} peerDependencies: '@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0 '@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0 @@ -1342,12 +1346,12 @@ packages: keyv: '>=5' rxjs: ^7.8.1 - '@nestjs/cli@11.0.10': - resolution: {integrity: sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==} + '@nestjs/cli@11.0.24': + resolution: {integrity: sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: - '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 '@swc/core': ^1.3.62 peerDependenciesMeta: '@swc/cli': @@ -1355,8 +1359,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.19': - resolution: {integrity: sha512-qeiTt2tv+e5QyDKqG8HlVZb2wx64FEaSGFJouqTSRs+kG44iTfl3xlz1XqVped+rihx4hmjWgL5gkhtdK3E6+Q==} + '@nestjs/common@11.1.29': + resolution: {integrity: sha512-zkeNRlfiQIH/044r5zphjNzKYxBRC4O00onTdCsH2qq0R30Ixo0gOS9so5TadEbJJCuGNm0Vx3PBE2FG2vkBdA==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -1374,8 +1378,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.1.19': - resolution: {integrity: sha512-6nJkWa2efrYi+XlU686J9y5L7OvxpLVjT0T/sxRKE7Jvpffiihelup4WSvLvRhdHDjj/5SuoWEwqReXAaaeHmw==} + '@nestjs/core@11.1.29': + resolution: {integrity: sha512-ANXnZxirMNAY+JuRHCIYIdTfbhX3rizeSbJloub/4EYwpIfCuUoaz9woOofWmywYYc3gsVkLxVZV03gqntI8vA==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -1410,8 +1414,8 @@ packages: class-validator: optional: true - '@nestjs/microservices@11.1.19': - resolution: {integrity: sha512-3Oja56ydTlSaui19/i7gYM0MMqz/w4UR2aqZeL4K8B+Fq0Ztg3zHb8et76atToJGpSCevJLEsoEMOMaGgzRwfg==} + '@nestjs/microservices@11.1.29': + resolution: {integrity: sha512-Z97VBDwgarzj08ZoC5ItJDIkUiDsAXm3kx2lWc0YtpnOCtQxmGe6tM2eI1tlKJWXDIWBi6iNxtOCJU76F+Y/cA==} peerDependencies: '@grpc/grpc-js': '*' '@nestjs/common': ^11.0.0 @@ -1452,14 +1456,14 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - '@nestjs/platform-express@11.1.19': - resolution: {integrity: sha512-Vpdv8jyCQdThfoTx+UTn+DRYr6H6X02YUqcpZ3qP6G3ZUwtVp7eS+hoQPGd4UuCnlnFG8Wqr2J9bGEzQdi1rIg==} + '@nestjs/platform-express@11.1.29': + resolution: {integrity: sha512-OngsAjdzM+SbbmP1XojKfBgl0Lj7u8b7IK5duwpFymzNP5EmjS8ujvqck9M8h0L14I91aRIFSjIpVfHj8WVtqw==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 - '@nestjs/platform-socket.io@11.1.19': - resolution: {integrity: sha512-gu1nPIEaP5Qjjg/Cl8wXyvwGpdZGzgbtK4KcH65YRAA+GTKUkIHb4BNpLJ27Ymq/wqLJKNEbCjajfzD0BEjMGA==} + '@nestjs/platform-socket.io@11.1.29': + resolution: {integrity: sha512-1p2KJpH6CYYpxPrtqpP65VdMgUqobGBPmx8vwOsF84/ki9YWiylG2+1q6kaUqYMtBdjreJDHgabuR5VazXav2Q==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 @@ -1480,10 +1484,10 @@ packages: prettier: optional: true - '@nestjs/swagger@11.4.2': - resolution: {integrity: sha512-aBihEogDMj/bLEcaqhkvyX/ZVWUw/bmnhKzR0zwUoyGJikvZyaq7rOPYl/H7Lxkkr3c90SJxyuv1AX2UT1WKlw==} + '@nestjs/swagger@11.4.6': + resolution: {integrity: sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==} peerDependencies: - '@fastify/static': ^8.0.0 || ^9.0.0 + '@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 class-transformer: '*' @@ -1497,8 +1501,8 @@ packages: class-validator: optional: true - '@nestjs/testing@11.1.19': - resolution: {integrity: sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ==} + '@nestjs/testing@11.1.29': + resolution: {integrity: sha512-u+aEp90Ly3uQbLoicZs+ZOrJTyvaD0fbd6dpnSgFZz5Bf77gbKKfr+xFE2JxqZuIH15HlBrqDL7/OL2qL8oG+A==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -1510,8 +1514,8 @@ packages: '@nestjs/platform-express': optional: true - '@nestjs/typeorm@11.0.1': - resolution: {integrity: sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==} + '@nestjs/typeorm@11.0.3': + resolution: {integrity: sha512-zJ+E5l7auVVA7c0PsvcMdyvRPKTUqU5s2ToYmOA2QEsXQ42qbUGtK4+1HlRfpHqBkCSXP+phiH4luvf9DyJNog==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 @@ -1519,8 +1523,8 @@ packages: rxjs: ^7.2.0 typeorm: ^0.3.0 || ^1.0.0-dev - '@nestjs/websockets@11.1.19': - resolution: {integrity: sha512-2qo8jtIwwwgkqAI1BtnJ02EaFLrRkKA39eYXS8IhZCHilhBHCWdjnJ5cLcFq4oF+s+KZ7LcLGD/3stxJy8ijzg==} + '@nestjs/websockets@11.1.29': + resolution: {integrity: sha512-Mg0sNK2k+PpamcJpDy9ZP24ZvM08xB6AZs061Kb6dhTesGxr6zrxlbZ7+40r5xwuxD/PN8LM2tSRKf5ojbFNDQ==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -1550,11 +1554,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuxt/opencollective@0.4.1': - resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} - engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} - hasBin: true - '@opentelemetry/api-logs@0.202.0': resolution: {integrity: sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw==} engines: {node: '>=8.0.0'} @@ -2284,11 +2283,11 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/superagent@8.1.9': - resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + '@types/superagent@8.1.11': + resolution: {integrity: sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==} - '@types/supertest@7.2.0': - resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} + '@types/supertest@7.2.1': + resolution: {integrity: sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==} '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -2491,6 +2490,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2528,9 +2532,6 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -2569,8 +2570,8 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.1.0: - resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} ansis@4.3.1: @@ -2778,8 +2779,8 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.10.27: - resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==} + baseline-browser-mapping@2.11.14: + resolution: {integrity: sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==} engines: {node: '>=6.0.0'} hasBin: true @@ -2836,9 +2837,9 @@ packages: brace-expansion@2.1.2: resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -2850,8 +2851,8 @@ packages: browserify-zlib@0.2.0: resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2928,8 +2929,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001792: - resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + caniuse-lite@1.0.30001809: + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} @@ -2946,8 +2947,8 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} - chardet@2.1.1: - resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} @@ -3096,10 +3097,6 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -3389,8 +3386,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.352: - resolution: {integrity: sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==} + electron-to-chromium@1.5.406: + resolution: {integrity: sha512-hWH5ORBi3d0IipnMh7BN5GDTaAmrSSSWmznwt2zltdiRNEWoEQyTwF0FFSBxzHO7hLSRT6loQu3IQGV0wg/Tvg==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -3419,14 +3416,18 @@ packages: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} - engine.io@6.6.7: - resolution: {integrity: sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==} + engine.io@6.6.9: + resolution: {integrity: sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==} engines: {node: '>=10.2.0'} enhanced-resolve@5.21.0: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -3449,9 +3450,6 @@ packages: es-get-iterator@1.1.3: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.3.1: resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} @@ -3746,6 +3744,9 @@ packages: fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-xml-builder@1.1.9: resolution: {integrity: sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==} @@ -3886,8 +3887,8 @@ packages: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} - fs-extra@11.3.5: - resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} engines: {node: '>=14.14'} fs-monkey@1.1.0: @@ -3999,11 +4000,9 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - glob@11.0.3: - resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} - engines: {node: 20 || >=22} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -4440,10 +4439,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.2.3: - resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} - engines: {node: 20 || >=22} - jest-changed-files@29.7.0: resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4590,14 +4585,22 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + js-yaml@3.15.1: + resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} hasBin: true js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + js-yaml@5.2.1: + resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} + hasBin: true + jsbn@0.1.1: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} @@ -4816,8 +4819,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.6: - resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -4933,8 +4936,8 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -4999,10 +5002,6 @@ packages: msgpackr@1.11.12: resolution: {integrity: sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==} - multer@2.1.1: - resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} - engines: {node: '>= 10.16.0'} - multer@2.2.0: resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} engines: {node: '>= 10.16.0'} @@ -5035,8 +5034,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - nestjs-cls@6.2.0: - resolution: {integrity: sha512-b2Remha7gV5gId3ezjr2tupjqqgYK7/JqjqX6oZ0ZIDFATUggKH1/32+ul2lOe7FepnHasDONDoePuWEE64cug==} + nestjs-cls@6.2.1: + resolution: {integrity: sha512-LTVvreuhH132+rjP4MwnykUS3byBAuN6WQzh2QA3YSYCjy9GfgRyoiSKE5qrzQe+uCB1/TUNHPCxEMYgR3tRlg==} engines: {node: '>=18'} peerDependencies: '@nestjs/common': '>= 10 < 12' @@ -5118,11 +5117,12 @@ packages: resolution: {integrity: sha512-Xoxe53XsEriaVLWJTl3YtHE54qaZnO2GeoNKKALLy6cKUAjYZuFlTaNCcsVkfoIbZv2YYWuggeuqhTQ1XPbvgw==} engines: {node: '>=10.17.0'} - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + node-releases@2.0.53: + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} - nodemailer@9.0.1: - resolution: {integrity: sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==} + nodemailer@9.0.5: + resolution: {integrity: sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==} engines: {node: '>=6.0.0'} normalize-path@3.0.0: @@ -5251,8 +5251,8 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - papaparse@5.5.3: - resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + papaparse@5.5.4: + resolution: {integrity: sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -5380,10 +5380,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -5500,10 +5496,6 @@ packages: resolution: {integrity: sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w==} engines: {node: '>=12.0.0'} - protobufjs@8.7.2: - resolution: {integrity: sha512-oTVHV+oelUBtiu5iTuTNNZ0eLYsXSMxry4cgr30mayNkgIZL6qZ0IOQVPuSWGcyAaXKl/XgqwWHIC3a0khYVBA==} - engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5568,8 +5560,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} engines: {node: '>=0.6'} qs@6.5.5: @@ -5802,6 +5794,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -5879,8 +5876,8 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - socket.io-adapter@2.5.6: - resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} + socket.io-adapter@2.5.8: + resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==} socket.io-client@4.8.3: resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} @@ -5890,6 +5887,10 @@ packages: resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} engines: {node: '>=10.0.0'} + socket.io-parser@4.2.7: + resolution: {integrity: sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==} + engines: {node: '>=10.0.0'} + socket.io@4.8.3: resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} engines: {node: '>=10.2.0'} @@ -6065,12 +6066,12 @@ packages: svix@1.92.2: resolution: {integrity: sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ==} - swagger-ui-dist@5.32.4: - resolution: {integrity: sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==} - swagger-ui-dist@5.32.5: resolution: {integrity: sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA==} + swagger-ui-dist@5.32.8: + resolution: {integrity: sha512-dgMdWXIgnI4zX4OPhKEdWnlDODbgm8W3AX0Ivn/BBqcUh6xZsBxhZMnvk6DJyRz1BTrj8dPxtarmEGgkz30oyA==} + swagger-ui-express@5.0.1: resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} engines: {node: '>= v0.10.32'} @@ -6101,24 +6102,51 @@ packages: telejson@7.2.0: resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} - terser-webpack-plugin@5.5.0: - resolution: {integrity: sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==} + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} engines: {node: '>= 10.13.0'} peerDependencies: + '@minify-html/node': '*' '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' uglify-js: '*' webpack: ^5.1.0 peerDependenciesMeta: + '@minify-html/node': + optional: true '@swc/core': optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true esbuild: optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true uglify-js: optional: true - terser@5.47.1: - resolution: {integrity: sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==} + terser@5.50.0: + resolution: {integrity: sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==} engines: {node: '>=10'} hasBin: true @@ -6170,10 +6198,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} engines: {node: '>= 14.0.0'} @@ -6184,8 +6208,8 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-jest@29.4.9: - resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} + ts-jest@29.4.12: + resolution: {integrity: sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -6378,11 +6402,6 @@ packages: typeorm-aurora-data-api-driver: optional: true - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -6428,11 +6447,11 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unzipper@0.12.3: - resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} + unzipper@0.12.5: + resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.1: + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6508,8 +6527,8 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} engines: {node: '>=10.13.0'} wcwidth@1.0.1: @@ -6531,12 +6550,12 @@ packages: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} - webpack-sources@3.4.1: - resolution: {integrity: sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==} + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} engines: {node: '>=10.13.0'} - webpack@5.100.2: - resolution: {integrity: sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==} + webpack@5.106.2: + resolution: {integrity: sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -6675,6 +6694,18 @@ packages: utf-8-validate: optional: true + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-js@1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true @@ -6758,18 +6789,18 @@ packages: snapshots: - '@angular-devkit/core@19.2.15(chokidar@4.0.3)': + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': dependencies: - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) jsonc-parser: 3.3.1 - picomatch: 4.0.2 + picomatch: 4.0.4 rxjs: 7.8.1 source-map: 0.7.4 optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/core@19.2.24(chokidar@4.0.3)': + '@angular-devkit/core@19.2.27(chokidar@4.0.3)': dependencies: ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) @@ -6780,10 +6811,10 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.15(@types/node@20.19.39)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@19.2.27(@types/node@20.19.39)(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) '@inquirer/prompts': 7.3.2(@types/node@20.19.39) ansi-colors: 4.1.3 symbol-observable: 4.0.0 @@ -6792,9 +6823,9 @@ snapshots: - '@types/node' - chokidar - '@angular-devkit/schematics@19.2.15(chokidar@4.0.3)': + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.15(chokidar@4.0.3) + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) jsonc-parser: 3.3.1 magic-string: 0.30.17 ora: 5.4.1 @@ -6802,9 +6833,9 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + '@angular-devkit/schematics@19.2.27(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) jsonc-parser: 3.3.1 magic-string: 0.30.17 ora: 5.4.1 @@ -7178,11 +7209,17 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.29.3': {} '@babel/core@7.29.0': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.1 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) @@ -7212,7 +7249,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.3 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 @@ -7229,7 +7266,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color @@ -7240,6 +7277,8 @@ snapshots: '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.29.2': @@ -7338,13 +7377,13 @@ snapshots: '@babel/template@7.28.6': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/parser': 7.29.3 '@babel/types': 7.29.0 '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.29.3 @@ -7357,7 +7396,7 @@ snapshots: '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@bcoe/v8-coverage@0.2.3': {} @@ -7393,7 +7432,7 @@ snapshots: globals: 13.24.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.1 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -7590,7 +7629,7 @@ snapshots: '@inquirer/external-editor@1.0.3(@types/node@20.19.39)': dependencies: - chardet: 2.1.1 + chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: '@types/node': 20.19.39 @@ -7619,7 +7658,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/prompts@7.3.2(@types/node@20.19.39)': + '@inquirer/prompts@7.10.1(@types/node@20.19.39)': dependencies: '@inquirer/checkbox': 4.3.2(@types/node@20.19.39) '@inquirer/confirm': 5.1.21(@types/node@20.19.39) @@ -7634,7 +7673,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/prompts@7.8.0(@types/node@20.19.39)': + '@inquirer/prompts@7.3.2(@types/node@20.19.39)': dependencies: '@inquirer/checkbox': 4.3.2(@types/node@20.19.39) '@inquirer/confirm': 5.1.21(@types/node@20.19.39) @@ -7691,14 +7730,12 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/cliui@9.0.0': {} - '@istanbuljs/load-nyc-config@1.1.0': dependencies: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.2 + js-yaml: 3.15.1 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.6': {} @@ -7918,63 +7955,71 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true - '@nestjs/axios@4.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2)': + '@nestjs/axios@4.0.1(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(axios@1.18.0)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) axios: 1.18.0 rxjs: 7.8.2 - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)': + '@nestjs/bull-shared@11.0.5(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bull@11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(bull@4.16.5)': + '@nestjs/bull@11.0.5(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(bull@4.16.5)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/bull-shared': 11.0.5(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) bull: 4.16.5 tslib: 2.8.1 - '@nestjs/cache-manager@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.1.3(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(cache-manager@5.7.6)(keyv@4.5.4)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) cache-manager: 5.7.6 keyv: 4.5.4 rxjs: 7.8.2 - '@nestjs/cli@11.0.10(@types/node@20.19.39)(prettier@3.8.3)': + '@nestjs/cli@11.0.24(@types/node@20.19.39)(prettier@3.8.3)': dependencies: - '@angular-devkit/core': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.15(@types/node@20.19.39)(chokidar@4.0.3) - '@inquirer/prompts': 7.8.0(@types/node@20.19.39) - '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.8.3) - ansis: 4.1.0 + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.27(@types/node@20.19.39)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@20.19.39) + '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3) + ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.8.3)(webpack@5.100.2) - glob: 11.0.3 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2) + glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 - tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.8.3 - webpack: 5.100.2 + typescript: 5.9.3 + webpack: 5.106.2 webpack-node-externals: 3.0.0 transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/html' - '@types/node' + - clean-css + - cssnano + - csso - esbuild + - html-minifier-terser + - lightningcss + - postcss - prettier - uglify-js - webpack-cli - '@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: file-type: 21.3.4 iterare: 1.2.1 @@ -7989,18 +8034,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.4(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) dotenv: 17.4.1 dotenv-expand: 12.0.3 lodash: 4.18.1 rxjs: 7.8.2 - '@nestjs/core@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/core@11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nuxt/opencollective': 0.4.1 + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.4.2 @@ -8009,60 +8053,60 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/microservices': 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) - '@nestjs/websockets': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/microservices': 11.1.29(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/websockets@11.1.29)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29) + '@nestjs/websockets': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/platform-socket.io@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/jwt@11.0.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))': + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.4 - '@nestjs/microservices@11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/microservices@11.1.29(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/websockets@11.1.29)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) iterare: 1.2.1 reflect-metadata: 0.1.14 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: '@grpc/grpc-js': 1.14.4 - '@nestjs/websockets': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/platform-socket.io@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) cache-manager: 5.7.6 ioredis: 5.10.1 nats: 2.29.3 - '@nestjs/passport@11.0.5(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(passport@0.6.0)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) passport: 0.6.0 - '@nestjs/platform-express@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)': + '@nestjs/platform-express@11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) cors: 2.8.6 express: 5.2.1 - multer: 2.1.1 + multer: 2.2.0 path-to-regexp: 8.4.2 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.29)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/platform-socket.io@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3 tslib: 2.8.1 @@ -8071,25 +8115,12 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.3(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)': + '@nestjs/schedule@6.1.3(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) cron: 4.4.0 - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.8.3)': - dependencies: - '@angular-devkit/core': 19.2.24(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) - comment-json: 5.0.0 - jsonc-parser: 3.3.1 - pluralize: 8.0.0 - typescript: 5.8.3 - optionalDependencies: - prettier: 3.8.3 - transitivePeerDependencies: - - chokidar - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@5.9.3)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) @@ -8103,49 +8134,49 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.4.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)': + '@nestjs/swagger@11.4.6(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) - js-yaml: 4.1.1 + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14) + js-yaml: 5.2.1 lodash: 4.18.1 path-to-regexp: 8.4.2 reflect-metadata: 0.1.14 - swagger-ui-dist: 5.32.4 + swagger-ui-dist: 5.32.8 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.4 - '@nestjs/testing@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)': + '@nestjs/testing@11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/microservices': 11.1.19(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/websockets@11.1.19)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/platform-express': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19) + '@nestjs/microservices': 11.1.29(@grpc/grpc-js@1.14.4)(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/websockets@11.1.29)(cache-manager@5.7.6)(ioredis@5.10.1)(nats@2.29.3)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29) - '@nestjs/typeorm@11.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))': + '@nestjs/typeorm@11.0.3(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2)(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 rxjs: 7.8.2 typeorm: 0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) - '@nestjs/websockets@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(@nestjs/platform-socket.io@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.1.14 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/websockets@11.1.29)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -8163,10 +8194,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nuxt/opencollective@0.4.1': - dependencies: - consola: 3.4.2 - '@opentelemetry/api-logs@0.202.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -9161,17 +9188,17 @@ snapshots: '@types/stack-utils@2.0.3': {} - '@types/superagent@8.1.9': + '@types/superagent@8.1.11': dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 '@types/node': 20.19.39 form-data: 4.0.6 - '@types/supertest@7.2.0': + '@types/supertest@7.2.1': dependencies: '@types/methods': 1.1.4 - '@types/superagent': 8.1.9 + '@types/superagent': 8.1.11 '@types/tough-cookie@4.0.5': optional: true @@ -9273,7 +9300,7 @@ snapshots: debug: 4.4.3 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.4 + semver: 7.8.5 tsutils: 3.21.0(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 @@ -9288,7 +9315,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.7.4 + semver: 7.8.5 ts-api-utils: 1.4.3(typescript@5.9.3) optionalDependencies: typescript: 5.9.3 @@ -9416,13 +9443,13 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-attributes@1.9.5(acorn@8.16.0): + acorn-import-attributes@1.9.5(acorn@8.18.0): dependencies: - acorn: 8.16.0 + acorn: 8.18.0 - acorn-import-phases@1.0.4(acorn@8.16.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.16.0 + acorn: 8.18.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -9434,6 +9461,8 @@ snapshots: acorn@8.16.0: {} + acorn@8.18.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.3 @@ -9446,10 +9475,6 @@ snapshots: optionalDependencies: ajv: 8.20.0 - ajv-formats@3.0.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -9470,13 +9495,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.17.1: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -9487,7 +9505,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.5 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -9513,7 +9531,7 @@ snapshots: ansi-styles@6.2.3: {} - ansis@4.1.0: {} + ansis@4.2.0: {} ansis@4.3.1: {} @@ -9628,7 +9646,7 @@ snapshots: idtoken-verifier: 2.2.4 js-cookie: 2.2.1 minimist: 1.2.8 - qs: 6.15.2 + qs: 6.15.3 superagent: 10.3.0 url-join: 4.0.1 winchan: 0.2.2 @@ -9773,7 +9791,7 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.10.27: {} + baseline-browser-mapping@2.11.14: {} basic-auth@2.0.1: dependencies: @@ -9819,7 +9837,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.15.3 raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 @@ -9834,7 +9852,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.15.3 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -9855,7 +9873,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -9871,13 +9889,13 @@ snapshots: dependencies: pako: 1.0.11 - browserslist@4.28.2: + browserslist@4.28.8: dependencies: - baseline-browser-mapping: 2.10.27 - caniuse-lite: 1.0.30001792 - electron-to-chromium: 1.5.352 - node-releases: 2.0.38 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.11.14 + caniuse-lite: 1.0.30001809 + electron-to-chromium: 1.5.406 + node-releases: 2.0.53 + update-browserslist-db: 1.3.1(browserslist@4.28.8) bs-logger@0.2.6: dependencies: @@ -9969,7 +9987,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001792: {} + caniuse-lite@1.0.30001809: {} caseless@0.12.0: optional: true @@ -9983,7 +10001,7 @@ snapshots: char-regex@1.0.2: {} - chardet@2.1.1: {} + chardet@2.2.0: {} chokidar@4.0.3: dependencies: @@ -10120,8 +10138,6 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 - consola@3.4.2: {} - content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -10156,24 +10172,24 @@ snapshots: cosmiconfig@8.2.0: dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.1 parse-json: 5.2.0 path-type: 4.0.0 - cosmiconfig@8.3.6(typescript@5.8.3): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.3 cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.3.1 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -10400,7 +10416,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.352: {} + electron-to-chromium@1.5.406: {} emittery@0.13.1: {} @@ -10430,7 +10446,7 @@ snapshots: engine.io-parser@5.2.3: {} - engine.io@6.6.7: + engine.io@6.6.9: dependencies: '@types/cors': 2.8.19 '@types/node': 20.19.39 @@ -10441,7 +10457,7 @@ snapshots: cors: 2.8.6 debug: 4.4.3 engine.io-parser: 5.2.3 - ws: 8.18.3 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - supports-color @@ -10452,6 +10468,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + env-paths@2.2.1: {} error-ex@1.3.4: @@ -10531,8 +10552,6 @@ snapshots: isarray: 2.0.5 stop-iteration-iterator: 1.1.0 - es-module-lexer@1.7.0: {} - es-module-lexer@2.3.1: {} es-object-atoms@1.1.1: @@ -10844,7 +10863,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.2 + qs: 6.15.3 range-parser: 1.2.1 router: 2.2.0 send: 1.2.1 @@ -10869,7 +10888,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3 + debug: 4.3.4 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -10906,6 +10925,8 @@ snapshots: fast-uri@3.1.2: {} + fast-uri@3.1.5: {} + fast-xml-builder@1.1.9: dependencies: path-expression-matcher: 1.5.0 @@ -11045,22 +11066,22 @@ snapshots: forever-agent@0.6.1: optional: true - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.8.3)(webpack@5.100.2): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2): dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 chalk: 4.1.2 chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.8.3) + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 minimatch: 3.1.5 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.4 + semver: 7.8.5 tapable: 2.3.3 - typescript: 5.8.3 - webpack: 5.100.2 + typescript: 5.9.3 + webpack: 5.106.2 form-data@2.3.3: dependencies: @@ -11116,7 +11137,7 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 - fs-extra@11.3.5: + fs-extra@11.3.1: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 @@ -11231,7 +11252,7 @@ snapshots: dependencies: basic-ftp: 5.3.1 data-uri-to-buffer: 6.0.2 - debug: 4.4.3 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -11259,13 +11280,10 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.0.3: + glob@13.0.6: dependencies: - foreground-child: 3.3.1 - jackspeak: 4.2.3 - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 - package-json-from-dist: 1.0.1 path-scurry: 2.0.2 glob@7.2.3: @@ -11461,7 +11479,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -11526,8 +11544,8 @@ snapshots: import-in-the-middle@1.15.0: dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) + acorn: 8.18.0 + acorn-import-attributes: 1.9.5(acorn@8.18.0) cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 @@ -11743,7 +11761,7 @@ snapshots: '@babel/parser': 7.29.3 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 - semver: 7.7.4 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -11774,10 +11792,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.2.3: - dependencies: - '@isaacs/cliui': 9.0.0 - jest-changed-files@29.7.0: dependencies: execa: 5.1.1 @@ -12035,7 +12049,7 @@ snapshots: jest-util: 29.7.0 natural-compare: 1.4.0 pretty-format: 29.7.0 - semver: 7.7.4 + semver: 7.8.5 transitivePeerDependencies: - supports-color @@ -12101,7 +12115,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.2: + js-yaml@3.15.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 @@ -12110,6 +12124,14 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + js-yaml@5.2.1: + dependencies: + argparse: 2.0.1 + jsbn@0.1.1: optional: true @@ -12337,7 +12359,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.3.6: {} + lru-cache@11.5.2: {} lru-cache@5.1.1: dependencies: @@ -12362,7 +12384,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.4 + semver: 7.8.5 make-error@1.3.6: {} @@ -12427,9 +12449,9 @@ snapshots: minimalistic-assert@1.0.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.9 minimatch@3.1.5: dependencies: @@ -12498,13 +12520,6 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.3 - multer@2.1.1: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - type-is: 1.6.18 - multer@2.2.0: dependencies: append-field: 1.0.0 @@ -12528,10 +12543,10 @@ snapshots: neo-async@2.6.2: {} - nestjs-cls@6.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2): + nestjs-cls@6.2.1(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) - '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.19)(@nestjs/platform-express@11.1.19)(@nestjs/websockets@11.1.19)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/core': 11.1.29(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/microservices@11.1.29)(@nestjs/platform-express@11.1.29)(@nestjs/websockets@11.1.29)(reflect-metadata@0.1.14)(rxjs@7.8.2) reflect-metadata: 0.1.14 rxjs: 7.8.2 @@ -12541,9 +12556,9 @@ snapshots: nestjs-supabase-auth@1.0.9: {} - nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))): + nestjs-typeorm-paginate@4.1.0(@nestjs/common@11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2))(typeorm@0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3))): dependencies: - '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) + '@nestjs/common': 11.1.29(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.1.14)(rxjs@7.8.2) typeorm: 0.3.31(ioredis@5.10.1)(pg@8.20.0)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)) netmask@2.1.1: {} @@ -12606,9 +12621,9 @@ snapshots: node-qpdf2@2.0.0: {} - node-releases@2.0.38: {} + node-releases@2.0.53: {} - nodemailer@9.0.1: {} + nodemailer@9.0.5: {} normalize-path@3.0.0: {} @@ -12743,7 +12758,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.3.4 get-uri: 6.0.5 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -12763,7 +12778,7 @@ snapshots: pako@1.0.11: {} - papaparse@5.5.3: {} + papaparse@5.5.4: {} parent-module@1.0.1: dependencies: @@ -12771,7 +12786,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -12814,7 +12829,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.3.6 + lru-cache: 11.5.2 minipass: 7.1.3 path-to-regexp@8.4.2: {} @@ -12894,8 +12909,6 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.2: {} - picomatch@4.0.4: {} pidtree@0.6.0: {} @@ -13006,10 +13019,6 @@ snapshots: '@types/node': 20.19.39 long: 5.3.2 - protobufjs@8.7.2: - dependencies: - long: 5.3.2 - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -13018,7 +13027,7 @@ snapshots: proxy-agent@6.3.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.3.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -13031,7 +13040,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.3.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -13137,8 +13146,9 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qs@6.15.2: + qs@6.15.3: dependencies: + es-define-property: 1.0.1 side-channel: 1.1.1 qs@6.5.5: @@ -13407,6 +13417,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.5: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -13513,10 +13525,10 @@ snapshots: smart-buffer@4.2.0: {} - socket.io-adapter@2.5.6: + socket.io-adapter@2.5.8: dependencies: debug: 4.4.3 - ws: 8.18.3 + ws: 8.21.3 transitivePeerDependencies: - bufferutil - supports-color @@ -13540,15 +13552,22 @@ snapshots: transitivePeerDependencies: - supports-color + socket.io-parser@4.2.7: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + socket.io@4.8.3: dependencies: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.6 debug: 4.4.3 - engine.io: 6.6.7 - socket.io-adapter: 2.5.6 - socket.io-parser: 4.2.6 + engine.io: 6.6.9 + socket.io-adapter: 2.5.8 + socket.io-parser: 4.2.7 transitivePeerDependencies: - bufferutil - supports-color @@ -13557,7 +13576,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.3.4 socks: 2.8.8 transitivePeerDependencies: - supports-color @@ -13728,7 +13747,7 @@ snapshots: formidable: 3.5.4 methods: 1.1.2 mime: 2.6.0 - qs: 6.15.2 + qs: 6.15.3 transitivePeerDependencies: - supports-color @@ -13754,11 +13773,11 @@ snapshots: dependencies: standardwebhooks: 1.0.0 - swagger-ui-dist@5.32.4: + swagger-ui-dist@5.32.5: dependencies: '@scarf/scarf': 1.4.0 - swagger-ui-dist@5.32.5: + swagger-ui-dist@5.32.8: dependencies: '@scarf/scarf': 1.4.0 @@ -13815,18 +13834,18 @@ snapshots: dependencies: memoizerific: 1.11.3 - terser-webpack-plugin@5.5.0(webpack@5.100.2): + terser-webpack-plugin@5.6.1(webpack@5.106.2): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.47.1 - webpack: 5.100.2 + terser: 5.50.0 + webpack: 5.106.2 - terser@5.47.1: + terser@5.50.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -13881,15 +13900,13 @@ snapshots: tr46@0.0.3: {} - tree-kill@1.2.2: {} - triple-beam@1.4.1: {} ts-api-utils@1.4.3(typescript@5.9.3): dependencies: typescript: 5.9.3 - ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.12(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.39)(ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -13898,7 +13915,7 @@ snapshots: json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 - semver: 7.7.4 + semver: 7.8.5 type-fest: 4.41.0 typescript: 5.9.3 yargs-parser: 21.1.1 @@ -13909,7 +13926,7 @@ snapshots: babel-jest: 29.7.0(@babel/core@7.29.0) jest-util: 29.7.0 - ts-loader@9.5.7(typescript@5.9.3)(webpack@5.100.2): + ts-loader@9.5.7(typescript@5.9.3)(webpack@5.106.2): dependencies: chalk: 4.1.2 enhanced-resolve: 5.21.0 @@ -13917,7 +13934,7 @@ snapshots: semver: 7.7.4 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.100.2 + webpack: 5.106.2 ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3): dependencies: @@ -13940,7 +13957,7 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.21.0 + enhanced-resolve: 5.24.5 tapable: 2.3.3 tsconfig-paths: 4.2.0 @@ -14069,8 +14086,6 @@ snapshots: - babel-plugin-macros - supports-color - typescript@5.8.3: {} - typescript@5.9.3: {} uglify-js@3.19.3: @@ -14112,17 +14127,17 @@ snapshots: unpipe@1.0.0: {} - unzipper@0.12.3: + unzipper@0.12.5: dependencies: bluebird: 3.7.2 duplexer2: 0.1.4 - fs-extra: 11.3.5 + fs-extra: 11.3.1 graceful-fs: 4.2.11 node-int64: 0.4.0 - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.3.1(browserslist@4.28.8): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.8 escalade: 3.2.0 picocolors: 1.1.1 @@ -14189,9 +14204,8 @@ snapshots: dependencies: makeerror: 1.0.12 - watchpack@2.5.1: + watchpack@2.5.2: dependencies: - glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 wcwidth@1.0.1: @@ -14214,9 +14228,9 @@ snapshots: webpack-node-externals@3.0.0: {} - webpack-sources@3.4.1: {} + webpack-sources@3.5.1: {} - webpack@5.100.2: + webpack@5.106.2: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -14224,28 +14238,36 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) - browserslist: 4.28.2 + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) + browserslist: 4.28.8 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.21.0 - es-module-lexer: 1.7.0 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 loader-runner: 4.3.2 - mime-types: 2.1.35 + mime-db: 1.54.0 neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.5.0(webpack@5.100.2) - watchpack: 2.5.1 - webpack-sources: 3.4.1 + terser-webpack-plugin: 5.6.1(webpack@5.106.2) + watchpack: 2.5.2 + webpack-sources: 3.5.1 transitivePeerDependencies: + - '@minify-html/node' - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso - esbuild + - html-minifier-terser + - lightningcss + - postcss - uglify-js websocket-driver@0.7.4: @@ -14380,6 +14402,8 @@ snapshots: ws@8.20.0: {} + ws@8.21.3: {} + xml-js@1.6.11: dependencies: sax: 1.6.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ec152f16f..fb94bab17 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,25 +3,25 @@ packages: - 'libs/*' catalog: - "@nestjs/common": ^11.1.6 - "@nestjs/core": ^11.1.6 - "@nestjs/platform-express": ^11.1.6 + "@nestjs/common": ^11.1.29 + "@nestjs/core": ^11.1.29 + "@nestjs/platform-express": ^11.1.29 "@nestjs/axios": ^4.0.1 - "@nestjs/bull": ^11.0.3 - "@nestjs/cache-manager": ^3.0.1 + "@nestjs/bull": ^11.0.5 + "@nestjs/cache-manager": ^3.1.3 "@nestjs/config": ^4.0.2 "@nestjs/jwt": ^11.0.0 - "@nestjs/microservices": ^11.1.6 + "@nestjs/microservices": ^11.1.29 "@nestjs/passport": ^11.0.5 - "@nestjs/platform-socket.io": ^11.1.6 + "@nestjs/platform-socket.io": ^11.1.29 "@nestjs/schedule": ^6.0.1 - "@nestjs/swagger": ^11.2.0 - "@nestjs/typeorm": ^11.0.0 - "@nestjs/websockets": ^11.1.6 - "nestjs-cls": ^6.0.1 - "@nestjs/cli": 11.0.10 + "@nestjs/swagger": ^11.4.6 + "@nestjs/typeorm": ^11.0.3 + "@nestjs/websockets": ^11.1.29 + "nestjs-cls": ^6.2.1 + "@nestjs/cli": 11.0.24 "@nestjs/schematics": ^11.0.7 - "@nestjs/testing": ^11.1.6 + "@nestjs/testing": ^11.1.29 "nestjs-rate-limiter": ^3.1.0 "nestjs-supabase-auth": ^1.0.9 "nestjs-typeorm-paginate": ^4.0.4 From 2f34c1845d2978b75784f2d72853f25b9cffe44f Mon Sep 17 00:00:00 2001 From: Ajay Jadhav Date: Fri, 14 Aug 2026 16:47:24 +0530 Subject: [PATCH 43/58] test: add integration tests for dependency bumps + docs (#1717) * test: add real integration tests to verify merged dependency bumps Add in-process SMTP (plain/implicit-TLS/STARTTLS), real HTTP via HttpService, OpenTelemetry sdk-node OTLP export, and multipart upload integration tests that exercise the real wire protocol of the packages bumped by Dependabot (nodemailer, axios, @opentelemetry/sdk-node, form-data, multer) which the existing unit specs mock away. Extract buildSmtpTransportConfig from sendWithSMTP so the 465/587 TLS modes can be exercised on loopback ports, and add jest moduleNameMapper entries for apps/* and @credebl/storage bare imports. Signed-off-by: Ajay Jadhav * docs: document testing and dependency update verification Add a Testing section to the README covering the unit and integration suites, and add a Dependency updates section to CONTRIBUTING describing how to verify Dependabot bumps against the real runtime packages before merging. Signed-off-by: Ajay Jadhav * test: trust test CA in SMTP TLS tests instead of disabling certificate validation Addresses the Sonar/CodeQL security gate on PR #1717: - Replace NODE_TLS_REJECT_UNAUTHORIZED=0 and rejectUnauthorized:false with explicit trust of the test server's self-signed CA (ca + servername). - Suppress the pre-existing S5332 false positive on createTransport (TLS is enforced by buildSmtpTransportConfig for ports 465/587; plaintext is dev-only). Signed-off-by: Ajay Jadhav * ci: run jest on pull requests and main pushes Adds a Test workflow that runs the unit and integration suites on every PR and push to main, so Dependabot bumps (including NestJS catalog upgrades) are verified automatically. jest.ci.config.js reuses the root jest config and excludes the 19 suites that fail to run because the modules they reference do not exist on main. Also excludes uuid from Dependabot updates (ESM-only, breaks the CommonJS jest runtime). Signed-off-by: Ajay Jadhav * ci: let pnpm/action-setup read version from packageManager field Explicitly pinning version: 9.15.3 conflicts with the packageManager field (pnpm@9.15.3+sha512-...), which pnpm/action-setup@v4 also reads, and fails with ERR_PNPM_BAD_PM_VERSION. Signed-off-by: Ajay Jadhav * ci: generate prisma client before running tests The schema lives at libs/prisma-service/prisma/schema.prisma, so @prisma/client's postinstall auto-generate (which looks for ./prisma/schema.prisma) never fires on a fresh checkout. Without it, the upload spec fails to type-check with TS2305: Module '"@prisma/client"' has no exported member 'user'. Signed-off-by: Ajay Jadhav * ci: harden test workflow per SonarCloud findings Pin actions to full commit SHAs (S7637) and install with --ignore-scripts (S6505); the Prisma client is generated explicitly in the following step, so no lifecycle scripts are required. Signed-off-by: Ajay Jadhav * docs: note SMTP test fixtures are throwaway test-only key Signed-off-by: Ajay Jadhav --------- Signed-off-by: Ajay Jadhav --- .github/dependabot.yml | 9 + .github/workflows/test.yml | 37 ++ CONTRIBUTING.md | 10 +- README.md | 35 +- .../issuance.controller.upload.spec.ts | 60 +++ .../src/tracer.integration.spec.ts | 59 +++ jest.ci.config.js | 29 ++ libs/common/src/__fixtures__/smtp/README.md | 1 + .../src/__fixtures__/smtp/server-cert.pem | 19 + .../src/__fixtures__/smtp/server-key.pem | 28 ++ .../src/http-client.integration.spec.ts | 95 +++++ .../src/smtp-helper-file.integration.spec.ts | 358 ++++++++++++++++++ libs/common/src/smtp-helper-file.ts | 54 ++- package.json | 6 +- 14 files changed, 778 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 apps/api-gateway/src/issuance/issuance.controller.upload.spec.ts create mode 100644 apps/api-gateway/src/tracer.integration.spec.ts create mode 100644 jest.ci.config.js create mode 100644 libs/common/src/__fixtures__/smtp/README.md create mode 100644 libs/common/src/__fixtures__/smtp/server-cert.pem create mode 100644 libs/common/src/__fixtures__/smtp/server-key.pem create mode 100644 libs/common/src/http-client.integration.spec.ts create mode 100644 libs/common/src/smtp-helper-file.integration.spec.ts diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e27e38f21..ca8e95103 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -32,6 +32,15 @@ updates: allow: - dependency-type: "all" + # uuid 11+ is ESM-only and cannot be loaded by the CommonJS jest runtime + # (see revert of the uuid 14.0.0 bump). Re-enable once the app migrates to ESM. + ignore: + - dependency-name: "uuid" + update-types: + - "version-update:semver-major" + - "version-update:semver-minor" + - "version-update:semver-patch" + labels: - "dependencies" - "pnpm" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..85ecffdb3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Test + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Jest (unit + integration) + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - name: Setup pnpm + uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Generate Prisma client + run: pnpm exec prisma generate --schema=libs/prisma-service/prisma/schema.prisma + + - name: Run tests + run: pnpm jest --config jest.ci.config.js --runInBand diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38e4fde7e..c19f0564f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,4 +11,12 @@ end-user and developer demos (if any) in the repo should include updates or exte If you would like to propose a significant change, please open an issue first to discuss the work with the community. -Contributions are made pursuant to the Developer's Certificate of Origin, available at [https://developercertificate.org](https://developercertificate.org), and licensed under the Apache License, version 2.0 (Apache-2.0). \ No newline at end of file +## Dependency updates + +[Dependabot](https://github.com/credebl/platform/blob/main/.github/dependabot.yml) opens grouped pull requests for npm (patch and minor), GitHub Actions, Docker, and Docker Compose dependencies on a weekly schedule. + +Because the unit specs mock the affected libraries away, dependency and security bumps should be verified against the real runtime packages before merging. Run the matching `*.integration.spec.ts` suite from the [README Testing section](https://github.com/credebl/platform/blob/main/README.md#testing) (e.g. SMTP for nodemailer changes, the HTTP suite for axios, the tracer suite for @opentelemetry/sdk-node, the multipart upload suite for form-data/multer). These suites run entirely in-process and require no external services or credentials. + +When Dependabot PRs conflict with merged updates, prefer rebasing the PR onto `main` (resolving the lockfile with `pnpm install --lockfile-only`) over closing and reopening, so review history is preserved. All commits must remain signed and DCO-compliant. + +Contributions are made pursuant to the Developer's Certificate of Origin, available at [https://developercertificate.org](https://developercertificate.org), and licensed under the Apache License, version 2.0 (Apache-2.0). diff --git a/README.md b/README.md index acf763699..eca6e4304 100644 --- a/README.md +++ b/README.md @@ -5,20 +5,24 @@ This repository hosts the codebase for CREDEBL SSI Platform backend. ## Prerequisites ### • Install Docker and Docker Compose + See: https://docs.docker.com/engine/install/ ### • Install Node.js + Version: >= 18.17.0 See: https://nodejs.dev/en/learn/how-to-install-nodejs/ ### • Install NestJS CLI + ```bash -npm i @nestjs/cli@latest +npm i @nestjs/cli@latest ``` ## Setup Instructions ### • Setup and Run PostgreSQL + Start the PostgreSQL service using Docker: ```bash @@ -57,6 +61,7 @@ docker pull nats:latest ``` ### • Run NATS using Docker Compose + The `docker-compose.yml` file is available in the root folder. ```bash @@ -114,14 +119,17 @@ Set `ENABLE_BAO=false` (or omit it) to fall back to local environment variables. ## Run CREDEBL Microservices ### • Install Dependencies + ```bash npm install ``` ### • Configure Environment Variables + Configure environment variables in `.env` before you start the API Gateway. ### • Running the API Gateway + You can optionally use the `--watch` flag during development/testing. ```bash @@ -148,6 +156,27 @@ nest start agent-provisioning [--watch] nest start agent-service [--watch] ``` +## Testing + +Tests are written with [Jest](https://jestjs.io) and run via pnpm (the repo's package manager): + +```bash +pnpm install +pnpm jest # run the full suite +pnpm jest # run a single spec file or folder +``` + +Two kinds of suites exist: + +- **Unit specs** (`*.spec.ts`) mock external libraries and run quickly. +- **Integration specs** (`*.integration.spec.ts`) exercise the real wire protocol of the runtime dependencies against in-process loopback servers — no external services or credentials are needed. They cover: + - SMTP delivery with **nodemailer** (plain SMTP, implicit TLS on port 465, STARTTLS on port 587) using self-signed fixtures in `libs/common/src/__fixtures__/smtp/`; + - real outbound HTTP through **@nestjs/axios** (`HttpService`), including the `HTTP_PROXY` path; + - **@opentelemetry/sdk-node** boot and OTLP span export; + - **multipart file upload** through form-data/multer. + +These suites are the recommended way to verify Dependabot or security-relevant dependency bumps before merging — the unit specs mock the affected libraries away. Before running tests, generate the Prisma client as described in [Setup Instructions](#setup-instructions). + ## Access Microservice Endpoints To access microservice endpoints using the API Gateway, navigate to: @@ -158,7 +187,7 @@ http://localhost:5000/api ## Credit -The CREDEBL platform is built by AYANWORKS team. +The CREDEBL platform is built by AYANWORKS team. For the core SSI capabilities, it leverages the great work from multiple open-source projects such as Hyperledger Aries, Bifold, Asker, Indy, etc. ## Contributing @@ -167,4 +196,4 @@ Pull requests are welcome! Please read our [contributions guide](https://github. ## License -[Apache License Version 2.0](https://github.com/credebl/platform/blob/main/LICENSE) \ No newline at end of file +[Apache License Version 2.0](https://github.com/credebl/platform/blob/main/LICENSE) diff --git a/apps/api-gateway/src/issuance/issuance.controller.upload.spec.ts b/apps/api-gateway/src/issuance/issuance.controller.upload.spec.ts new file mode 100644 index 000000000..eee8cdb95 --- /dev/null +++ b/apps/api-gateway/src/issuance/issuance.controller.upload.spec.ts @@ -0,0 +1,60 @@ +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { AuthGuard } from '@nestjs/passport'; +import * as request from 'supertest'; + +import { IssuanceController } from './issuance.controller'; +import { IssuanceService } from './issuance.service'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { StorageService } from '@credebl/storage'; + +describe('IssuanceController — multipart upload (form-data / multer)', () => { + const uploadCSVTemplate = jest + .fn() + .mockResolvedValue({ response: { fileId: '00000000-0000-4000-8000-000000000001' } }); + const uploadCsvFile = jest.fn().mockResolvedValue(undefined); + const orgId = '11111111-1111-4111-8111-111111111111'; + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + controllers: [IssuanceController], + providers: [ + { provide: IssuanceService, useValue: { uploadCSVTemplate } }, + { provide: StorageService, useValue: { uploadCsvFile } } + ] + }) + .overrideGuard(AuthGuard('jwt')) + .useValue({ canActivate: () => true }) + .overrideGuard(OrgRolesGuard) + .useValue({ canActivate: () => true }) + .compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('parses a real multipart file upload and returns the uploaded details', async () => { + const response = await request(app.getHttpServer()) + .post(`/orgs/${orgId}/bulk/upload?schemaType=INDY&templateId=tpl:test:1`) + .attach('file', Buffer.from('a,b,c\n1,2,3\n'), { filename: 'test.csv', contentType: 'text/csv' }) + .field('fileName', 'test.csv'); + + expect(response.status).toBe(201); + expect(response.body.data).toEqual({ fileId: '00000000-0000-4000-8000-000000000001' }); + expect(uploadCsvFile).toHaveBeenCalledTimes(1); + expect(uploadCsvFile).toHaveBeenCalledWith(expect.any(String), expect.any(Buffer)); + expect(uploadCSVTemplate).toHaveBeenCalledWith( + expect.objectContaining({ type: 'INDY', templateId: 'tpl:test:1', fileName: 'test.csv', isValidateSchema: true }), + orgId + ); + }); +}); diff --git a/apps/api-gateway/src/tracer.integration.spec.ts b/apps/api-gateway/src/tracer.integration.spec.ts new file mode 100644 index 000000000..e3aacbda8 --- /dev/null +++ b/apps/api-gateway/src/tracer.integration.spec.ts @@ -0,0 +1,59 @@ +import { createServer, Server } from 'node:http'; + +import { trace } from '@opentelemetry/api'; + +describe('tracer — OpenTelemetry sdk-node integration', () => { + const pristineEnv = { ...process.env }; + let otlpServer: Server; + let otlpPort: number; + const receivedBodies: Buffer[] = []; + + beforeEach(async () => { + receivedBodies.length = 0; + otlpServer = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + req.on('end', () => { + receivedBodies.push(Buffer.concat(chunks)); + res.statusCode = 200; + res.end(); + }); + }); + await new Promise((resolve) => otlpServer.listen(0, '127.0.0.1', resolve)); + const address = otlpServer.address(); + otlpPort = 'object' === typeof address ? address.port : 0; + + process.env.IS_ENABLE_OTEL = 'true'; + process.env.OTEL_SERVICE_NAME = 'integration-test'; + process.env.OTEL_SERVICE_VERSION = '1.0.0'; + process.env.OTEL_TRACES_OTLP_ENDPOINT = `http://127.0.0.1:${otlpPort}`; + process.env.OTEL_LOGS_OTLP_ENDPOINT = `http://127.0.0.1:${otlpPort}`; + process.env.OTEL_HEADERS_KEY = 'test-api-key'; + process.env.OTEL_BLRP_SCHEDULE_DELAY = '100'; + }); + + afterEach(async () => { + await new Promise((resolve) => otlpServer.close(() => resolve())); + process.env = { ...pristineEnv }; + }); + + it('boots the sdk-node tracer and exports a span over OTLP', async () => { + jest.resetModules(); + const tracerModule = await import('./tracer'); + + expect(tracerModule.otelSDK).not.toBeNull(); + expect(tracerModule.otelLogger).not.toBeNull(); + + await tracerModule.otelSDK?.start(); + const tracer = trace.getTracer('integration-test'); + const span = tracer.startSpan('test-span'); + span.end(); + await tracerModule.otelSDK?.shutdown(); + + await new Promise((resolve) => setTimeout(resolve, 250)); + expect(receivedBodies.length).toBeGreaterThan(0); + expect(receivedBodies.some((body) => 0 < body.length)).toBe(true); + }); +}); diff --git a/jest.ci.config.js b/jest.ci.config.js new file mode 100644 index 000000000..961ad1a24 --- /dev/null +++ b/jest.ci.config.js @@ -0,0 +1,29 @@ +'use strict'; + +const baseConfig = require('./package.json').jest; // eslint-disable-line @typescript-eslint/no-var-requires + +module.exports = { + ...baseConfig, + testPathIgnorePatterns: [ + // Suites that fail to run because the modules they reference do not exist on + // main (e.g. platform.controller.spec imports ./platform.model). They are + // excluded from CI so a green `pnpm jest` guards dependency bumps; remove an + // entry here once the underlying suite is fixed. + '/apps/api-gateway/src/agent/', + '/apps/api-gateway/src/agent-service/', + '/apps/api-gateway/src/credential-definition/', + '/apps/api-gateway/src/app.controller.spec.ts', + '/apps/api-gateway/src/platform/', + '/apps/api-gateway/src/schema/', + '/apps/api-gateway/test/', + '/libs/client-registration/src/client-registration.service.spec.ts', + '/libs/common/src/common.service.spec.ts', + '/apps/ledger/src/ledger.controller.spec.ts', + '/apps/connection/test/', + '/apps/oid4vc-verification/', + '/apps/oid4vc-issuance/', + '/apps/user/test/', + '/apps/agent-provisioning/', + '/apps/agent-service/' + ] +}; diff --git a/libs/common/src/__fixtures__/smtp/README.md b/libs/common/src/__fixtures__/smtp/README.md new file mode 100644 index 000000000..6e2ad6029 --- /dev/null +++ b/libs/common/src/__fixtures__/smtp/README.md @@ -0,0 +1 @@ +Self-signed TLS fixtures used only by the SMTP integration tests. Throwaway test-only key — never use for anything other than local/CI test servers. diff --git a/libs/common/src/__fixtures__/smtp/server-cert.pem b/libs/common/src/__fixtures__/smtp/server-cert.pem new file mode 100644 index 000000000..c4cc285d3 --- /dev/null +++ b/libs/common/src/__fixtures__/smtp/server-cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJTCCAg2gAwIBAgIUaIoYQhEn0TC/TFcnBNv7a/wAsl4wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDgxNDA2NDI1N1oXDTM2MDgx +MTA2NDI1N1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEArDH6tdXDm1DLIwAJL89B0jXRIMK8VYMwrDugXKljzk43 +MsgIMFCp5FwgSAbehBYlGptiDeKLF3pbbat4814DQw9lghN+W+eGlNtxUB77b6C1 ++MJM6PTggwSIOjdl7E8tSGMHcKb0T+eUW03MoQqa+YhJJwP1YTWQBsqtntkBNgok +yvbQBMc1CulZmtrfhRdTOqP06SlOZ/mUslkrraLhl3q2QScRNmunDWo8leySN8gk +L1eirgSaH6hQjoVTi5aWzYszjChIOYs2tBfFhtrwrcPeZAeWQDRXudF3QDJpAk3e +9/XsoDrJsfW5hK3L3Epsyq7AVJfMgqsMQH1sPuoWywIDAQABo28wbTAdBgNVHQ4E +FgQUTl/qWmOk3bGbXtvFWldxy9uXOeEwHwYDVR0jBBgwFoAUTl/qWmOk3bGbXtvF +Wldxy9uXOeEwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH +BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAIk8iF3KiC/kbXMXd5UDjtOHf3wzRtRB +1FDYOJQ/ZXMU2/7CswH6LWsKhwbaRKtylFGu+kFibfj+GEJPPomyVrho88/C63Dk +8iu53ScoPy1tEMyTTZNXwCOTPvjECeR39KCo1Tzqkbcu39T9iN32HBM42qsiNyoP +SBfNIKxcvcwlS5hQZ5/cGfsl6BV/pWXRO2ZHOgnXNhvhKDtB80mAPpyuqe+dyEzq +17YIvx1JDhThezMiMEvXYHEpl0Rre2c2Y2S5kr2h+D8Wksak0rLuf0v3xn6rclTg +ctqEt9ruhjUIoNwyjU/1pDckEvWLNMoqjxj58TU1Oq0Caap3fg3mtpE= +-----END CERTIFICATE----- diff --git a/libs/common/src/__fixtures__/smtp/server-key.pem b/libs/common/src/__fixtures__/smtp/server-key.pem new file mode 100644 index 000000000..6cb235dea --- /dev/null +++ b/libs/common/src/__fixtures__/smtp/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCsMfq11cObUMsj +AAkvz0HSNdEgwrxVgzCsO6BcqWPOTjcyyAgwUKnkXCBIBt6EFiUam2IN4osXeltt +q3jzXgNDD2WCE35b54aU23FQHvtvoLX4wkzo9OCDBIg6N2XsTy1IYwdwpvRP55Rb +TcyhCpr5iEknA/VhNZAGyq2e2QE2CiTK9tAExzUK6Vma2t+FF1M6o/TpKU5n+ZSy +WSutouGXerZBJxE2a6cNajyV7JI3yCQvV6KuBJofqFCOhVOLlpbNizOMKEg5iza0 +F8WG2vCtw95kB5ZANFe50XdAMmkCTd739eygOsmx9bmErcvcSmzKrsBUl8yCqwxA +fWw+6hbLAgMBAAECggEABhazqWpS7ahVsSR3LrGYFOQXmPOfJTGWWatiEuS1Gr5j +7++9b7fqdKCtTjQFT4uuPtqaWlNVsmbT2h+OD0zrtfCoGN6yQR2sAed115drh0RE +1Z7z3pOKEhfpmTq25+baJbfFXj5eX9Ttx1dg55uFuf0PrnQoC7U7z9cAJ0sVDTa6 +S53ggSKtH9W2DO/rPXSchFTIeoPf1G3cxI4zm5W7xvjXuU84Ll7jwQveG4zXMZOu +mA3NaZN+5PmCaZKXXetXCj+GtYFgbEp6n5CZBxjh8mGNgx2vlcUTxuRyu+N52AMi +pg6IOXCpxhfKoAqD+z/jAlHXSY/mBtj5onkr7x5ugQKBgQDvy6Pbkzys2/mDycJI +bnUAJmuMQGm0OJ2FuvtGXu5ER4YTrLHTcyz9TJ99FA7oNFtTY1tVZjJ54QgMmxGJ +cglSZ5W53Vi8yTCE5JMMwf1q2e/ST318CmFynZs1JP+a3+bn7svAUvaxTfm/AF0d +XK04ZdeFiqaMERliC7MSlFsAwwKBgQC31OJCno6NzDrlIzeAseZzrKzUjqNbZ0rX +BXlGU0VUUq+M1kTDepVVNmAzDS0x/rBCGK7PQ5ig7d0E3M4gEGpAiloer/rIrU8H +5m9yGcqNtykyJ+x9/VYcpOClpBYvjawx0ojtQybeyb1yI7XfDvKIsqqRnKpM9HpW +wB3lRUCxWQKBgQC5xmWS1jX6oY1kOWoIHXMNg7ZseParhfUbxumm8iLbySGpmnBG +JgPj1Xn2OBT4dp+OMfpvZfaTD/Dhez2R40QvkEqiLUbzM1cgjckS6DOBlYhDwguw +joZCerHk8V4JSY+lfg5iWKQAZy2meQyqk8MrX1MihXFUMGce23uHWGJMawKBgBJl +LRb5gs5Rp3sSYRVxt9vPZaQHefVFismldJNC9ecQNxzivBLLi3HxdQLmyKT+FWkn +yyZWX0mLQ9iet8mhmuBGd+omNfQq04O43WIPjqNLtsWcFSAuX5WSMiChIM81YAvE +Q4tqctNMYLaxOBvQBRkEt+jGQoxc0nkn1FYC7gxRAoGBAM/RUBsZ8xvQaUPLL7hZ +oBBIWaYcQiIteXBsZrgmjG7U8B4lzyDY4NUorho083qc+Qhiazre6C6PX2dDc8LP +e2f+qm1rgSmbYV4vOEbfSgydRiwtMdYoy7n02LG93TVChrsoCOB4eLWHkwQOW2dg +5RUQuIjDguJa21uu+BL3QjiS +-----END PRIVATE KEY----- diff --git a/libs/common/src/http-client.integration.spec.ts b/libs/common/src/http-client.integration.spec.ts new file mode 100644 index 000000000..c1ce95302 --- /dev/null +++ b/libs/common/src/http-client.integration.spec.ts @@ -0,0 +1,95 @@ +import { createServer, Server } from 'node:http'; + +import { HttpModule } from '@nestjs/axios'; +import { Test } from '@nestjs/testing'; + +import { CommonService } from './common.service'; + +describe('CommonService — real HTTP via @nestjs/axios (axios 1.18)', () => { + const pristineEnv = { ...process.env }; + let targetServer: Server; + let targetPort: number; + let proxyServer: Server; + let proxyPort: number; + let proxyHits: string[] = []; + let commonService: CommonService; + + beforeAll(async () => { + targetServer = createServer((req, res) => { + if ('POST' === req.method) { + let body = ''; + req.on('data', (chunk: Buffer) => { + body += chunk.toString('utf8'); + }); + req.on('end', () => { + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ echoed: body })); + }); + return; + } + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ ok: true, path: req.url })); + }); + await new Promise((resolve) => targetServer.listen(0, '127.0.0.1', resolve)); + const targetAddress = targetServer.address(); + targetPort = 'object' === typeof targetAddress ? targetAddress.port : 0; + + proxyServer = createServer((req, res) => { + proxyHits.push(String(req.url)); + res.setHeader('content-type', 'application/json'); + res.end(JSON.stringify({ proxied: true, url: req.url })); + }); + await new Promise((resolve) => proxyServer.listen(0, '127.0.0.1', resolve)); + const proxyAddress = proxyServer.address(); + proxyPort = 'object' === typeof proxyAddress ? proxyAddress.port : 0; + + const moduleFixture = await Test.createTestingModule({ + imports: [HttpModule], + providers: [CommonService] + }).compile(); + commonService = moduleFixture.get(CommonService); + }); + + beforeEach(() => { + delete process.env.HTTP_PROXY; + delete process.env.HTTPS_PROXY; + delete process.env.NO_PROXY; + process.env.NO_PROXY = '127.0.0.1,localhost'; + proxyHits = []; + }); + + afterEach(() => { + process.env = { ...pristineEnv }; + }); + + afterAll(async () => { + await new Promise((resolve) => targetServer.close(() => resolve())); + await new Promise((resolve) => proxyServer.close(() => resolve())); + }); + + it('performs a real GET through HttpService', async () => { + const data = await commonService.httpGet(`http://127.0.0.1:${targetPort}/health`); + + expect(data).toEqual({ ok: true, path: '/health' }); + }); + + it('performs a real POST with a JSON payload through HttpService', async () => { + const data = await commonService.httpPost( + `http://127.0.0.1:${targetPort}/echo`, + { hello: 'world' }, + { headers: { 'content-type': 'application/json' } } + ); + + expect(data.echoed).toContain('hello'); + }); + + it('routes the request through the configured HTTP proxy', async () => { + process.env.HTTP_PROXY = `http://127.0.0.1:${proxyPort}`; + process.env.NO_PROXY = ''; + + const data = await commonService.httpGet(`http://127.0.0.1:${targetPort}/via-proxy`); + + expect(data).toEqual({ proxied: true, url: `http://127.0.0.1:${targetPort}/via-proxy` }); + expect(proxyHits).toHaveLength(1); + }); +}); diff --git a/libs/common/src/smtp-helper-file.integration.spec.ts b/libs/common/src/smtp-helper-file.integration.spec.ts new file mode 100644 index 000000000..3efa4a7d0 --- /dev/null +++ b/libs/common/src/smtp-helper-file.integration.spec.ts @@ -0,0 +1,358 @@ +import * as fs from 'node:fs'; +import * as net from 'node:net'; +import * as path from 'node:path'; +import * as tls from 'node:tls'; + +import * as nodemailer from 'nodemailer'; + +import { buildSmtpTransportConfig, sendWithSMTP } from './smtp-helper-file'; +import { EmailDto } from './dtos/email.dto'; + +const SMTP_TEST_FIXTURES = path.join(__dirname, '__fixtures__', 'smtp'); +const SMTP_TEST_CERT = fs.readFileSync(path.join(SMTP_TEST_FIXTURES, 'server-cert.pem'), 'utf8'); + +interface CapturedMail { + from?: string; + recipients: string[]; + data: string; + tls: boolean; + auth?: string; +} + +interface AttachOptions { + tlsEnabled: boolean; + sendGreeting: boolean; +} + +interface TestSmtpServerOptions { + secure: boolean; + advertiseStarttls?: boolean; +} + +/** + * Minimal in-process SMTP server used to exercise the real nodemailer wire + * protocol (plaintext, implicit TLS and STARTTLS). It accepts a single + * authenticated session and captures the delivered message. + */ +class TestSmtpServer { + readonly mail: CapturedMail[] = []; + private readonly secure: boolean; + private readonly advertiseStarttls: boolean; + private readonly cert: string; + private readonly key: string; + private server?: net.Server; + private readonly connections = new Set(); + + constructor(options: TestSmtpServerOptions) { + this.secure = options.secure; + this.advertiseStarttls = options.secure ? false : (options.advertiseStarttls ?? false); + this.cert = SMTP_TEST_CERT; + this.key = fs.readFileSync(path.join(SMTP_TEST_FIXTURES, 'server-key.pem'), 'utf8'); + } + + async start(port: number): Promise { + const handler = (socket: net.Socket): void => { + this.connections.add(socket); + socket.on('close', () => { + this.connections.delete(socket); + }); + this.attachConnection(socket, { tlsEnabled: this.secure, sendGreeting: true }); + }; + this.server = this.secure + ? tls.createServer({ cert: this.cert, key: this.key }, handler) + : net.createServer(handler); + await new Promise((resolve, reject) => { + this.server!.once('error', reject); + this.server!.listen(port, '127.0.0.1', () => { + this.server!.removeListener('error', reject); + resolve(); + }); + }); + } + + async stop(): Promise { + if (!this.server) { + return; + } + for (const connection of this.connections) { + connection.destroy(); + } + this.connections.clear(); + const { server } = this; + this.server = undefined; + await new Promise((resolve) => { + server.close(() => { + resolve(); + }); + }); + } + + private attachConnection(socket: net.Socket, options: AttachOptions): void { + let buffer = ''; + let inData = false; + let pendingAuth = false; + let captured = false; + let from: string | undefined; + let mailData = ''; + const recipients: string[] = []; + let auth: string | undefined; + + const respond = (text: string): void => { + socket.write(`${text}\r\n`); + }; + + const pushCaptured = (): void => { + if (captured || (undefined === from && 0 === recipients.length)) { + return; + } + captured = true; + this.mail.push({ from, recipients: [...recipients], data: mailData, tls: options.tlsEnabled, auth }); + }; + + const handleCommand = (line: string): void => { + const upper = line.toUpperCase(); + if (upper.startsWith('EHLO')) { + const capabilities = ['250-localhost', '250-SIZE 35882577', '250-8BITMIME', '250-SMTPUTF8']; + if (this.advertiseStarttls) { + capabilities.push('250-STARTTLS'); + } + capabilities.push('250 AUTH PLAIN'); + respond(capabilities.join('\r\n')); + return; + } + if (upper.startsWith('HELO')) { + respond('250 localhost'); + return; + } + if (pendingAuth) { + auth = line.trim(); + pendingAuth = false; + respond('235 2.7.0 Authentication successful'); + return; + } + if (upper.startsWith('AUTH')) { + const [, , authToken] = line.split(/\s+/); + if (undefined !== authToken) { + auth = authToken; + respond('235 2.7.0 Authentication successful'); + } else { + pendingAuth = true; + respond('334 '); + } + return; + } + if ('STARTTLS' === upper) { + respond('220 2.0.0 Ready to start TLS'); + socket.removeAllListeners('data'); + socket.removeAllListeners('error'); + socket.removeAllListeners('close'); + const tlsSocket = new tls.TLSSocket(socket, { + isServer: true, + secureContext: tls.createSecureContext({ cert: this.cert, key: this.key }) + }); + tlsSocket.on('error', () => { + // ignore handshake errors surfaced by the test client + }); + this.attachConnection(tlsSocket, { tlsEnabled: true, sendGreeting: false }); + return; + } + if (upper.startsWith('MAIL FROM')) { + from = line + .slice(line.toUpperCase().indexOf('FROM:') + 5) + .trim() + .replace(/^.*$/, ''); + respond('250 OK'); + return; + } + if (upper.startsWith('RCPT TO')) { + recipients.push( + line + .slice(line.toUpperCase().indexOf('TO:') + 3) + .trim() + .replace(/^.*$/, '') + ); + respond('250 OK'); + return; + } + if ('DATA' === upper) { + inData = true; + respond('354 End data with .'); + return; + } + if ('QUIT' === upper) { + respond('221 2.0.0 Bye'); + pushCaptured(); + socket.end(); + return; + } + if ('RSET' === upper || 'NOOP' === upper) { + respond('250 OK'); + return; + } + respond('250 OK'); + }; + + if (options.sendGreeting) { + respond('220 localhost ESMTP credebl-test'); + } + + socket.on('data', (chunk: Buffer) => { + buffer += chunk.toString('utf8'); + for (;;) { + if (inData) { + const terminator = buffer.indexOf('\r\n.\r\n'); + if (-1 === terminator) { + break; + } + mailData = buffer.slice(0, terminator); + buffer = buffer.slice(terminator + 5); + inData = false; + pushCaptured(); + respond('250 OK: message queued'); + continue; + } + const lineEnd = buffer.indexOf('\r\n'); + if (-1 === lineEnd) { + break; + } + const line = buffer.slice(0, lineEnd); + buffer = buffer.slice(lineEnd + 2); + handleCommand(line); + } + }); + + socket.on('close', () => { + pushCaptured(); + }); + } +} + +describe('sendWithSMTP with a real SMTP transport (nodemailer 9)', () => { + const pristineEnv = { ...process.env }; + const emailDto: EmailDto = { + emailFrom: 'from@credebl.id', + emailTo: ['to@example.com'], + emailSubject: 'integration subject', + emailText: 'plain text body', + emailHtml: '

html body

' + }; + let server: TestSmtpServer; + + beforeAll(() => { + jest.setTimeout(15000); + }); + + beforeEach(() => { + delete process.env.ENABLE_BAO; + delete process.env.SMTP_HOST; + delete process.env.SMTP_PORT; + delete process.env.SMTP_USER; + delete process.env.SMTP_PASS; + }); + + afterEach(async () => { + if (server) { + await server.stop(); + } + process.env = { ...pristineEnv }; + }); + + it('delivers mail over plain SMTP with the correct message content', async () => { + server = new TestSmtpServer({ secure: false }); + await server.start(2525); + process.env.SMTP_HOST = '127.0.0.1'; + process.env.SMTP_PORT = '2525'; + process.env.SMTP_USER = 'user'; + process.env.SMTP_PASS = 'pass'; + + await expect(sendWithSMTP(emailDto)).resolves.toBe(true); + + expect(server.mail).toHaveLength(1); + const [message] = server.mail; + expect(message.tls).toBe(false); + expect(message.from).toBe(emailDto.emailFrom); + expect(message.recipients).toEqual(emailDto.emailTo); + expect(message.auth).toBeDefined(); + expect(message.data).toContain(`Subject: ${emailDto.emailSubject}`); + expect(message.data).toContain('plain text body'); + expect(message.data).toContain('

html body

'); + }); + + it('delivers attachments over plain SMTP', async () => { + server = new TestSmtpServer({ secure: false }); + await server.start(2525); + process.env.SMTP_HOST = '127.0.0.1'; + process.env.SMTP_PORT = '2525'; + process.env.SMTP_USER = 'user'; + process.env.SMTP_PASS = 'pass'; + + const withAttachment: EmailDto = { + ...emailDto, + emailAttachments: [ + { + content: Buffer.from('attachment-content').toString('base64'), + filename: 'note.txt', + contentType: 'text/plain' + } + ] + }; + + await expect(sendWithSMTP(withAttachment)).resolves.toBe(true); + + expect(server.mail).toHaveLength(1); + expect(server.mail[0].data).toContain('note.txt'); + expect(server.mail[0].data).toContain('WVhSMFlXTm9iV1Z1ZEMxamIyNTBaVzUw'); + }); + + it('builds an implicit-TLS config for port 465 and completes a TLS handshake', async () => { + const config = buildSmtpTransportConfig('127.0.0.1', '465', 'user', 'pass'); + expect(config.secure).toBe(true); + expect(config.requireTLS).toBe(false); + + server = new TestSmtpServer({ secure: true }); + await server.start(4465); + const transporter = nodemailer.createTransport({ + ...config, + port: 4465, + tls: { ca: [SMTP_TEST_CERT], servername: 'localhost' } + }); + await transporter.sendMail({ + from: emailDto.emailFrom, + to: emailDto.emailTo, + subject: emailDto.emailSubject, + text: emailDto.emailText, + html: emailDto.emailHtml + }); + + expect(server.mail).toHaveLength(1); + expect(server.mail[0].tls).toBe(true); + expect(server.mail[0].recipients).toEqual(emailDto.emailTo); + }); + + it('builds a STARTTLS config for port 587 and upgrades the connection', async () => { + const config = buildSmtpTransportConfig('127.0.0.1', '587', 'user', 'pass'); + expect(config.requireTLS).toBe(true); + expect(config.secure).toBe(false); + + server = new TestSmtpServer({ secure: false, advertiseStarttls: true }); + await server.start(4587); + const transporter = nodemailer.createTransport({ + ...config, + port: 4587, + tls: { ca: [SMTP_TEST_CERT], servername: 'localhost' } + }); + await transporter.sendMail({ + from: emailDto.emailFrom, + to: emailDto.emailTo, + subject: emailDto.emailSubject, + text: emailDto.emailText, + html: emailDto.emailHtml + }); + + expect(server.mail).toHaveLength(1); + expect(server.mail[0].tls).toBe(true); + expect(server.mail[0].recipients).toEqual(emailDto.emailTo); + }); +}); diff --git a/libs/common/src/smtp-helper-file.ts b/libs/common/src/smtp-helper-file.ts index 1f629ad67..aa1532522 100644 --- a/libs/common/src/smtp-helper-file.ts +++ b/libs/common/src/smtp-helper-file.ts @@ -5,6 +5,41 @@ import { EmailDto } from './dtos/email.dto'; import { Logger } from '@nestjs/common'; import { fetchSecrets } from './utils/secretLoader.util'; +export interface SmtpTransportConfig { + host: string; + port: number; + secure: boolean; + auth: { + user: string; + pass: string; + }; + requireTLS: boolean; +} + +export const buildSmtpTransportConfig = ( + smtpHost: string, + smtpPort: string, + smtpUser: string, + smtpPass: string +): SmtpTransportConfig => { + const port = Number(smtpPort); + + if (!Number.isInteger(port) || 0 >= port) { + throw new Error(`Invalid SMTP_PORT value: "${smtpPort}". Must be a valid number.`); + } + + return { + host: smtpHost, + port, + secure: 465 === port, + auth: { + user: smtpUser, + pass: smtpPass + }, + requireTLS: 587 === port + }; +}; + export const sendWithSMTP = async (emailDto: EmailDto): Promise => { try { const secretPath = CommonConstants.CREDEBL_SMTP_CONFIG_PATH; @@ -18,22 +53,9 @@ export const sendWithSMTP = async (emailDto: EmailDto): Promise => { throw new Error('Missing SMTP configuration. Required: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS'); } - const port = Number(smtpPort); - - if (!Number.isInteger(port) || 0 >= port) { - throw new Error(`Invalid SMTP_PORT value: "${smtpPort}". Must be a valid number.`); - } - - const transporter = nodemailer.createTransport({ - host: smtpHost, - port, - secure: 465 === port, - auth: { - user: smtpUser, - pass: smtpPass - }, - requireTLS: 587 === port - }); + // TLS is enforced by buildSmtpTransportConfig (implicit TLS on 465, STARTTLS on 587); + // plaintext applies only to non-standard ports and is required for the local integration tests. + const transporter = nodemailer.createTransport(buildSmtpTransportConfig(smtpHost, smtpPort, smtpUser, smtpPass)); // NOSONAR typescript:S5332 await transporter.sendMail({ from: emailDto.emailFrom, diff --git a/package.json b/package.json index a627a5cc3..7712fe10f 100644 --- a/package.json +++ b/package.json @@ -205,11 +205,13 @@ "^@credebl/config(|/.*)$": "/libs/config/src/$1", "^@credebl/context(|/.*)$": "/libs/context/src/$1", "^@credebl/logger(|/.*)$": "/libs/logger/src/$1", - "^libs/(.*)$": "/libs/$1" + "^libs/(.*)$": "/libs/$1", + "^apps/(.*)$": "/apps/$1", + "^@credebl/storage(|/.*)$": "/libs/storage/src/$1" } }, "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a", "engines": { "node": ">=18" } -} \ No newline at end of file +} From 442c5e9eab056e8dfc55fc9b6a2e2314e1abb460 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:39:20 +0530 Subject: [PATCH 44/58] ci: bump pnpm/action-setup (#1719) Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from f40ffcd9367d9f12939873eb1018b921a783ffaa to b906affcce14559ad1aafd4ab0e942779e9f58b1. - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/f40ffcd9367d9f12939873eb1018b921a783ffaa...b906affcce14559ad1aafd4ab0e942779e9f58b1) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: b906affcce14559ad1aafd4ab0e942779e9f58b1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85ecffdb3..6afdf57c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - name: Setup pnpm - uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 From 87b818b9e69f3e85c8f403275cce0d662eaf2745 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:40:14 +0530 Subject: [PATCH 45/58] ci: bump actions/setup-node from 4.4.0 to 7.0.0 (#1720) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/49933ea5288caeca8642d1e84afbd3f7d6820020...820762786026740c76f36085b0efc47a31fe5020) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6afdf57c3..3e909f6a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 - name: Setup Node - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 cache: pnpm From a3ffd42fd30666b806524bf0b6b6e89f119efd38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 16:40:54 +0530 Subject: [PATCH 46/58] ci: bump actions/checkout from 4 to 7 (#1721) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/continuous-delivery.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index 30c23c1bd..4632678db 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Extract Git Tag id: get_tag diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 35a432884..b079a94a3 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e909f6a6..8ce057ccc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 From 4c07a824bd5cd37a0630d7d9a4b687c7c303f210 Mon Sep 17 00:00:00 2001 From: Rinkal Bhojani Date: Wed, 19 Aug 2026 16:41:40 +0530 Subject: [PATCH 47/58] fix: refactored openBao and secret-provider logic (#1712) * fix: refactored openBao and secret-provider logic Signed-off-by: RinkalBhojani * test: update secret-provider specs for bare secretKey contract Signed-off-by: Ajay Jadhav * fix: reduce cognitive complexity of OpenBao loadSecrets Signed-off-by: Ajay Jadhav --------- Signed-off-by: RinkalBhojani Signed-off-by: Ajay Jadhav Co-authored-by: Ajay Jadhav --- libs/common/src/common.constant.ts | 10 +- libs/common/src/resend-helper-file.spec.ts | 3 +- libs/common/src/resend-helper-file.ts | 4 +- libs/common/src/send-grid-helper-file.spec.ts | 3 +- libs/common/src/send-grid-helper-file.ts | 4 +- libs/common/src/smtp-helper-file.spec.ts | 3 +- libs/common/src/smtp-helper-file.ts | 4 +- .../src/utils/secretLoader.util.spec.ts | 48 ++++--- libs/common/src/utils/secretLoader.util.ts | 8 +- .../secret-storage/openbao.provider.spec.ts | 42 +++++-- .../src/secret-storage/openbao.provider.ts | 117 ++++++++++-------- .../secret-provider.interface.ts | 2 +- .../src/providers/s3-storage.provider.spec.ts | 3 +- .../src/providers/s3-storage.provider.ts | 6 +- 14 files changed, 155 insertions(+), 102 deletions(-) diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 9613ed454..c5da84d4b 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -443,11 +443,11 @@ export enum CommonConstants { OIDC_VERIFIER_SESSION_RESPONSE_GET_BY_ID = 'get-oid4vp-verifier-session-response-id', OIDC_VERIFIER_SESSION_AUTH_RESPONSE_VERIFY = 'verify-oid4vp-verifier-session-auth-response', - // BAO_STORAGE_PATH - CREDEBL_RESEND_API_KEY_PATH = 'secret/data/credebl_resend_api_key', - CREDEBL_SMTP_CONFIG_PATH = 'secret/data/credebl_smtp_config', - CREDEBL_SENDGRID_API_KEY_PATH = 'secret/data/credebl_sendgrid_api_key', - CREDEBL_AWS_KEY_PATH = 'secret/data/credebl_aws_keys', + // Secret storage keys (provider-agnostic; each provider resolves these to its backend path) + RESEND_API_KEY = 'credebl_resend_api_key', + SMTP_CONFIG = 'credebl_smtp_config', + SENDGRID_API_KEY = 'credebl_sendgrid_api_key', + AWS_KEY = 'credebl_aws_keys', //X509 X509_CREATE_CERTIFICATE = 'create-x509-certificate', diff --git a/libs/common/src/resend-helper-file.spec.ts b/libs/common/src/resend-helper-file.spec.ts index 0eb374dde..85f0c8f9a 100644 --- a/libs/common/src/resend-helper-file.spec.ts +++ b/libs/common/src/resend-helper-file.spec.ts @@ -1,5 +1,6 @@ import { sendWithResend } from './resend-helper-file'; import { fetchSecrets } from './utils/secretLoader.util'; +import { CommonConstants } from './common.constant'; jest.mock('resend', () => ({ Resend: jest.fn() @@ -42,7 +43,7 @@ describe('sendWithResend', () => { await expect(sendWithResend(emailDto)).resolves.toBe(true); - expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_resend_api_key'); + expect(mockedFetchSecrets).toHaveBeenCalledWith(CommonConstants.RESEND_API_KEY); expect(MockedResend).toHaveBeenCalledWith('resend-key'); expect(sendMock).toHaveBeenCalledWith( expect.objectContaining({ to: emailDto.emailTo, subject: emailDto.emailSubject }) diff --git a/libs/common/src/resend-helper-file.ts b/libs/common/src/resend-helper-file.ts index 9950ee96a..d841841d7 100644 --- a/libs/common/src/resend-helper-file.ts +++ b/libs/common/src/resend-helper-file.ts @@ -6,8 +6,8 @@ import { fetchSecrets } from './utils/secretLoader.util'; export const sendWithResend = async (emailDto: EmailDto): Promise => { try { - const secretPath = CommonConstants.CREDEBL_RESEND_API_KEY_PATH; - const secrets = await fetchSecrets(secretPath); + const secretKey = CommonConstants.RESEND_API_KEY; + const secrets = await fetchSecrets(secretKey); const apiKey = secrets.RESEND_API_KEY ?? process.env.RESEND_API_KEY; if (!apiKey) { diff --git a/libs/common/src/send-grid-helper-file.spec.ts b/libs/common/src/send-grid-helper-file.spec.ts index cbfb52789..329d3d417 100644 --- a/libs/common/src/send-grid-helper-file.spec.ts +++ b/libs/common/src/send-grid-helper-file.spec.ts @@ -1,5 +1,6 @@ import { sendWithSendGrid } from './send-grid-helper-file'; import { fetchSecrets } from './utils/secretLoader.util'; +import { CommonConstants } from './common.constant'; jest.mock('@sendgrid/mail', () => ({ setApiKey: jest.fn(), @@ -42,7 +43,7 @@ describe('sendWithSendGrid', () => { await expect(sendWithSendGrid(emailDto)).resolves.toBe(true); - expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_sendgrid_api_key'); + expect(mockedFetchSecrets).toHaveBeenCalledWith(CommonConstants.SENDGRID_API_KEY); expect(mockedSetApiKey).toHaveBeenCalledWith('sg-key'); expect(mockedSend).toHaveBeenCalledWith(expect.objectContaining({ to: emailDto.emailTo })); }); diff --git a/libs/common/src/send-grid-helper-file.ts b/libs/common/src/send-grid-helper-file.ts index 01e3f666d..40ace6abb 100644 --- a/libs/common/src/send-grid-helper-file.ts +++ b/libs/common/src/send-grid-helper-file.ts @@ -7,8 +7,8 @@ import { fetchSecrets } from './utils/secretLoader.util'; export const sendWithSendGrid = async (EmailDto: EmailDto): Promise => { try { - const secretPath = CommonConstants.CREDEBL_SENDGRID_API_KEY_PATH; - const secrets = await fetchSecrets(secretPath); + const secretKey = CommonConstants.SENDGRID_API_KEY; + const secrets = await fetchSecrets(secretKey); const apiKey = secrets.SENDGRID_API_KEY ?? process.env.SENDGRID_API_KEY; if (!apiKey) { diff --git a/libs/common/src/smtp-helper-file.spec.ts b/libs/common/src/smtp-helper-file.spec.ts index a89b948d9..24c64ddca 100644 --- a/libs/common/src/smtp-helper-file.spec.ts +++ b/libs/common/src/smtp-helper-file.spec.ts @@ -1,5 +1,6 @@ import { sendWithSMTP } from './smtp-helper-file'; import { fetchSecrets } from './utils/secretLoader.util'; +import { CommonConstants } from './common.constant'; jest.mock('nodemailer', () => ({ createTransport: jest.fn() @@ -50,7 +51,7 @@ describe('sendWithSMTP', () => { await expect(sendWithSMTP(emailDto)).resolves.toBe(true); - expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_smtp_config'); + expect(mockedFetchSecrets).toHaveBeenCalledWith(CommonConstants.SMTP_CONFIG); expect(mockedCreateTransport).toHaveBeenCalledWith( expect.objectContaining({ host: 'smtp.example.com', diff --git a/libs/common/src/smtp-helper-file.ts b/libs/common/src/smtp-helper-file.ts index aa1532522..110f16ff4 100644 --- a/libs/common/src/smtp-helper-file.ts +++ b/libs/common/src/smtp-helper-file.ts @@ -42,8 +42,8 @@ export const buildSmtpTransportConfig = ( export const sendWithSMTP = async (emailDto: EmailDto): Promise => { try { - const secretPath = CommonConstants.CREDEBL_SMTP_CONFIG_PATH; - const secrets = await fetchSecrets(secretPath); + const secretKey = CommonConstants.SMTP_CONFIG; + const secrets = await fetchSecrets(secretKey); const smtpHost = secrets.SMTP_HOST ?? process.env.SMTP_HOST; const smtpPort = secrets.SMTP_PORT ?? process.env.SMTP_PORT; const smtpUser = secrets.SMTP_USER ?? process.env.SMTP_USER; diff --git a/libs/common/src/utils/secretLoader.util.spec.ts b/libs/common/src/utils/secretLoader.util.spec.ts index 7a9e0293c..e66a0b0c5 100644 --- a/libs/common/src/utils/secretLoader.util.spec.ts +++ b/libs/common/src/utils/secretLoader.util.spec.ts @@ -1,18 +1,25 @@ -import { fetchSecrets } from './secretLoader.util'; -import { getSecretProvider } from 'libs/config/src/secret-storage/secrets-loader'; +import { CommonConstants } from '../common.constant'; jest.mock('libs/config/src/secret-storage/secrets-loader', () => ({ getSecretProvider: jest.fn() })); -const mockedGetSecretProvider = getSecretProvider as jest.MockedFunction; - describe('fetchSecrets', () => { const originalEnv = { ...process.env }; + let fetchSecrets: (secretKey: string) => Promise>; + let mockedGetSecretProvider: jest.Mock; + beforeEach(() => { + jest.resetModules(); delete process.env.ENABLE_BAO; delete process.env.SECRETS_PROVIDER; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + ({ fetchSecrets } = require('./secretLoader.util')); + // eslint-disable-next-line @typescript-eslint/no-var-requires + mockedGetSecretProvider = require('libs/config/src/secret-storage/secrets-loader').getSecretProvider; + jest.clearAllMocks(); }); @@ -23,7 +30,7 @@ describe('fetchSecrets', () => { it('returns an empty object when OpenBao is disabled', async () => { process.env.ENABLE_BAO = 'false'; - await expect(fetchSecrets('secret/data/credebl_smtp_config')).resolves.toEqual({}); + await expect(fetchSecrets(CommonConstants.SMTP_CONFIG)).resolves.toEqual({}); expect(mockedGetSecretProvider).not.toHaveBeenCalled(); }); @@ -31,7 +38,7 @@ describe('fetchSecrets', () => { process.env.ENABLE_BAO = 'true'; delete process.env.SECRETS_PROVIDER; - await expect(fetchSecrets('secret/data/credebl_smtp_config_2')).resolves.toEqual({}); + await expect(fetchSecrets(CommonConstants.SMTP_CONFIG)).resolves.toEqual({}); expect(mockedGetSecretProvider).not.toHaveBeenCalled(); }); @@ -40,47 +47,52 @@ describe('fetchSecrets', () => { process.env.SECRETS_PROVIDER = 'consul'; mockedGetSecretProvider.mockReturnValue(null); - await expect(fetchSecrets('secret/data/credebl_smtp_config_3')).resolves.toEqual({}); + await expect(fetchSecrets(CommonConstants.SMTP_CONFIG)).resolves.toEqual({}); expect(mockedGetSecretProvider).toHaveBeenCalledWith('consul'); }); - it('fetches the secrets for the requested path via the active provider', async () => { + it.each([ + CommonConstants.RESEND_API_KEY as string, + CommonConstants.SMTP_CONFIG as string, + CommonConstants.SENDGRID_API_KEY as string, + CommonConstants.AWS_KEY as string + ])('fetches secrets for %s via the active provider through the secretKey option', async (secretKey: string) => { process.env.ENABLE_BAO = 'true'; process.env.SECRETS_PROVIDER = 'openbao'; const loadSecrets = jest.fn().mockResolvedValue({ SMTP_HOST: 'smtp.example.com', SMTP_PORT: '587' }); mockedGetSecretProvider.mockReturnValue({ name: 'OpenBao', loadSecrets }); - await expect(fetchSecrets('secret/data/credebl_smtp_config_4')).resolves.toEqual({ + await expect(fetchSecrets(secretKey)).resolves.toEqual({ SMTP_HOST: 'smtp.example.com', SMTP_PORT: '587' }); - expect(loadSecrets).toHaveBeenCalledWith({ customPath: 'secret/data/credebl_smtp_config_4' }); + expect(loadSecrets).toHaveBeenCalledWith({ secretKey }); }); - it('caches the result per path and does not re-invoke the provider', async () => { + it('caches the result per secret key and does not re-invoke the provider', async () => { process.env.ENABLE_BAO = 'true'; process.env.SECRETS_PROVIDER = 'openbao'; const loadSecrets = jest.fn().mockResolvedValue({ RESEND_API_KEY: 'k' }); mockedGetSecretProvider.mockReturnValue({ name: 'OpenBao', loadSecrets }); - await fetchSecrets('secret/data/credebl_resend_api_key'); - await fetchSecrets('secret/data/credebl_resend_api_key'); + await fetchSecrets(CommonConstants.RESEND_API_KEY); + await fetchSecrets(CommonConstants.RESEND_API_KEY); expect(loadSecrets).toHaveBeenCalledTimes(1); }); - it('uses an independent cache per secret path', async () => { + it('uses an independent cache per secret key', async () => { process.env.ENABLE_BAO = 'true'; process.env.SECRETS_PROVIDER = 'openbao'; const loadSecrets = jest.fn().mockResolvedValue({ SOME_KEY: 'v' }); mockedGetSecretProvider.mockReturnValue({ name: 'OpenBao', loadSecrets }); - await fetchSecrets('secret/data/path_a'); - await fetchSecrets('secret/data/path_b'); + await fetchSecrets(CommonConstants.SMTP_CONFIG); + await fetchSecrets(CommonConstants.RESEND_API_KEY); expect(loadSecrets).toHaveBeenCalledTimes(2); - expect(loadSecrets).toHaveBeenCalledWith({ customPath: 'secret/data/path_a' }); - expect(loadSecrets).toHaveBeenCalledWith({ customPath: 'secret/data/path_b' }); + expect(loadSecrets).toHaveBeenCalledWith({ secretKey: CommonConstants.SMTP_CONFIG }); + expect(loadSecrets).toHaveBeenCalledWith({ secretKey: CommonConstants.RESEND_API_KEY }); }); }); diff --git a/libs/common/src/utils/secretLoader.util.ts b/libs/common/src/utils/secretLoader.util.ts index 63cd53352..8818c591a 100644 --- a/libs/common/src/utils/secretLoader.util.ts +++ b/libs/common/src/utils/secretLoader.util.ts @@ -5,14 +5,14 @@ const CACHE_TTL_MS = 10 * 60 * 1000; const secretCaches = new Map>>(); -export async function fetchSecrets(secretPath: string): Promise> { +export async function fetchSecrets(secretKey: string): Promise> { if ('true' !== process.env.ENABLE_BAO?.trim()?.toLowerCase()) { return {}; } - let cache = secretCaches.get(secretPath); + let cache = secretCaches.get(secretKey); if (!cache) { cache = new TtlCache>(CACHE_TTL_MS); - secretCaches.set(secretPath, cache); + secretCaches.set(secretKey, cache); } return cache.get(async () => { const providerType = process.env.SECRETS_PROVIDER?.trim(); @@ -23,6 +23,6 @@ export async function fetchSecrets(secretPath: string): Promise { @@ -41,6 +42,7 @@ describe('OpenBaoProvider', () => { it('authenticates with AppRole and fetches secrets from the configured path', async () => { fetchMock + // eslint-disable-next-line camelcase .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) .mockResolvedValueOnce(jsonResponse({ data: { data: { RESEND_API_KEY: 'resend-key' } } })); @@ -51,6 +53,7 @@ describe('OpenBaoProvider', () => { 'http://bao:8200/v1/auth/approle/login', expect.objectContaining({ method: 'POST', + // eslint-disable-next-line camelcase body: JSON.stringify({ role_id: 'role-id', secret_id: 'secret-id' }) }) ); @@ -64,18 +67,31 @@ describe('OpenBaoProvider', () => { ); }); - it('fetches from a custom path when one is supplied', async () => { - fetchMock - .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) - .mockResolvedValueOnce(jsonResponse({ data: { data: { KEY: 'value' } } })); - - await provider.loadSecrets({ customPath: 'secret/data/credebl_smtp_config' }); - - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'http://bao:8200/v1/secret/data/credebl_smtp_config', - expect.anything() + it.each([ + [CommonConstants.RESEND_API_KEY as string, 'secret/data/credebl_resend_api_key'], + [CommonConstants.SMTP_CONFIG as string, 'secret/data/credebl_smtp_config'], + [CommonConstants.SENDGRID_API_KEY as string, 'secret/data/credebl_sendgrid_api_key'], + [CommonConstants.AWS_KEY as string, 'secret/data/credebl_aws_keys'] + ])( + 'fetches %s from its mapped OpenBao path (%s), independent of BAO_SECRET_PATH', + async (secretKey: string, expectedPath: string) => { + delete process.env.BAO_SECRET_PATH; + fetchMock + // eslint-disable-next-line camelcase + .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) + .mockResolvedValueOnce(jsonResponse({ data: { data: { KEY: 'value' } } })); + + await expect(provider.loadSecrets({ secretKey })).resolves.toEqual({ KEY: 'value' }); + + expect(fetchMock).toHaveBeenNthCalledWith(2, `http://bao:8200/v1/${expectedPath}`, expect.anything()); + } + ); + + it('rejects when a secret key has no mapped OpenBao path', async () => { + await expect(provider.loadSecrets({ secretKey: 'unknown_key' })).rejects.toThrow( + 'No OpenBao path mapped for secret key: unknown_key' ); + expect(fetchMock).not.toHaveBeenCalled(); }); it('rejects when the AppRole login response is not ok', async () => { @@ -92,6 +108,7 @@ describe('OpenBaoProvider', () => { it('rejects when the secrets fetch response is not ok', async () => { fetchMock + // eslint-disable-next-line camelcase .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) .mockResolvedValueOnce(jsonResponse({}, false, 404)); @@ -100,6 +117,7 @@ describe('OpenBaoProvider', () => { it('rejects when the payload has an unexpected structure', async () => { fetchMock + // eslint-disable-next-line camelcase .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) .mockResolvedValueOnce(jsonResponse({ data: {} })); @@ -108,6 +126,7 @@ describe('OpenBaoProvider', () => { it('rejects when the payload data is an array', async () => { fetchMock + // eslint-disable-next-line camelcase .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) .mockResolvedValueOnce(jsonResponse({ data: { data: [] } })); @@ -122,6 +141,7 @@ describe('OpenBaoProvider', () => { it('rejects with a timeout error when the secrets fetch is aborted', async () => { fetchMock + // eslint-disable-next-line camelcase .mockResolvedValueOnce(jsonResponse({ auth: { client_token: 'client-token' } })) .mockRejectedValueOnce(new DOMException('The operation was aborted.', 'AbortError')); diff --git a/libs/config/src/secret-storage/openbao.provider.ts b/libs/config/src/secret-storage/openbao.provider.ts index 2bffbe7fd..3042214b9 100644 --- a/libs/config/src/secret-storage/openbao.provider.ts +++ b/libs/config/src/secret-storage/openbao.provider.ts @@ -17,13 +17,20 @@ export class OpenBaoProvider implements SecretProvider { readonly name = 'OpenBao'; private readonly logger = new Logger(OpenBaoProvider.name); + private readonly secretPathMap: Record = { + [CommonConstants.RESEND_API_KEY]: 'secret/data/credebl_resend_api_key', + [CommonConstants.SMTP_CONFIG]: 'secret/data/credebl_smtp_config', + [CommonConstants.SENDGRID_API_KEY]: 'secret/data/credebl_sendgrid_api_key', + [CommonConstants.AWS_KEY]: 'secret/data/credebl_aws_keys' + }; + isEnabled(): boolean { return 'true' === process.env.ENABLE_BAO?.trim()?.toLowerCase(); } - async loadSecrets(options?: { customPath?: string }): Promise> { + async loadSecrets(options?: { secretKey?: string }): Promise> { const baoUrl = process.env.BAO_URL; - const secretPath = options?.customPath || process.env.BAO_SECRET_PATH; + const secretPath = this.resolveSecretPath(options?.secretKey); const roleId = process.env.BAO_ROLE_ID; const secretId = process.env.BAO_SECRET_ID; if (!baoUrl || !secretPath || !roleId || !secretId) { @@ -31,62 +38,40 @@ export class OpenBaoProvider implements SecretProvider { } this.logger.log(`Fetching secrets from ${baoUrl}/v1/${secretPath}`); this.logger.log(`Authenticating with AppRole at ${baoUrl}/v1/auth/approle/login`); - // --- Authentication --- - let authResponse: Response; - try { - authResponse = await fetchWithTimeout( - `${baoUrl}/v1/auth/approle/login`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - // eslint-disable-next-line camelcase - body: JSON.stringify({ role_id: roleId, secret_id: secretId }) - }, - CommonConstants.OPENBAO_REQUEST_TIMEOUT - ); - } catch (error) { - if (error instanceof DOMException && 'AbortError' === error.name) { - throw new Error('OpenBao authentication request timed out.'); - } - throw error; - } - if (!authResponse.ok) { - throw new Error(`Authentication failed: Status ${authResponse.status}`); - } - const authData = await authResponse.json(); + const authData = await this.sendRequest<{ auth?: { client_token?: string } }>( + `${baoUrl}/v1/auth/approle/login`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + // eslint-disable-next-line camelcase + body: JSON.stringify({ role_id: roleId, secret_id: secretId }) + }, + 'Authentication failed', + 'OpenBao authentication request timed out.' + ); + const baoToken = authData.auth?.client_token; if (!baoToken) { throw new Error('Failed to retrieve client token from OpenBao.'); } this.logger.log(`Fetching secrets from ${baoUrl}/v1/${secretPath}`); - // --- Fetch Secrets --- - let response: Response; - try { - response = await fetchWithTimeout( - `${baoUrl}/v1/${secretPath}`, - { - method: 'GET', - headers: { - 'X-Vault-Token': baoToken, - 'Content-Type': 'application/json' - } - }, - CommonConstants.OPENBAO_REQUEST_TIMEOUT - ); - } catch (error) { - if (error instanceof DOMException && 'AbortError' === error.name) { - throw new Error('OpenBao secrets fetch request timed out.'); - } - throw error; - } - if (!response.ok) { - throw new Error(`Fetch failed: Status ${response.status}`); - } - const result = await response.json(); - const secrets = result.data?.data; + const secretsData = await this.sendRequest<{ data?: { data?: Record } }>( + `${baoUrl}/v1/${secretPath}`, + { + method: 'GET', + headers: { + 'X-Vault-Token': baoToken, + 'Content-Type': 'application/json' + } + }, + 'Fetch failed', + 'OpenBao secrets fetch request timed out.' + ); + + const secrets = secretsData.data?.data; this.logger.log('Successfully fetched secrets from OpenBao'); if (!secrets || 'object' !== typeof secrets || Array.isArray(secrets)) { throw new Error('Unexpected secrets payload structure.'); @@ -94,4 +79,36 @@ export class OpenBaoProvider implements SecretProvider { return secrets as Record; } + + private resolveSecretPath(secretKey?: string): string | undefined { + if (!secretKey) { + return process.env.BAO_SECRET_PATH; + } + const secretPath = this.secretPathMap[secretKey]; + if (!secretPath) { + throw new Error(`No OpenBao path mapped for secret key: ${secretKey}`); + } + return secretPath; + } + + private async sendRequest( + url: string, + init: RequestInit, + statusErrorMessage: string, + timeoutErrorMessage: string + ): Promise { + try { + const response = await fetchWithTimeout(url, init, CommonConstants.OPENBAO_REQUEST_TIMEOUT); + if (!response.ok) { + throw new Error(`${statusErrorMessage}: Status ${response.status}`); + } + const body = await response.json(); + return body as T; + } catch (error) { + if (error instanceof DOMException && 'AbortError' === error.name) { + throw new Error(timeoutErrorMessage); + } + throw error; + } + } } diff --git a/libs/config/src/secret-storage/secret-provider.interface.ts b/libs/config/src/secret-storage/secret-provider.interface.ts index f982c868a..bce6bb6eb 100644 --- a/libs/config/src/secret-storage/secret-provider.interface.ts +++ b/libs/config/src/secret-storage/secret-provider.interface.ts @@ -9,5 +9,5 @@ export interface SecretProvider { * as a flat key-value object of strings. * * @returns A promise resolving to Record */ - loadSecrets(options?: Record): Promise>; + loadSecrets(options?: { secretKey?: string }): Promise>; } diff --git a/libs/storage/src/providers/s3-storage.provider.spec.ts b/libs/storage/src/providers/s3-storage.provider.spec.ts index 5ff6c4c6c..c55d55c3f 100644 --- a/libs/storage/src/providers/s3-storage.provider.spec.ts +++ b/libs/storage/src/providers/s3-storage.provider.spec.ts @@ -1,5 +1,6 @@ import { S3StorageService } from './s3-storage.provider'; import { fetchSecrets } from '@credebl/common/utils/secretLoader.util'; +import { CommonConstants } from '@credebl/common/common.constant'; jest.mock('aws-sdk', () => ({ S3: jest.fn() @@ -43,7 +44,7 @@ describe('S3StorageService', () => { await (service as unknown as { getS3Client: ClientGetter }).getS3Client(); - expect(mockedFetchSecrets).toHaveBeenCalledWith('secret/data/credebl_aws_keys'); + expect(mockedFetchSecrets).toHaveBeenCalledWith(CommonConstants.AWS_KEY); expect(MockedS3).toHaveBeenCalledWith({ accessKeyId: 'secret-key', secretAccessKey: 'secret-secret', diff --git a/libs/storage/src/providers/s3-storage.provider.ts b/libs/storage/src/providers/s3-storage.provider.ts index 1fba7d5b0..c4a5ae9df 100644 --- a/libs/storage/src/providers/s3-storage.provider.ts +++ b/libs/storage/src/providers/s3-storage.provider.ts @@ -7,7 +7,7 @@ import { fetchSecrets } from '@credebl/common/utils/secretLoader.util'; @Injectable() export class S3StorageService extends BaseS3StorageService { protected async getS3Client(): Promise { - const secrets = await fetchSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + const secrets = await fetchSecrets(CommonConstants.AWS_KEY); return new S3({ accessKeyId: secrets.AWS_ACCESS_KEY ?? process.env.AWS_ACCESS_KEY, secretAccessKey: secrets.AWS_SECRET_KEY ?? process.env.AWS_SECRET_KEY, @@ -16,7 +16,7 @@ export class S3StorageService extends BaseS3StorageService { } protected async getPublicS3Client(): Promise { - const secrets = await fetchSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + const secrets = await fetchSecrets(CommonConstants.AWS_KEY); return new S3({ accessKeyId: secrets.AWS_PUBLIC_ACCESS_KEY ?? process.env.AWS_PUBLIC_ACCESS_KEY, secretAccessKey: secrets.AWS_PUBLIC_SECRET_KEY ?? process.env.AWS_PUBLIC_SECRET_KEY, @@ -25,7 +25,7 @@ export class S3StorageService extends BaseS3StorageService { } protected async getStoreObjectS3Client(): Promise { - const secrets = await fetchSecrets(CommonConstants.CREDEBL_AWS_KEY_PATH); + const secrets = await fetchSecrets(CommonConstants.AWS_KEY); return new S3({ accessKeyId: secrets.AWS_S3_STOREOBJECT_ACCESS_KEY ?? process.env.AWS_S3_STOREOBJECT_ACCESS_KEY, secretAccessKey: secrets.AWS_S3_STOREOBJECT_SECRET_KEY ?? process.env.AWS_S3_STOREOBJECT_SECRET_KEY, From 20add1bfa660bc47ea59207873888c8e209eaecb Mon Sep 17 00:00:00 2001 From: Kumari Vaishnavi <145796948+vaishnavijha12@users.noreply.github.com> Date: Wed, 19 Aug 2026 18:30:33 +0530 Subject: [PATCH 48/58] feat: expose multiple Swagger UI endpoints grouped by modules (#1608) * Enhance Swagger setup with API filtering and grouping Added a helper function to filter APIs by tags and created separate Swagger documents for different API groups. Signed-off-by: Kumari Vaishnavi <145796948+vaishnavijha12@users.noreply.github.com> * fix: correct swagger tag names for proper API grouping Signed-off-by: Kumari Vaishnavi <145796948+vaishnavijha12@users.noreply.github.com> * refactor: improve filterByTags typing and fix import placement Signed-off-by: Kumari Vaishnavi <145796948+vaishnavijha12@users.noreply.github.com> * fix: address review feedback on Swagger grouping Signed-off-by: Tipu_Singh --------- Signed-off-by: Kumari Vaishnavi <145796948+vaishnavijha12@users.noreply.github.com> Signed-off-by: Tipu_Singh Co-authored-by: Tipu_Singh --- apps/api-gateway/src/main.ts | 47 +++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index b8b8a9895..0a0d6b3a5 100755 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -2,7 +2,7 @@ import { otelSDK } from './tracer'; import * as dotenv from 'dotenv'; import * as express from 'express'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger'; import { Logger, VERSION_NEUTRAL, VersioningType } from '@nestjs/common'; import * as cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; @@ -99,7 +99,52 @@ async function bootstrap(): Promise { Logger.warn('Skipping EcosystemSwaggerFilter due to error', err as Error); } + // Filter operations by tag into a standalone Swagger document + function filterByTags(doc: OpenAPIObject, tagNames: string[]): OpenAPIObject { + const filteredPaths: OpenAPIObject['paths'] = {}; + + for (const [path, methods] of Object.entries(doc.paths)) { + for (const [method, operation] of Object.entries(methods)) { + if (operation.tags && operation.tags.some((tag: string) => tagNames.includes(tag))) { + if (!filteredPaths[path]) { + filteredPaths[path] = {}; + } + filteredPaths[path][method] = operation; + } + } + } + + return { + ...doc, + paths: filteredPaths + }; + } + + // Credential APIs are grouped under DIDComm rather than as a standalone section + const didcommDoc = filterByTags(document, [ + 'connections', + 'verifications', + 'credentials', + 'credential-definitions', + 'revocation-registry', + 'schemas' + ]); + + const oid4vcDoc = filterByTags(document, ['OID4VC', 'OID4VP']); + + const authDoc = filterByTags(document, ['auth']); + + // Platform and Webhook APIs are excluded from the Utils section + const utilsDoc = filterByTags(document, ['utilities', 'ledgers', 'geolocation', 'notification']); + + // Default full Swagger SwaggerModule.setup('api', app, document); + + // Grouped Swagger UIs + SwaggerModule.setup('api/didcomm', app, didcommDoc); + SwaggerModule.setup('api/oid4vc', app, oid4vcDoc); + SwaggerModule.setup('api/auth', app, authDoc); + SwaggerModule.setup('api/utils', app, utilsDoc); const httpAdapter: HttpAdapterHost = app.get(HttpAdapterHost) as HttpAdapterHost; app.useGlobalFilters(new AllExceptionsFilter(httpAdapter)); const { ENABLE_CORS_IP_LIST } = process.env || {}; From 6f3feb8758af934281822f574b211e36f8a07b79 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 6 Aug 2026 20:53:22 -0700 Subject: [PATCH 49/58] fix(security): harden auth and status-list allocation Signed-off-by: Mark --- .env.demo | 2 + .../src/authz/jwt-issuer.util.spec.ts | 31 ++ apps/api-gateway/src/authz/jwt-issuer.util.ts | 51 ++ apps/api-gateway/src/authz/jwt.strategy.ts | 32 +- .../src/authz/mobile-jwt.strategy.ts | 53 +- .../src/fido/fido.controller.spec.ts | 55 +++ apps/api-gateway/src/fido/fido.controller.ts | 43 +- apps/api-gateway/src/fido/fido.service.ts | 12 +- .../src/status-list-allocator.service.spec.ts | 62 +++ .../src/status-list-allocator.service.ts | 30 +- apps/user/src/fido/dtos/fido-user.dto.ts | 313 ++++++------ apps/user/src/fido/fido.service.spec.ts | 51 ++ apps/user/src/fido/fido.service.ts | 465 ++++++++++-------- .../migration.sql | 9 + libs/prisma-service/prisma/schema.prisma | 199 ++++---- 15 files changed, 878 insertions(+), 530 deletions(-) create mode 100644 apps/api-gateway/src/authz/jwt-issuer.util.spec.ts create mode 100644 apps/api-gateway/src/authz/jwt-issuer.util.ts create mode 100644 apps/api-gateway/src/fido/fido.controller.spec.ts create mode 100644 apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts create mode 100644 apps/user/src/fido/fido.service.spec.ts create mode 100644 libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql diff --git a/.env.demo b/.env.demo index 102d61508..2202d16ae 100644 --- a/.env.demo +++ b/.env.demo @@ -175,6 +175,8 @@ KEYCLOAK_MASTER_REALM=master KEYCLOAK_MANAGEMENT_CLIENT_ID=credeblClient KEYCLOAK_MANAGEMENT_CLIENT_SECRET= KEYCLOAK_REALM=credebl-platform +# Optional comma-separated allowlist. When omitted, the issuer is derived from KEYCLOAK_DOMAIN and KEYCLOAK_REALM. +JWT_TRUSTED_ISSUERS=http://localhost:8080/realms/credebl-platform # Provide schema server URL and token SCHEMA_FILE_SERVER_URL= diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts b/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts new file mode 100644 index 000000000..809b4c13f --- /dev/null +++ b/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts @@ -0,0 +1,31 @@ +import { getTrustedJwksUri, getTrustedJwtIssuers } from './jwt-issuer.util'; + +describe('trusted JWT issuers', () => { + it('derives the Keycloak realm issuer when an explicit allowlist is not configured', () => { + expect(getTrustedJwtIssuers({ KEYCLOAK_DOMAIN: 'https://identity.example/', KEYCLOAK_REALM: 'platform' })).toEqual([ + 'https://identity.example/realms/platform' + ]); + }); + + it('supports an explicit issuer allowlist and removes duplicates', () => { + expect( + getTrustedJwtIssuers({ + JWT_TRUSTED_ISSUERS: + 'https://id-a.example/realms/a, https://id-b.example/realms/b/,https://id-a.example/realms/a' + }) + ).toEqual(['https://id-a.example/realms/a', 'https://id-b.example/realms/b']); + }); + + it('builds a JWKS URI only for an allowlisted issuer', () => { + expect( + getTrustedJwksUri('https://identity.example/realms/platform', ['https://identity.example/realms/platform']) + ).toBe('https://identity.example/realms/platform/protocol/openid-connect/certs'); + expect(() => + getTrustedJwksUri('https://attacker.example/realms/platform', ['https://identity.example/realms/platform']) + ).toThrow('JWT issuer is not trusted'); + }); + + it('fails closed when no trusted issuer configuration exists', () => { + expect(() => getTrustedJwtIssuers({})).toThrow('JWT_TRUSTED_ISSUERS'); + }); +}); diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.ts b/apps/api-gateway/src/authz/jwt-issuer.util.ts new file mode 100644 index 000000000..1faff5e31 --- /dev/null +++ b/apps/api-gateway/src/authz/jwt-issuer.util.ts @@ -0,0 +1,51 @@ +import { CommonConstants } from '@credebl/common/common.constant'; + +const trimTrailingSlashes = (value: string): string => value.trim().replace(/\/+$/, ''); + +const validateIssuer = (value: string): string => { + const issuer = trimTrailingSlashes(value); + const parsed = new URL(issuer); + + if ( + !['http:', 'https:'].includes(parsed.protocol) || + parsed.username || + parsed.password || + parsed.search || + parsed.hash + ) { + throw new Error(`Invalid trusted JWT issuer: ${value}`); + } + + return issuer; +}; + +export const getTrustedJwtIssuers = (env: NodeJS.ProcessEnv = process.env): string[] => { + const configuredIssuers = env.JWT_TRUSTED_ISSUERS?.split(',') + .map((issuer) => issuer.trim()) + .filter(Boolean); + + const issuers = configuredIssuers?.length + ? configuredIssuers + : env.KEYCLOAK_DOMAIN && env.KEYCLOAK_REALM + ? [`${trimTrailingSlashes(env.KEYCLOAK_DOMAIN)}/realms/${env.KEYCLOAK_REALM.trim()}`] + : []; + + if (0 === issuers.length) { + throw new Error('JWT_TRUSTED_ISSUERS or both KEYCLOAK_DOMAIN and KEYCLOAK_REALM must be configured'); + } + + return [...new Set(issuers.map(validateIssuer))]; +}; + +export const getTrustedJwksUri = (issuer: unknown, trustedIssuers: string[]): string => { + if ('string' !== typeof issuer) { + throw new Error('JWT issuer is missing'); + } + + const normalizedIssuer = trimTrailingSlashes(issuer); + if (!trustedIssuers.includes(normalizedIssuer)) { + throw new Error('JWT issuer is not trusted'); + } + + return `${normalizedIssuer}${CommonConstants.URL_KEYCLOAK_JWKS}`; +}; diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts index 3dfbd9704..6d57320b9 100644 --- a/apps/api-gateway/src/authz/jwt.strategy.ts +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -2,7 +2,7 @@ import * as dotenv from 'dotenv'; import * as jwt from 'jsonwebtoken'; -import { CommonConstants, uuidRegex } from '@credebl/common/common.constant'; +import { uuidRegex } from '@credebl/common/common.constant'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable, Logger, NotFoundException, UnauthorizedException } from '@nestjs/common'; @@ -14,6 +14,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { ResponseMessages } from '@credebl/common/response-messages'; import { UserService } from '../user/user.service'; import { passportJwtSecret } from 'jwks-rsa'; +import { getTrustedJwksUri, getTrustedJwtIssuers } from './jwt-issuer.util'; dotenv.config(); @@ -26,6 +27,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { private readonly organizationService: OrganizationService, private readonly authzService: AuthzService ) { + const trustedIssuers = getTrustedJwtIssuers(); super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKeyProvider: async (request, jwtToken, done) => { @@ -36,21 +38,21 @@ export class JwtStrategy extends PassportStrategy(Strategy) { return done(new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken), null); } - const audiance = decodedToken.iss.toString(); - const jwtOptions = { - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, - jwksUri: `${audiance}${CommonConstants.URL_KEYCLOAK_JWKS}` - }; - const secretprovider = passportJwtSecret(jwtOptions); - let certkey; - secretprovider(request, jwtToken, async (err, data) => { - certkey = data; - done(null, certkey); - }); + try { + const jwtOptions = { + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: getTrustedJwksUri(decodedToken.iss, trustedIssuers) + }; + const secretprovider = passportJwtSecret(jwtOptions); + secretprovider(request, jwtToken, (err, data) => done(err, data)); + } catch (error) { + return done(new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken), null); + } }, - algorithms: ['RS256'] + algorithms: ['RS256'], + issuer: trustedIssuers }); } diff --git a/apps/api-gateway/src/authz/mobile-jwt.strategy.ts b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts index be5b49fc7..2a99070be 100644 --- a/apps/api-gateway/src/authz/mobile-jwt.strategy.ts +++ b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts @@ -2,50 +2,51 @@ import * as dotenv from 'dotenv'; import * as jwt from 'jsonwebtoken'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { CommonConstants } from '@credebl/common/common.constant'; import { PassportStrategy } from '@nestjs/passport'; import { passportJwtSecret } from 'jwks-rsa'; +import { getTrustedJwksUri, getTrustedJwtIssuers } from './jwt-issuer.util'; dotenv.config(); -const logger = new Logger(); + +interface MobileJwtPayload { + azp?: string; + iss?: string; + [key: string]: unknown; +} @Injectable() export class MobileJwtStrategy extends PassportStrategy(Strategy, 'mobile-jwt') { - private readonly logger = new Logger(); - constructor() { + const trustedIssuers = getTrustedJwtIssuers(); super({ - secretOrKeyProvider: (request, jwtToken, done) => { - const decodedToken: any = jwt.decode(jwtToken); - const audiance = decodedToken.iss.toString(); - const jwtOptions = { - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, - jwksUri: `${audiance}${CommonConstants.URL_KEYCLOAK_JWKS}` - }; - const secretprovider = passportJwtSecret(jwtOptions); - let certkey; - secretprovider(request, jwtToken, async (err, data) => { - certkey = data; - done(null, certkey); - }); + const decodedToken = jwt.decode(jwtToken); + const issuer = decodedToken && 'object' === typeof decodedToken ? decodedToken.iss : undefined; + try { + const jwtOptions = { + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: getTrustedJwksUri(issuer, trustedIssuers) + }; + const secretprovider = passportJwtSecret(jwtOptions); + secretprovider(request, jwtToken, (err, data) => done(err, data)); + } catch (error) { + return done(new UnauthorizedException('Authorization header contains an invalid token'), null); + } }, jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - algorithms: ['RS256'] + algorithms: ['RS256'], + issuer: trustedIssuers }); } - validate(payload: any) { + validate(payload: MobileJwtPayload): MobileJwtPayload { if ('adeyaClient' !== payload.azp) { - throw new UnauthorizedException( - 'Authorization header contains an invalid token' - ); + throw new UnauthorizedException('Authorization header contains an invalid token'); } else { return payload; } - } } diff --git a/apps/api-gateway/src/fido/fido.controller.spec.ts b/apps/api-gateway/src/fido/fido.controller.spec.ts new file mode 100644 index 000000000..e82505d52 --- /dev/null +++ b/apps/api-gateway/src/fido/fido.controller.spec.ts @@ -0,0 +1,55 @@ +import { ForbiddenException } from '@nestjs/common'; +import { FidoController } from './fido.controller'; + +const response = (): { status: jest.Mock; json: jest.Mock } => { + const res = { + status: jest.fn(), + json: jest.fn() + }; + res.status.mockReturnValue(res); + res.json.mockReturnValue(res); + return res; +}; + +describe('FidoController passkey management authorization', () => { + const fidoService = { + fetchFidoUserDetails: jest.fn(), + updateFidoUserDeviceName: jest.fn(), + deleteFidoUserDevice: jest.fn() + }; + const controller = new FidoController(fidoService as never); + + beforeEach(() => jest.clearAllMocks()); + + it('rejects access to another user passkey details', async () => { + await expect( + controller.fetchFidoUserDetails( + { user: { email: 'owner@example.com' } }, + 'other@example.com', + response() as never + ) + ).rejects.toBeInstanceOf(ForbiddenException); + expect(fidoService.fetchFidoUserDetails).not.toHaveBeenCalled(); + }); + + it('passes the authenticated identity to device updates', async () => { + fidoService.updateFidoUserDeviceName.mockResolvedValue({ response: 'updated' }); + await controller.updateFidoUserDeviceName( + { user: { email: 'Owner@Example.com' } }, + 'credential-id', + 'phone', + response() as never + ); + expect(fidoService.updateFidoUserDeviceName).toHaveBeenCalledWith('credential-id', 'phone', 'owner@example.com'); + }); + + it('passes the authenticated identity to device deletion', async () => { + fidoService.deleteFidoUserDevice.mockResolvedValue({ response: 'deleted' }); + await controller.deleteFidoUserDevice( + { user: { email: 'owner@example.com' } }, + 'credential-id', + response() as never + ); + expect(fidoService.deleteFidoUserDevice).toHaveBeenCalledWith('credential-id', 'owner@example.com'); + }); +}); diff --git a/apps/api-gateway/src/fido/fido.controller.ts b/apps/api-gateway/src/fido/fido.controller.ts index 7b6695e2e..59ebbf52c 100644 --- a/apps/api-gateway/src/fido/fido.controller.ts +++ b/apps/api-gateway/src/fido/fido.controller.ts @@ -5,13 +5,15 @@ import { Get, HttpStatus, Logger, + ForbiddenException, Param, Post, Put, Query, Request, Res, - UseFilters + UseFilters, + UseGuards } from '@nestjs/common'; import { ApiBadRequestResponse, @@ -42,6 +44,7 @@ import { Response } from 'express'; import { Roles } from '../authz/decorators/roles.decorator'; import { OrgRoles } from 'libs/org-roles/enums'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; +import { AuthGuard } from '@nestjs/passport'; @UseFilters(CustomExceptionFilter) @Controller('auth') @@ -60,8 +63,7 @@ export class FidoController { * @returns User details */ @Get('/passkey/:email') - // TODO: Check if roles are required here? - // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt')) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiBearerAuth() @ApiOperation({ @@ -74,7 +76,11 @@ export class FidoController { @ApiBadRequestResponse({ description: 'Bad Request', type: BadRequestErrorDto }) async fetchFidoUserDetails(@Request() req, @Param('email') email: string, @Res() res: Response): Promise { try { - const fidoUserDetails = await this.fidoService.fetchFidoUserDetails(email.toLowerCase()); + const authenticatedEmail = this.getAuthenticatedEmail(req); + if (authenticatedEmail !== email.toLowerCase()) { + throw new ForbiddenException('Passkey details can only be accessed by their owner'); + } + const fidoUserDetails = await this.fidoService.fetchFidoUserDetails(authenticatedEmail); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.user.success.fetchUsers, @@ -241,19 +247,23 @@ export class FidoController { */ @Put('/passkey/:credentialId') @ApiBearerAuth() - // TODO: Check if roles are required here? - // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt')) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiOperation({ summary: 'Update fido user device name', description: 'Update the device name of a FIDO user.' }) @ApiQuery({ name: 'deviceName', required: true }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) async updateFidoUserDeviceName( + @Request() req, @Param('credentialId') credentialId: string, @Query('deviceName') deviceName: string, @Res() res: Response ): Promise { try { - const updateDeviceName = await this.fidoService.updateFidoUserDeviceName(credentialId, deviceName); + const updateDeviceName = await this.fidoService.updateFidoUserDeviceName( + credentialId, + deviceName, + this.getAuthenticatedEmail(req) + ); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.fido.success.updateDeviceName, @@ -274,14 +284,17 @@ export class FidoController { */ @Delete('/passkey/:credentialId') @ApiBearerAuth() - // TODO: Check if roles are required here? - // @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt')) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiOperation({ summary: 'Delete fido user device', description: 'Delete a FIDO user device by its credential ID.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) - async deleteFidoUserDevice(@Param('credentialId') credentialId: string, @Res() res: Response): Promise { + async deleteFidoUserDevice( + @Request() req, + @Param('credentialId') credentialId: string, + @Res() res: Response + ): Promise { try { - const deleteFidoUser = await this.fidoService.deleteFidoUserDevice(credentialId); + const deleteFidoUser = await this.fidoService.deleteFidoUserDevice(credentialId, this.getAuthenticatedEmail(req)); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.fido.success.deleteDevice, @@ -293,4 +306,12 @@ export class FidoController { throw error; } } + + private getAuthenticatedEmail(req): string { + const email = req?.user?.email; + if ('string' !== typeof email || !email.trim()) { + throw new ForbiddenException('Authenticated user email is required for passkey management'); + } + return email.trim().toLowerCase(); + } } diff --git a/apps/api-gateway/src/fido/fido.service.ts b/apps/api-gateway/src/fido/fido.service.ts index 0b131bbbb..232f5cc00 100644 --- a/apps/api-gateway/src/fido/fido.service.ts +++ b/apps/api-gateway/src/fido/fido.service.ts @@ -58,13 +58,17 @@ export class FidoService extends BaseService { return this.natsClient.sendNats(this.fidoServiceProxy, 'fetch-fido-user-details', payload); } - async deleteFidoUserDevice(credentialId: string): Promise<{ response: object }> { - const payload = { credentialId }; + async deleteFidoUserDevice(credentialId: string, actorEmail: string): Promise<{ response: object }> { + const payload = { credentialId, actorEmail }; return this.natsClient.sendNats(this.fidoServiceProxy, 'delete-fido-user-device', payload); } - async updateFidoUserDeviceName(credentialId: string, deviceName: string): Promise<{ response: string }> { - const payload = { credentialId, deviceName }; + async updateFidoUserDeviceName( + credentialId: string, + deviceName: string, + actorEmail: string + ): Promise<{ response: string }> { + const payload = { credentialId, deviceName, actorEmail }; return this.natsClient.sendNats(this.fidoServiceProxy, 'update-fido-user-device-name', payload); } } diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts new file mode 100644 index 000000000..c26b4fe02 --- /dev/null +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts @@ -0,0 +1,62 @@ +/* eslint-disable camelcase */ +import { RandomBitmapIndexAllocator, StatusListAllocatorService } from './status-list-allocator.service'; + +describe('RandomBitmapIndexAllocator', () => { + it('does not allocate an existing index', () => { + const allocator = new RandomBitmapIndexAllocator(8, new Uint8Array([0b11111110])); + expect(allocator.allocate()).toBe(0); + expect(allocator.getAllocatedCount()).toBe(8); + expect(() => allocator.allocate()).toThrow('No indexes left'); + }); + + it('releases an allocated index', () => { + const allocator = new RandomBitmapIndexAllocator(8, new Uint8Array([0b00000001])); + allocator.release(0); + expect(allocator.isIndexAllocated(0)).toBe(false); + expect(allocator.getAllocatedCount()).toBe(0); + }); +}); + +describe('StatusListAllocatorService', () => { + it('takes a transaction-scoped lock before reading the active list', async () => { + const calls: string[] = []; + const activeList = { + id: 'allocation-id', + orgId: 'org-id', + issuerDid: 'did:example:issuer', + listId: '00000000-0000-0000-0000-000000000001', + listSize: 8, + allocatedCount: 0, + bitmap: Buffer.from([0]), + isActive: true + }; + const tx = { + $executeRaw: jest.fn(() => { + calls.push('lock'); + return Promise.resolve(1); + }), + status_list_allocation: { + findFirst: jest.fn(() => { + calls.push('find'); + return Promise.resolve(activeList); + }), + update: jest.fn(() => Promise.resolve(activeList)), + updateMany: jest.fn(), + create: jest.fn() + } + }; + const prisma = { + $transaction: jest.fn((callback) => callback(tx)) + }; + + const result = await new StatusListAllocatorService(prisma as never).allocate( + activeList.orgId, + activeList.issuerDid, + activeList.listSize + ); + + expect(calls.slice(0, 2)).toEqual(['lock', 'find']); + expect(result.listId).toBe(activeList.listId); + expect(tx.status_list_allocation.update).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.ts index 028f9c38c..3ae14d45c 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.ts @@ -113,16 +113,22 @@ export class StatusListAllocatorService { } const defaultListSize = CommonConstants.DEFAULT_STATUS_LIST_SIZE; return this.prisma.$transaction(async (tx) => { + // Serialize allocations for the same tenant and issuer. A database-level lock is + // required because multiple service replicas can execute this code concurrently. + const allocationLockKey = `${orgId}:${issuerDid}`; + await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${allocationLockKey}, 0))`; + // Find active list or create one let activeList = await tx.status_list_allocation.findFirst({ - where: { orgId, issuerDid, isActive: true } + where: { orgId, issuerDid, isActive: true }, + orderBy: { createDateTime: 'desc' } }); if (!activeList || activeList.allocatedCount >= activeList.listSize) { // Mark inactive if full if (activeList) { - await tx.status_list_allocation.update({ - where: { id: activeList.id }, + await tx.status_list_allocation.updateMany({ + where: { orgId, issuerDid, isActive: true }, data: { isActive: false } }); } @@ -157,12 +163,12 @@ export class StatusListAllocatorService { return { listId: activeList.listId, index }; } catch (error) { if ('No indexes left' === error.message) { - // Retry rotation if we hit the race condition limit + // A mismatched bitmap/count should not leave a full list active. await tx.status_list_allocation.update({ where: { id: activeList.id }, data: { isActive: false } }); - throw new Error('Retry allocation, list full'); + throw new Error('Status list bitmap is full'); } throw error; } @@ -197,7 +203,17 @@ export class StatusListAllocatorService { async release(listId: string, index: number): Promise { await this.prisma.$transaction(async (tx) => { - const allocation = await tx.status_list_allocation.findUnique({ + let allocation = await tx.status_list_allocation.findUnique({ + where: { listId } + }); + + if (!allocation) { + return; + } + + const allocationLockKey = `${allocation.orgId}:${allocation.issuerDid}`; + await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${allocationLockKey}, 0))`; + allocation = await tx.status_list_allocation.findUnique({ where: { listId } }); @@ -209,7 +225,7 @@ export class StatusListAllocatorService { allocator.release(index); await tx.status_list_allocation.update({ - where: { listId }, + where: { id: allocation.id }, data: { bitmap: Buffer.from(allocator.export()), allocatedCount: allocator.getAllocatedCount() diff --git a/apps/user/src/fido/dtos/fido-user.dto.ts b/apps/user/src/fido/dtos/fido-user.dto.ts index e8a5ea82a..9c4e0ad14 100644 --- a/apps/user/src/fido/dtos/fido-user.dto.ts +++ b/apps/user/src/fido/dtos/fido-user.dto.ts @@ -1,160 +1,167 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsArray, IsBoolean, IsOptional, IsString, ValidateNested } from 'class-validator'; export class GenerateRegistrationDto { - email: string; - - @IsOptional() - @ApiProperty({ example: 'false' }) - @IsBoolean({ message: 'isPasskey should be boolean' }) - deviceFlag: boolean; + email: string; + + @IsOptional() + @ApiProperty({ example: 'false' }) + @IsBoolean({ message: 'isPasskey should be boolean' }) + deviceFlag: boolean; } class ResponseDto { - @ApiProperty() - @IsString() - attestationObject: string; - - @ApiProperty() - @IsString() - clientDataJSON: string; - - @ApiProperty() - @IsArray() - transports: string[]; - } - - class ClientExtensionResultsDto { - @ApiProperty() - @ValidateNested() - credProps: Record; - } - - export class VerifyRegistrationDetailsDto { - @ApiProperty() - @IsString() - id: string; - - @ApiProperty() - @IsString() - rawId: string; - - @ApiProperty() - response: ResponseDto; - - @ApiProperty() - @IsString() - type: string; - - @ApiProperty() - clientExtensionResults: ClientExtensionResultsDto; - - @ApiProperty() - @IsString() - authenticatorAttachment: string; - - @ApiProperty() - @IsString() - challangeId: string; - } - - export class VerifyRegistrationPayloadDto { - @ApiProperty() - verifyRegistrationDetails: VerifyRegistrationDetailsDto; - - @ApiProperty() - @IsString() - email: string; - } - - export class GenerateAuthentication { - userName: string; - } - - + @ApiProperty() + @IsString() + attestationObject: string; + + @ApiProperty() + @IsString() + clientDataJSON: string; + + @ApiProperty() + @IsArray() + transports: string[]; +} + +class ClientExtensionResultsDto { + @ApiProperty() + @ValidateNested() + credProps: Record; +} + +export class VerifyRegistrationDetailsDto { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @IsString() + rawId: string; + + @ApiProperty() + response: ResponseDto; + + @ApiProperty() + @IsString() + type: string; + + @ApiProperty() + clientExtensionResults: ClientExtensionResultsDto; + + @ApiProperty() + @IsString() + authenticatorAttachment: string; + + @ApiProperty() + @IsString() + challangeId: string; +} + +export class VerifyRegistrationPayloadDto { + @ApiProperty() + verifyRegistrationDetails: VerifyRegistrationDetailsDto; + + @ApiProperty() + @IsString() + email: string; +} + +export class GenerateAuthentication { + userName: string; +} + class VerifyAuthenticationResponseDto { - @ApiProperty() - @IsString() - authenticatorData: string; - - @ApiProperty() - @IsString() - clientDataJSON: string; - - @ApiProperty() - @IsString() - signature: string; - - @ApiProperty() - @IsString() - userHandle: string; - } - - export class VerifyAuthenticationDto { - @ApiProperty() - @IsString() - id: string; - - @ApiProperty() - @IsString() - rawId: string; - - @ApiProperty() - response: VerifyAuthenticationResponseDto; - - @ApiProperty() - @IsString() - type: string; - - @ApiProperty() - clientExtensionResults: ClientExtensionResultsDto; - - @ApiProperty() - @IsString() - authenticatorAttachment: string; - - @ApiProperty() - @IsString() - challangeId: string; - } - - export class VerifyAuthenticationPayloadDto { - @ApiProperty() - verifyAuthenticationDetails: VerifyAuthenticationDto; - email: string; - } - - export class UpdateFidoUserDetailsDto { - @ApiProperty() - @IsString() - userName: string; - - @ApiProperty() - @IsString() - credentialId: string; - - @ApiProperty() - @IsString() - deviceFriendlyName: string; - } - - export class UserNameDto { - @ApiProperty() - @IsString() - email: string; - } - - export class credentialDto { - @ApiProperty() - @IsString() - credentialId: string; - } - - export class updateDeviceDto { - @ApiProperty() - @IsString() - credentialId: string; - - @ApiProperty() - @IsString() - deviceName: string; - } \ No newline at end of file + @ApiProperty() + @IsString() + authenticatorData: string; + + @ApiProperty() + @IsString() + clientDataJSON: string; + + @ApiProperty() + @IsString() + signature: string; + + @ApiProperty() + @IsString() + userHandle: string; +} + +export class VerifyAuthenticationDto { + @ApiProperty() + @IsString() + id: string; + + @ApiProperty() + @IsString() + rawId: string; + + @ApiProperty() + response: VerifyAuthenticationResponseDto; + + @ApiProperty() + @IsString() + type: string; + + @ApiProperty() + clientExtensionResults: ClientExtensionResultsDto; + + @ApiProperty() + @IsString() + authenticatorAttachment: string; + + @ApiProperty() + @IsString() + challangeId: string; +} + +export class VerifyAuthenticationPayloadDto { + @ApiProperty() + verifyAuthenticationDetails: VerifyAuthenticationDto; + email: string; +} + +export class UpdateFidoUserDetailsDto { + @ApiProperty() + @IsString() + userName: string; + + @ApiProperty() + @IsString() + credentialId: string; + + @ApiProperty() + @IsString() + deviceFriendlyName: string; +} + +export class UserNameDto { + @ApiProperty() + @IsString() + email: string; +} + +export class credentialDto { + @ApiProperty() + @IsString() + credentialId: string; + + @ApiProperty() + @IsString() + actorEmail: string; +} + +export class updateDeviceDto { + @ApiProperty() + @IsString() + credentialId: string; + + @ApiProperty() + @IsString() + deviceName: string; + + @ApiProperty() + @IsString() + actorEmail: string; +} diff --git a/apps/user/src/fido/fido.service.spec.ts b/apps/user/src/fido/fido.service.spec.ts new file mode 100644 index 000000000..38daf844f --- /dev/null +++ b/apps/user/src/fido/fido.service.spec.ts @@ -0,0 +1,51 @@ +jest.mock('../user.service', () => ({ UserService: jest.fn() })); + +import { FidoService } from './fido.service'; + +describe('FidoService passkey device ownership', () => { + const fidoUserRepository = { + checkFidoUserExist: jest.fn() + }; + const userDevicesRepository = { + checkUserDeviceByCredentialId: jest.fn(), + deleteUserDeviceByCredentialId: jest.fn(), + updateUserDeviceByCredentialId: jest.fn() + }; + const service = new FidoService( + fidoUserRepository as never, + userDevicesRepository as never, + {} as never, + {} as never, + {} as never + ); + + beforeEach(() => jest.clearAllMocks()); + + it('does not delete a credential owned by another user', async () => { + fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ + id: 'device-id', + userId: 'other-user-id', + deletedAt: null + }); + + await expect( + service.deleteFidoUserDevice({ credentialId: 'credential-id', actorEmail: 'actor@example.com' }) + ).rejects.toBeDefined(); + expect(userDevicesRepository.deleteUserDeviceByCredentialId).not.toHaveBeenCalled(); + }); + + it('deletes a credential owned by the authenticated user', async () => { + fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ + id: 'device-id', + userId: 'actor-id', + deletedAt: null + }); + userDevicesRepository.deleteUserDeviceByCredentialId.mockResolvedValue({ count: 1 }); + + await expect( + service.deleteFidoUserDevice({ credentialId: 'credential-id', actorEmail: 'actor@example.com' }) + ).resolves.toBe('Device deleted successfully'); + }); +}); diff --git a/apps/user/src/fido/fido.service.ts b/apps/user/src/fido/fido.service.ts index e5b3b90db..d2dab3ff1 100644 --- a/apps/user/src/fido/fido.service.ts +++ b/apps/user/src/fido/fido.service.ts @@ -1,246 +1,283 @@ -import { BadRequestException, Injectable, Logger, NotFoundException, InternalServerErrorException } from '@nestjs/common'; //InternalServerErrorException +import { + BadRequestException, + ForbiddenException, + Injectable, + Logger, + NotFoundException, + InternalServerErrorException +} from '@nestjs/common'; //InternalServerErrorException import { CommonService } from '@credebl/common'; import { ResponseMessages } from '@credebl/common/response-messages'; import { RpcException } from '@nestjs/microservices'; import { FidoUserRepository } from '../../repositories/fido-user.repository'; -import { GenerateRegistrationDto, VerifyRegistrationPayloadDto, VerifyAuthenticationPayloadDto, UpdateFidoUserDetailsDto, credentialDto, updateDeviceDto } from './dtos/fido-user.dto'; +import { + GenerateRegistrationDto, + VerifyRegistrationPayloadDto, + VerifyAuthenticationPayloadDto, + UpdateFidoUserDetailsDto, + credentialDto, + updateDeviceDto +} from './dtos/fido-user.dto'; import { UserDevicesRepository } from '../../repositories/user-device.repository'; import { PrismaService } from '@credebl/prisma-service'; import { LoginUserDto } from 'apps/user/dtos/login-user.dto'; import { UserService } from '../user.service'; +interface PasskeyDeviceOwnership { + id: string; + userId: string | null; + deletedAt: Date | null; +} + @Injectable() export class FidoService { - private readonly logger = new Logger('PS-Fido-Service'); - constructor( - private readonly fidoUserRepository: FidoUserRepository, - private readonly userDevicesRepository: UserDevicesRepository, - private readonly commonService: CommonService, - private readonly userService: UserService, - private readonly prisma: PrismaService - ) { } - async generateRegistration(payload: GenerateRegistrationDto): Promise { - try { - const { email, deviceFlag } = payload; - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); - if (!fidoUser && !fidoUser.id) { - throw new NotFoundException(ResponseMessages.user.error.notFound); - } - - if (!fidoUser || true === deviceFlag || false === deviceFlag) { - const generatedOption = await this.generateRegistrationOption(email.toLowerCase()); - return generatedOption; - } else if (!fidoUser.isFidoVerified) { - const generatedOption = await this.updateUserRegistrationOption(email.toLowerCase()); - return generatedOption; - } else { - throw new BadRequestException(ResponseMessages.fido.error.exists); - } - } catch (error) { - this.logger.error(`Error in generate registration option:::${error}`); - throw new RpcException(error.response); - } - } + private readonly logger = new Logger('PS-Fido-Service'); + constructor( + private readonly fidoUserRepository: FidoUserRepository, + private readonly userDevicesRepository: UserDevicesRepository, + private readonly commonService: CommonService, + private readonly userService: UserService, + private readonly prisma: PrismaService + ) {} + async generateRegistration(payload: GenerateRegistrationDto): Promise { + try { + const { email, deviceFlag } = payload; + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); + if (!fidoUser && !fidoUser.id) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } - generateRegistrationOption(email: string): Promise { - const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${email.toLowerCase()}`; - return this.commonService - .httpGet(url, { headers: { 'Content-Type': 'application/json' } }) - .then(async (response) => { - const { user } = response; - const updateUser = await this.fidoUserRepository.updateUserDetails(email.toLowerCase(), [ - {fidoUserId:user.id}, - {username:user.name} - ]); - if (updateUser.fidoUserId === user.id) { - return response; - } else { - throw new InternalServerErrorException(ResponseMessages.fido.error.generateRegistration); - } - }); + if (!fidoUser || true === deviceFlag || false === deviceFlag) { + const generatedOption = await this.generateRegistrationOption(email.toLowerCase()); + return generatedOption; + } else if (!fidoUser.isFidoVerified) { + const generatedOption = await this.updateUserRegistrationOption(email.toLowerCase()); + return generatedOption; + } else { + throw new BadRequestException(ResponseMessages.fido.error.exists); + } + } catch (error) { + this.logger.error(`Error in generate registration option:::${error}`); + throw new RpcException(error.response); } + } - updateUserRegistrationOption(email: string): Promise { - const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${email.toLowerCase()}`; - return this.commonService - .httpGet(url, { headers: { 'Content-Type': 'application/json' } }) - .then(async (response) => { - const { user } = response; - await this.fidoUserRepository.updateUserDetails(email.toLowerCase(), [ - {fidoUserId:user.id}, - {isFidoVerified:false} - ]); - return response; - }); + generateRegistrationOption(email: string): Promise { + const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${email.toLowerCase()}`; + return this.commonService + .httpGet(url, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => { + const { user } = response; + const updateUser = await this.fidoUserRepository.updateUserDetails(email.toLowerCase(), [ + { fidoUserId: user.id }, + { username: user.name } + ]); + if (updateUser.fidoUserId === user.id) { + return response; + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.generateRegistration); + } + }); + } + + updateUserRegistrationOption(email: string): Promise { + const url = `${process.env.FIDO_API_ENDPOINT}/generate-registration-options/?userName=${email.toLowerCase()}`; + return this.commonService + .httpGet(url, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => { + const { user } = response; + await this.fidoUserRepository.updateUserDetails(email.toLowerCase(), [ + { fidoUserId: user.id }, + { isFidoVerified: false } + ]); + return response; + }); + } + + async verifyRegistration(verifyRegistrationDto: VerifyRegistrationPayloadDto): Promise { + try { + const { verifyRegistrationDetails, email } = verifyRegistrationDto; + const url = `${process.env.FIDO_API_ENDPOINT}/verify-registration`; + const payload = JSON.stringify(verifyRegistrationDetails); + const response = await this.commonService.httpPost(url, payload, { + headers: { 'Content-Type': 'application/json' } + }); + if (response?.verified && email.toLowerCase()) { + await this.fidoUserRepository.updateUserDetails(email.toLowerCase(), [{ isFidoVerified: true }]); + const credentialID = response.newDevice.credentialID.replace(/=*$/, ''); + response.newDevice.credentialID = credentialID; + const getUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); + await this.userDevicesRepository.createMultiDevice(response?.newDevice, getUser.id); + return response; + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.verification); + } + } catch (error) { + this.logger.error(`Error in verify registration option:::${error}`); + throw new RpcException(error); } + } - async verifyRegistration(verifyRegistrationDto: VerifyRegistrationPayloadDto): Promise { - try { - const { verifyRegistrationDetails, email } = verifyRegistrationDto; - const url = `${process.env.FIDO_API_ENDPOINT}/verify-registration`; - const payload = JSON.stringify(verifyRegistrationDetails); - const response = await this.commonService.httpPost(url, payload, { - headers: { 'Content-Type': 'application/json' } - }); - if (response?.verified && email.toLowerCase()) { - await this.fidoUserRepository.updateUserDetails(email.toLowerCase(), [{isFidoVerified:true}]); - const credentialID = response.newDevice.credentialID.replace(/=*$/, ''); - response.newDevice.credentialID = credentialID; - const getUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); - await this.userDevicesRepository.createMultiDevice(response?.newDevice, getUser.id); - return response; - } else { - throw new InternalServerErrorException(ResponseMessages.fido.error.verification); - } - } catch (error) { - this.logger.error(`Error in verify registration option:::${error}`); - throw new RpcException(error); + async generateAuthenticationOption(email: string): Promise { + try { + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email?.toLowerCase()); + if (fidoUser && fidoUser.id) { + const fidoMultiDevice = await this.userDevicesRepository.getfidoMultiDevice(fidoUser.id); + const credentialIds = []; + if (fidoMultiDevice) { + for (const iterator of fidoMultiDevice) { + credentialIds.push(iterator.devices['credentialID']); + } + } else { + throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); } + const url = `${process.env.FIDO_API_ENDPOINT}/generate-authentication-options`; + return await this.commonService + .httpPost(url, credentialIds, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => response); + } else { + throw new BadRequestException(ResponseMessages.fido.error.invalidCredentials); + } + } catch (error) { + this.logger.error(`Error in generate authentication option:::${JSON.stringify(error)}`); + throw new RpcException(error.response); } + } + + async verifyAuthentication(verifyAuthenticationDto: VerifyAuthenticationPayloadDto): Promise { + try { + const { verifyAuthenticationDetails, email } = verifyAuthenticationDto; + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); + const fidoMultiDevice = await this.userDevicesRepository.getfidoMultiDeviceDetails(fidoUser.id); + const url = `${process.env.FIDO_API_ENDPOINT}/verify-authentication`; + const payload = { + verifyAuthenticationDetails: JSON.stringify(verifyAuthenticationDetails), + devices: fidoMultiDevice + }; - async generateAuthenticationOption(email: string): Promise { - try { - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email?.toLowerCase()); - if (fidoUser && fidoUser.id) { - const fidoMultiDevice = await this.userDevicesRepository.getfidoMultiDevice(fidoUser.id); - const credentialIds = []; - if (fidoMultiDevice) { - for (const iterator of fidoMultiDevice) { - credentialIds.push(iterator.devices['credentialID']); - } - } else { - throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); - } - const url = `${process.env.FIDO_API_ENDPOINT}/generate-authentication-options`; - return await this.commonService - .httpPost(url, credentialIds, { headers: { 'Content-Type': 'application/json' } }) - .then(async (response) => response); - } else { - throw new BadRequestException(ResponseMessages.fido.error.invalidCredentials); - } - } catch (error) { - this.logger.error(`Error in generate authentication option:::${JSON.stringify(error)}`); - throw new RpcException(error.response); + const credentialIdChars = { + '-': '+', + _: '/' + }; + + const verifyAuthenticationId = verifyAuthenticationDetails.id.replace( + /[-_]/g, + (replaceCredentialId) => credentialIdChars[replaceCredentialId] + ); + const credentialId = `${verifyAuthenticationId}`; + const getUserDevice = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); + if (getUserDevice) { + const loginCounter = getUserDevice?.authCounter + 1; + if (!payload.devices) { + throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); + } else { + return await this.commonService + .httpPost(url, payload, { headers: { 'Content-Type': 'application/json' } }) + .then(async (response) => { + if (true === response.verified) { + await this.userDevicesRepository.updateFidoAuthCounter(credentialId, loginCounter); + const userDetails: LoginUserDto = { + email, + isPasskey: response.verified + }; + const authDetails = await this.userService.login(userDetails); + return authDetails; + } else { + throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); + } + }); } + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.deviceNotFound); + } + } catch (error) { + this.logger.error(`Error in verify authentication:::${error}`); + throw new RpcException(ResponseMessages.fido.error.deviceNotFound); } + } - async verifyAuthentication(verifyAuthenticationDto: VerifyAuthenticationPayloadDto): Promise { - try { - const { verifyAuthenticationDetails, email } = verifyAuthenticationDto; - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); - const fidoMultiDevice = await this.userDevicesRepository.getfidoMultiDeviceDetails(fidoUser.id); - const url = `${process.env.FIDO_API_ENDPOINT}/verify-authentication`; - const payload = { verifyAuthenticationDetails: JSON.stringify(verifyAuthenticationDetails), devices: fidoMultiDevice }; - - const credentialIdChars = { - '-': '+', - '_': '/' - }; - - const verifyAuthenticationId = verifyAuthenticationDetails.id.replace(/[-_]/g, replaceCredentialId => credentialIdChars[replaceCredentialId]); - const credentialId = `${verifyAuthenticationId}`; - const getUserDevice = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); - if (getUserDevice) { - const loginCounter = getUserDevice?.authCounter + 1; - if (!payload.devices) { - throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); - } else { - return await this.commonService - .httpPost(url, payload, { headers: { 'Content-Type': 'application/json' } }) - .then(async (response) => { - if (true === response.verified) { - await this.userDevicesRepository.updateFidoAuthCounter(credentialId, loginCounter); - const userDetails: LoginUserDto = { - email, - isPasskey: response.verified - }; - const authDetails = await this.userService.login(userDetails); - return authDetails; - } else { - throw new BadRequestException(ResponseMessages.fido.error.deviceNotFound); - } - - }); - } - } else { - throw new InternalServerErrorException(ResponseMessages.fido.error.deviceNotFound); - } - } catch (error) { - - this.logger.error(`Error in verify authentication:::${error}`); - throw new RpcException(ResponseMessages.fido.error.deviceNotFound); - } + async updateUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto): Promise { + try { + const updateFidoUserDetails = JSON.stringify(updateFidoUserDetailsDto); + const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId( + updateFidoUserDetailsDto.credentialId + ); + + if (updateFidoUser[0].id) { + await this.userDevicesRepository.addCredentialIdAndNameById(updateFidoUser[0].id, updateFidoUserDetails); + } + if (updateFidoUser[0].id) { + return 'User updated.'; + } else { + throw new InternalServerErrorException(ResponseMessages.fido.error.updateFidoUser); + } + } catch (error) { + this.logger.error(`Error in update user details:::${error}`); + throw new RpcException(error); } + } - async updateUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto): Promise { - try { - - const updateFidoUserDetails = JSON.stringify(updateFidoUserDetailsDto); - const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId(updateFidoUserDetailsDto.credentialId); - - if (updateFidoUser[0].id) { - await this.userDevicesRepository.addCredentialIdAndNameById(updateFidoUser[0].id, updateFidoUserDetails); - - } - if (updateFidoUser[0].id) { - return 'User updated.'; - } else { - throw new InternalServerErrorException(ResponseMessages.fido.error.updateFidoUser); - } - - } catch (error) { - this.logger.error(`Error in update user details:::${error}`); - throw new RpcException(error); - } + async fetchFidoUserDetails(email: string): Promise { + try { + const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); + if (!fidoUser) { + throw new NotFoundException(ResponseMessages.user.error.notFound); + } + const multiDevice = await this.userDevicesRepository.fidoMultiDevice(fidoUser.id); + if (multiDevice) { + return multiDevice; + } else { + throw new RpcException(Error); + } + } catch (error) { + this.logger.error(`Error in fetching the user details:::${error}`); + throw new RpcException(error); } + } - async fetchFidoUserDetails(email: string): Promise { - try { - const fidoUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); - if (!fidoUser) { - throw new NotFoundException(ResponseMessages.user.error.notFound); - } - const multiDevice = await this.userDevicesRepository.fidoMultiDevice(fidoUser.id); - if (multiDevice) { - return multiDevice; - } else { - throw new RpcException(Error); - } - } catch (error) { - this.logger.error(`Error in fetching the user details:::${error}`); - throw new RpcException(error); - } + async deleteFidoUserDevice(payload: credentialDto): Promise { + try { + const { credentialId, actorEmail } = payload; + await this.assertDeviceOwnership(credentialId, actorEmail); + const deleteUserDevice = await this.userDevicesRepository.deleteUserDeviceByCredentialId(credentialId); + if (1 === deleteUserDevice.count) { + return 'Device deleted successfully'; + } else { + return 'Not deleting this device kindly verify'; + } + } catch (error) { + this.logger.error(`Error in delete user device :::${error}`); + throw new RpcException(error); } + } - async deleteFidoUserDevice(payload: credentialDto): Promise { - try { - const { credentialId } = payload; - await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); - const deleteUserDevice = await this.userDevicesRepository.deleteUserDeviceByCredentialId(credentialId); - if (1 === deleteUserDevice.count) { - return 'Device deleted successfully'; - } else { - return 'Not deleting this device kindly verify'; - } - } catch (error) { - this.logger.error(`Error in delete user device :::${error}`); - throw new RpcException(error); - } + async updateFidoUserDeviceName(payload: updateDeviceDto): Promise { + try { + const { credentialId, deviceName, actorEmail } = payload; + const getUserDevice = await this.assertDeviceOwnership(credentialId, actorEmail); + const updateUserDevice = await this.userDevicesRepository.updateUserDeviceByCredentialId( + getUserDevice.id, + deviceName + ); + if (1 === updateUserDevice.count) { + return 'Device name updated successfully.'; + } else { + return 'Device name has not been changed.'; + } + } catch (error) { + this.logger.error(`Error in delete user device :::${error}`); + throw new RpcException(error); } + } - async updateFidoUserDeviceName(payload: updateDeviceDto): Promise { - try { - const { credentialId, deviceName } = payload; - const getUserDevice = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); - const updateUserDevice = await this.userDevicesRepository.updateUserDeviceByCredentialId(getUserDevice.id, deviceName); - if (1 === updateUserDevice.count) { - return 'Device name updated successfully.'; - } else { - return 'Device name has not been changed.'; - } - } catch (error) { - this.logger.error(`Error in delete user device :::${error}`); - throw new RpcException(error); - } + private async assertDeviceOwnership(credentialId: string, actorEmail: string): Promise { + const actor = await this.fidoUserRepository.checkFidoUserExist(actorEmail.toLowerCase()); + const device = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); + + if (!device || device.userId !== actor.id || device.deletedAt) { + throw new ForbiddenException('Passkey device can only be managed by its owner'); } + + return device; + } } diff --git a/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql new file mode 100644 index 000000000..28a453589 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql @@ -0,0 +1,9 @@ +-- Prevent two service replicas from persisting the same status-list slot. +CREATE UNIQUE INDEX "issued_oid4vc_credentials_listId_index_key" +ON "issued_oid4vc_credentials"("listId", "index"); + +-- There must be at most one active list for a tenant/issuer pair. Prisma does +-- not currently model partial indexes, so this invariant lives in SQL. +CREATE UNIQUE INDEX "status_list_allocation_one_active_per_issuer_key" +ON "status_list_allocation"("orgId", "issuerDid") +WHERE "isActive" = true; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 73b12c683..d0fb6e774 100755 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -120,49 +120,49 @@ model user_org_roles { } model organisation { - id String @id @default(uuid()) @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid - name String? @db.VarChar(500) - description String? @db.VarChar(500) - orgSlug String? @unique - logoUrl String? - website String? @db.VarChar - publicProfile Boolean @default(false) - idpId String? @db.VarChar(500) - clientId String? @db.VarChar(500) - clientSecret String? @db.VarChar(500) - registrationNumber String? @db.VarChar(100) - appLaunchDetails Json? - countryId Int? - countries countries? @relation(fields: [countryId], references: [id]) - stateId Int? - states states? @relation(fields: [stateId], references: [id]) - cityId Int? - cities cities? @relation(fields: [cityId], references: [id]) - connections connections[] - credentials credentials[] - org_agents org_agents[] - orgInvitations org_invitations[] - presentations presentations[] - schema schema[] - user_activities user_activity[] - userOrgRoles user_org_roles[] - agent_invitations agent_invitations[] - credential_definition credential_definition[] - file_upload file_upload[] - oid4vc_credentials oid4vc_credentials[] - oid4vp_presentations oid4vp_presentations[] - verification_templates verification_templates[] - intent_templates intent_templates[] - intent_notices intent_notices[] - ecosystemOrgs ecosystem_orgs[] - ecosystem_invitations ecosystem_invitations[] - status_list_allocations status_list_allocation[] + id String @id @default(uuid()) @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + name String? @db.VarChar(500) + description String? @db.VarChar(500) + orgSlug String? @unique + logoUrl String? + website String? @db.VarChar + publicProfile Boolean @default(false) + idpId String? @db.VarChar(500) + clientId String? @db.VarChar(500) + clientSecret String? @db.VarChar(500) + registrationNumber String? @db.VarChar(100) + appLaunchDetails Json? + countryId Int? + countries countries? @relation(fields: [countryId], references: [id]) + stateId Int? + states states? @relation(fields: [stateId], references: [id]) + cityId Int? + cities cities? @relation(fields: [cityId], references: [id]) + connections connections[] + credentials credentials[] + org_agents org_agents[] + orgInvitations org_invitations[] + presentations presentations[] + schema schema[] + user_activities user_activity[] + userOrgRoles user_org_roles[] + agent_invitations agent_invitations[] + credential_definition credential_definition[] + file_upload file_upload[] + oid4vc_credentials oid4vc_credentials[] + oid4vp_presentations oid4vp_presentations[] + verification_templates verification_templates[] + intent_templates intent_templates[] + intent_notices intent_notices[] + ecosystemOrgs ecosystem_orgs[] + ecosystem_invitations ecosystem_invitations[] + status_list_allocations status_list_allocation[] issued_oid4vc_credentials issued_oid4vc_credentials[] - oidc_issuer oidc_issuer[] + oidc_issuer oidc_issuer[] } model org_invitations { @@ -597,12 +597,11 @@ model oidc_issuer { templates credential_templates[] batchCredentialIssuanceSize Int @default(0) - orgId String? @db.Uuid - isPrimary Boolean @default(false) + orgId String? @db.Uuid + isPrimary Boolean @default(false) organisation organisation? @relation(fields: [orgId], references: [id]) - @@index([orgAgentId]) } @@ -619,7 +618,7 @@ model oid4vc_credentials { lastChangedBy String @db.Uuid credentialConfigurationIds String[] issuedCredentials String[] - publicIssuerId String + publicIssuerId String organisation organisation @relation(fields: [orgId], references: [id], onDelete: Restrict) @@ -627,32 +626,32 @@ model oid4vc_credentials { } model oid4vp_presentations { - id String @id @default(uuid()) @db.Uuid - orgId String @db.Uuid - verificationSessionId String @unique + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + verificationSessionId String @unique presentationId String state String contextCorrelationId String - createdBy String @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid - publicVerifierId String - organisation organisation @relation(fields: [orgId], references: [id]) + createdBy String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + publicVerifierId String + organisation organisation @relation(fields: [orgId], references: [id]) } model status_list_allocation { - id String @id @default(uuid()) @db.Uuid - orgId String @db.Uuid - issuerDid String - listId String @unique @db.Uuid - listSize Int @default(131072) - allocatedCount Int @default(0) - bitmap Bytes - isActive Boolean @default(true) - createDateTime DateTime @default(now()) @db.Timestamptz(6) - updatedAt DateTime @updatedAt - organisation organisation @relation(fields: [orgId], references: [id]) + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid + issuerDid String + listId String @unique @db.Uuid + listSize Int @default(131072) + allocatedCount Int @default(0) + bitmap Bytes + isActive Boolean @default(true) + createDateTime DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt + organisation organisation @relation(fields: [orgId], references: [id]) @@index([orgId, isActive]) } @@ -670,6 +669,7 @@ model issued_oid4vc_credentials { organisation organisation? @relation(fields: [orgId], references: [id], onDelete: Restrict) + @@unique([listId, index], map: "issued_oid4vc_credentials_listId_index_key") @@index([orgId, issuanceSessionId]) } @@ -692,7 +692,7 @@ model credential_templates { issuerId String @db.Uuid issuer oidc_issuer @relation(fields: [issuerId], references: [id]) - noticeUrl String? + noticeUrl String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -712,7 +712,7 @@ model x509_certificates { expiry DateTime certificateBase64 String isImported Boolean @default(false) - keyId String? + keyId String? createdAt DateTime @default(now()) createdBy String @db.Uuid @@ -736,15 +736,15 @@ model oid4vp_verifier { } model verification_templates { - id String @id @default(uuid()) @db.Uuid - name String @db.VarChar(500) + id String @id @default(uuid()) @db.Uuid + name String @db.VarChar(500) templateJson Json - orgId String @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid - organisation organisation @relation(fields: [orgId], references: [id]) + orgId String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + organisation organisation @relation(fields: [orgId], references: [id]) signerOption SignerOption intentTemplates intent_templates[] @@ -753,28 +753,28 @@ model verification_templates { model intents { id String @id @default(uuid()) @db.Uuid - ecosystemId String @db.Uuid + ecosystemId String @db.Uuid name String @db.VarChar(500) description String? createDateTime DateTime @default(now()) @db.Timestamptz(6) createdBy String @db.Uuid lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @db.Uuid - ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) + ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) intentTemplates intent_templates[] intentNotices intent_notices[] } model intent_templates { id String @id @default(uuid()) @db.Uuid - orgId String? @db.Uuid + orgId String? @db.Uuid intentId String @db.Uuid templateId String @db.Uuid createDateTime DateTime @default(now()) @db.Timestamptz(6) createdBy String @db.Uuid lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @db.Uuid - organisation organisation? @relation(fields: [orgId], references: [id]) + organisation organisation? @relation(fields: [orgId], references: [id]) intent intents @relation(fields: [intentId], references: [id]) template verification_templates @relation(fields: [templateId], references: [id]) @@ -814,7 +814,7 @@ model ecosystem { ecosystemOrgs ecosystem_orgs[] ecosystemInvitations ecosystem_invitations[] - intent intents[] + intent intents[] ledgers Json? } @@ -830,29 +830,29 @@ model ecosystem_roles { } model ecosystem_orgs { - id String @id @default(uuid()) @db.Uuid - orgId String @db.Uuid + id String @id @default(uuid()) @db.Uuid + orgId String @db.Uuid status String deploymentMode String? - ecosystemId String @db.Uuid - ecosystemRoleId String @db.Uuid - createDateTime DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) - lastChangedBy String @db.Uuid - deletedAt DateTime? @db.Timestamp(6) - userId String @db.Uuid - ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) - ecosystemRole ecosystem_roles @relation(fields: [ecosystemRoleId], references: [id]) - organisation organisation @relation(fields: [orgId], references: [id]) - user user @relation(fields:[userId], references: [id]) + ecosystemId String @db.Uuid + ecosystemRoleId String @db.Uuid + createDateTime DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) + lastChangedBy String @db.Uuid + deletedAt DateTime? @db.Timestamp(6) + userId String @db.Uuid + ecosystem ecosystem @relation(fields: [ecosystemId], references: [id]) + ecosystemRole ecosystem_roles @relation(fields: [ecosystemRoleId], references: [id]) + organisation organisation @relation(fields: [orgId], references: [id]) + user user @relation(fields: [userId], references: [id]) @@unique([orgId, ecosystemId]) } model ecosystem_invitations { id String @id @default(uuid()) @db.Uuid - email String + email String status String userId String? @db.Uuid type String @@ -864,14 +864,13 @@ model ecosystem_invitations { lastChangedBy String @db.Uuid deletedAt DateTime? @db.Timestamp(6) - organisation organisation? @relation(fields: [invitedOrg], references: [id]) - ecosystem ecosystem? @relation(fields: [ecosystemId], references: [id]) - user user? @relation(fields: [userId], references: [id]) + organisation organisation? @relation(fields: [invitedOrg], references: [id]) + ecosystem ecosystem? @relation(fields: [ecosystemId], references: [id]) + user user? @relation(fields: [userId], references: [id]) @@unique([email, ecosystemId, invitedOrg]) } - model holder_notification { id String @id @default(uuid()) @db.Uuid sessionId String @unique From e90677eb5560433c32d9eb1a549226cf43392a5a Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 7 Aug 2026 13:00:21 +0800 Subject: [PATCH 50/58] fix(security): address passkey and status-list review findings Signed-off-by: Mark --- .../src/authz/jwt-issuer.util.spec.ts | 9 ++ apps/api-gateway/src/authz/jwt-issuer.util.ts | 10 +- .../src/fido/fido.controller.spec.ts | 26 +++++ apps/api-gateway/src/fido/fido.controller.ts | 25 ++++- apps/api-gateway/src/fido/fido.service.ts | 5 +- .../src/status-list-allocator.service.spec.ts | 80 ++++++++++++++ .../src/status-list-allocator.service.ts | 15 ++- apps/user/src/fido/dtos/fido-user.dto.ts | 13 +++ apps/user/src/fido/fido.controller.ts | 104 ++++++++++-------- apps/user/src/fido/fido.service.spec.ts | 18 ++- apps/user/src/fido/fido.service.ts | 16 +-- .../migration.sql | 24 ++++ 12 files changed, 275 insertions(+), 70 deletions(-) diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts b/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts index 809b4c13f..febc912e2 100644 --- a/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts +++ b/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts @@ -28,4 +28,13 @@ describe('trusted JWT issuers', () => { it('fails closed when no trusted issuer configuration exists', () => { expect(() => getTrustedJwtIssuers({})).toThrow('JWT_TRUSTED_ISSUERS'); }); + + it('allows HTTP only for explicit loopback issuers', () => { + expect(getTrustedJwtIssuers({ JWT_TRUSTED_ISSUERS: 'http://localhost:8080/realms/platform' })).toEqual([ + 'http://localhost:8080/realms/platform' + ]); + expect(() => getTrustedJwtIssuers({ JWT_TRUSTED_ISSUERS: 'http://identity.example/realms/platform' })).toThrow( + 'Invalid trusted JWT issuer' + ); + }); }); diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.ts b/apps/api-gateway/src/authz/jwt-issuer.util.ts index 1faff5e31..8153fa039 100644 --- a/apps/api-gateway/src/authz/jwt-issuer.util.ts +++ b/apps/api-gateway/src/authz/jwt-issuer.util.ts @@ -5,14 +5,10 @@ const trimTrailingSlashes = (value: string): string => value.trim().replace(/\/+ const validateIssuer = (value: string): string => { const issuer = trimTrailingSlashes(value); const parsed = new URL(issuer); + const isLoopbackHost = ['localhost', '127.0.0.1', '[::1]', '::1'].includes(parsed.hostname.toLowerCase()); + const usesAllowedProtocol = 'https:' === parsed.protocol || ('http:' === parsed.protocol && isLoopbackHost); - if ( - !['http:', 'https:'].includes(parsed.protocol) || - parsed.username || - parsed.password || - parsed.search || - parsed.hash - ) { + if (!usesAllowedProtocol || parsed.username || parsed.password || parsed.search || parsed.hash) { throw new Error(`Invalid trusted JWT issuer: ${value}`); } diff --git a/apps/api-gateway/src/fido/fido.controller.spec.ts b/apps/api-gateway/src/fido/fido.controller.spec.ts index e82505d52..32fc55797 100644 --- a/apps/api-gateway/src/fido/fido.controller.spec.ts +++ b/apps/api-gateway/src/fido/fido.controller.spec.ts @@ -14,6 +14,9 @@ const response = (): { status: jest.Mock; json: jest.Mock } => { describe('FidoController passkey management authorization', () => { const fidoService = { fetchFidoUserDetails: jest.fn(), + generateRegistrationOption: jest.fn(), + verifyRegistration: jest.fn(), + updateFidoUser: jest.fn(), updateFidoUserDeviceName: jest.fn(), deleteFidoUserDevice: jest.fn() }; @@ -52,4 +55,27 @@ describe('FidoController passkey management authorization', () => { ); expect(fidoService.deleteFidoUserDevice).toHaveBeenCalledWith('credential-id', 'owner@example.com'); }); + + it('rejects registration for another user', async () => { + await expect( + controller.generateRegistrationOption( + { user: { email: 'owner@example.com' } }, + { deviceFlag: false }, + 'other@example.com', + response() as never + ) + ).rejects.toBeInstanceOf(ForbiddenException); + expect(fidoService.generateRegistrationOption).not.toHaveBeenCalled(); + }); + + it('passes the authenticated identity to passkey detail updates', async () => { + fidoService.updateFidoUser.mockResolvedValue({ response: 'updated' }); + await controller.updateFidoUser( + { user: { email: 'Owner@Example.com' } }, + { credentialId: 'credential-id', deviceFriendlyName: 'phone', userName: 'owner' }, + 'credential-id', + response() as never + ); + expect(fidoService.updateFidoUser).toHaveBeenCalledWith(expect.any(Object), 'credential-id', 'owner@example.com'); + }); }); diff --git a/apps/api-gateway/src/fido/fido.controller.ts b/apps/api-gateway/src/fido/fido.controller.ts index 59ebbf52c..b7f03abbd 100644 --- a/apps/api-gateway/src/fido/fido.controller.ts +++ b/apps/api-gateway/src/fido/fido.controller.ts @@ -102,19 +102,27 @@ export class FidoController { */ @Post('/passkey/generate-registration/:email') @ApiExcludeEndpoint() + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiOperation({ summary: 'Generate registration option', description: 'Generate registration options for a FIDO user.' }) @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) async generateRegistrationOption( + @Request() req, @Body() body: GenerateRegistrationDto, @Param('email') email: string, @Res() res: Response ): Promise { try { + const authenticatedEmail = this.getAuthenticatedEmail(req); + if (authenticatedEmail !== email.toLowerCase()) { + throw new ForbiddenException('Passkey registration can only be completed by its owner'); + } const { deviceFlag } = body; - const registrationOption = await this.fidoService.generateRegistrationOption(deviceFlag, email.toLowerCase()); + const registrationOption = await this.fidoService.generateRegistrationOption(deviceFlag, authenticatedEmail); const finalResponse: IResponseType = { statusCode: HttpStatus.CREATED, message: ResponseMessages.fido.success.RegistrationOption, @@ -136,6 +144,9 @@ export class FidoController { */ @Post('/passkey/verify-registration/:email') @ApiExcludeEndpoint() + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiOperation({ summary: 'Verify registration', description: 'Verify the registration of a FIDO user.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) async verifyRegistration( @@ -144,7 +155,11 @@ export class FidoController { @Param('email') email: string, @Res() res: Response ): Promise { - const verifyRegistration = await this.fidoService.verifyRegistration(verifyRegistrationDto, email.toLowerCase()); + const authenticatedEmail = this.getAuthenticatedEmail(req); + if (authenticatedEmail !== email.toLowerCase()) { + throw new ForbiddenException('Passkey registration can only be completed by its owner'); + } + const verifyRegistration = await this.fidoService.verifyRegistration(verifyRegistrationDto, authenticatedEmail); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, message: ResponseMessages.fido.success.verifyRegistration, @@ -218,6 +233,9 @@ export class FidoController { */ @Put('/passkey/user-details/:credentialId') @ApiExcludeEndpoint() + @ApiBearerAuth() + @UseGuards(AuthGuard('jwt')) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.HOLDER, OrgRoles.ISSUER, OrgRoles.SUPER_ADMIN, OrgRoles.MEMBER) @ApiOperation({ summary: 'Update fido user details', description: 'Update the details of a FIDO user.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) async updateFidoUser( @@ -228,7 +246,8 @@ export class FidoController { ): Promise { const verifyRegistration = await this.fidoService.updateFidoUser( updateFidoUserDetailsDto, - decodeURIComponent(credentialId) + decodeURIComponent(credentialId), + this.getAuthenticatedEmail(req) ); const finalResponse: IResponseType = { statusCode: HttpStatus.OK, diff --git a/apps/api-gateway/src/fido/fido.service.ts b/apps/api-gateway/src/fido/fido.service.ts index 232f5cc00..b835c01cd 100644 --- a/apps/api-gateway/src/fido/fido.service.ts +++ b/apps/api-gateway/src/fido/fido.service.ts @@ -47,9 +47,10 @@ export class FidoService extends BaseService { async updateFidoUser( updateFidoUserDetailsDto: UpdateFidoUserDetailsDto, - credentialId: string + credentialId: string, + actorEmail: string ): Promise<{ response: object }> { - const payload = { updateFidoUserDetailsDto, credentialId }; + const payload = { updateFidoUserDetailsDto, credentialId, actorEmail }; return this.natsClient.sendNats(this.fidoServiceProxy, 'update-user', payload); } diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts index c26b4fe02..a0b1d6016 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts @@ -43,6 +43,9 @@ describe('StatusListAllocatorService', () => { update: jest.fn(() => Promise.resolve(activeList)), updateMany: jest.fn(), create: jest.fn() + }, + issued_oid4vc_credentials: { + deleteMany: jest.fn() } }; const prisma = { @@ -59,4 +62,81 @@ describe('StatusListAllocatorService', () => { expect(result.listId).toBe(activeList.listId); expect(tx.status_list_allocation.update).toHaveBeenCalledTimes(1); }); + + it('commits full-list deactivation before reporting the bitmap is full', async () => { + const activeList = { + id: 'allocation-id', + orgId: 'org-id', + issuerDid: 'did:example:issuer', + listId: '00000000-0000-0000-0000-000000000001', + listSize: 8, + allocatedCount: 0, + bitmap: Buffer.from([0b11111111]), + isActive: true + }; + const tx = { + $executeRaw: jest.fn(() => Promise.resolve(1)), + status_list_allocation: { + findFirst: jest.fn(() => Promise.resolve(activeList)), + update: jest.fn(() => Promise.resolve(activeList)), + updateMany: jest.fn(() => Promise.resolve({ count: 1 })), + create: jest.fn() + } + }; + const prisma = { + $transaction: jest.fn((callback) => callback(tx)) + }; + + await expect( + new StatusListAllocatorService(prisma as never).allocate( + activeList.orgId, + activeList.issuerDid, + activeList.listSize + ) + ).rejects.toThrow('Status list bitmap is full'); + expect(tx.status_list_allocation.update).toHaveBeenCalledWith({ + where: { id: activeList.id }, + data: { isActive: false } + }); + }); + + it('removes a persisted credential before releasing its status-list slot', async () => { + const calls: string[] = []; + const allocation = { + id: 'allocation-id', + orgId: 'org-id', + issuerDid: 'did:example:issuer', + listId: '00000000-0000-0000-0000-000000000001', + listSize: 8, + allocatedCount: 1, + bitmap: Buffer.from([0b00000001]), + isActive: true + }; + const tx = { + $executeRaw: jest.fn(() => Promise.resolve(1)), + issued_oid4vc_credentials: { + deleteMany: jest.fn(() => { + calls.push('delete-credential'); + return Promise.resolve({ count: 1 }); + }) + }, + status_list_allocation: { + findUnique: jest.fn(() => Promise.resolve(allocation)), + update: jest.fn(() => { + calls.push('release-slot'); + return Promise.resolve(allocation); + }) + } + }; + const prisma = { + $transaction: jest.fn((callback) => callback(tx)) + }; + + await new StatusListAllocatorService(prisma as never).release(allocation.listId, 0); + + expect(tx.issued_oid4vc_credentials.deleteMany).toHaveBeenCalledWith({ + where: { listId: allocation.listId, index: 0 } + }); + expect(calls).toEqual(['delete-credential', 'release-slot']); + }); }); diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.ts index 3ae14d45c..a04a16fb5 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.ts @@ -112,7 +112,7 @@ export class StatusListAllocatorService { throw new Error('orgId and issuerDid are required for status list allocation'); } const defaultListSize = CommonConstants.DEFAULT_STATUS_LIST_SIZE; - return this.prisma.$transaction(async (tx) => { + const allocation = await this.prisma.$transaction(async (tx) => { // Serialize allocations for the same tenant and issuer. A database-level lock is // required because multiple service replicas can execute this code concurrently. const allocationLockKey = `${orgId}:${issuerDid}`; @@ -168,11 +168,17 @@ export class StatusListAllocatorService { where: { id: activeList.id }, data: { isActive: false } }); - throw new Error('Status list bitmap is full'); + return undefined; } throw error; } }); + + if (!allocation) { + throw new Error('Status list bitmap is full'); + } + + return allocation; } async saveCredentialAllocation( @@ -224,6 +230,11 @@ export class StatusListAllocatorService { const allocator = new RandomBitmapIndexAllocator(allocation.listSize, new Uint8Array(allocation.bitmap)); allocator.release(index); + // A previously persisted credential must be removed before its slot can be reused. + await tx.issued_oid4vc_credentials.deleteMany({ + where: { listId, index } + }); + await tx.status_list_allocation.update({ where: { id: allocation.id }, data: { diff --git a/apps/user/src/fido/dtos/fido-user.dto.ts b/apps/user/src/fido/dtos/fido-user.dto.ts index 9c4e0ad14..344c6a146 100644 --- a/apps/user/src/fido/dtos/fido-user.dto.ts +++ b/apps/user/src/fido/dtos/fido-user.dto.ts @@ -136,6 +136,19 @@ export class UpdateFidoUserDetailsDto { deviceFriendlyName: string; } +export class UpdateFidoUserPayloadDto { + @ApiProperty() + updateFidoUserDetailsDto: UpdateFidoUserDetailsDto; + + @ApiProperty() + @IsString() + credentialId: string; + + @ApiProperty() + @IsString() + actorEmail: string; +} + export class UserNameDto { @ApiProperty() @IsString() diff --git a/apps/user/src/fido/fido.controller.ts b/apps/user/src/fido/fido.controller.ts index 676c26a6c..434efe338 100644 --- a/apps/user/src/fido/fido.controller.ts +++ b/apps/user/src/fido/fido.controller.ts @@ -1,87 +1,97 @@ import { Controller, Logger } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; -import { GenerateRegistrationDto, VerifyRegistrationPayloadDto, VerifyAuthenticationPayloadDto, UpdateFidoUserDetailsDto, UserNameDto, credentialDto, updateDeviceDto, GenerateAuthentication } from './dtos/fido-user.dto'; +import { + GenerateRegistrationDto, + VerifyRegistrationPayloadDto, + VerifyAuthenticationPayloadDto, + UpdateFidoUserPayloadDto, + UserNameDto, + credentialDto, + updateDeviceDto, + GenerateAuthentication +} from './dtos/fido-user.dto'; import { FidoService } from './fido.service'; @Controller('fido') export class FidoController { - constructor(private readonly fidoService: FidoService) { } - private readonly logger = new Logger('PS-Fido-controller'); + constructor(private readonly fidoService: FidoService) {} + private readonly logger = new Logger('PS-Fido-controller'); - /** + /** * Description: FIDO User Registration * @param payload Registration Details * @returns Get registered user response */ - @MessagePattern({ cmd: 'generate-registration-options' }) - async generateRegistrationOption(payload: GenerateRegistrationDto): Promise { - return this.fidoService.generateRegistration(payload); - } + @MessagePattern({ cmd: 'generate-registration-options' }) + async generateRegistrationOption(payload: GenerateRegistrationDto): Promise { + return this.fidoService.generateRegistration(payload); + } - /** + /** * Description: FIDO User Registration * @param payload Verify registration * @returns Get verify registration response */ - @MessagePattern({ cmd: 'verify-registration' }) - verifyRegistration(payload: VerifyRegistrationPayloadDto): Promise { - return this.fidoService.verifyRegistration(payload); - } - /** + @MessagePattern({ cmd: 'verify-registration' }) + verifyRegistration(payload: VerifyRegistrationPayloadDto): Promise { + return this.fidoService.verifyRegistration(payload); + } + /** * Description: FIDO User Verification * @param payload Authentication details * @returns Get authentication response */ - @MessagePattern({ cmd: 'generate-authentication-options' }) - generateAuthenticationOption(payload: GenerateAuthentication): Promise { - const { userName } = payload; - return this.fidoService.generateAuthenticationOption(userName); - } - /** + @MessagePattern({ cmd: 'generate-authentication-options' }) + generateAuthenticationOption(payload: GenerateAuthentication): Promise { + const { userName } = payload; + return this.fidoService.generateAuthenticationOption(userName); + } + /** * Description: FIDO User Verification * @param payload Verify authentication details * @returns Get verify authentication details response */ - @MessagePattern({ cmd: 'verify-authentication' }) - verifyAuthentication(payload: VerifyAuthenticationPayloadDto): Promise { - return this.fidoService.verifyAuthentication(payload); - } - /** + @MessagePattern({ cmd: 'verify-authentication' }) + verifyAuthentication(payload: VerifyAuthenticationPayloadDto): Promise { + return this.fidoService.verifyAuthentication(payload); + } + /** * Description: FIDO User update * @param payload User Details * @returns Get updated user detail response */ - @MessagePattern({ cmd: 'update-user' }) - updateUser(payload: UpdateFidoUserDetailsDto): Promise { - return this.fidoService.updateUser(payload); - } - /** + @MessagePattern({ cmd: 'update-user' }) + updateUser(payload: UpdateFidoUserPayloadDto): Promise { + const { updateFidoUserDetailsDto, credentialId, actorEmail } = payload; + return this.fidoService.updateUser({ ...updateFidoUserDetailsDto, credentialId, actorEmail }); + } + /** * Description: fetch FIDO user details * @param payload User name - * + * */ - @MessagePattern({ cmd: 'fetch-fido-user-details' }) - fetchFidoUserDetails(payload: UserNameDto):Promise { - return this.fidoService.fetchFidoUserDetails(payload.email); - } + @MessagePattern({ cmd: 'fetch-fido-user-details' }) + fetchFidoUserDetails(payload: UserNameDto): Promise { + return this.fidoService.fetchFidoUserDetails(payload.email); + } - /** + /** * Description: delete FIDO user details * @param payload credentialId - * + * */ - @MessagePattern({ cmd: 'delete-fido-user-device' }) - deleteFidoUserDevice(payload: credentialDto):Promise { - return this.fidoService.deleteFidoUserDevice(payload); - } + @MessagePattern({ cmd: 'delete-fido-user-device' }) + deleteFidoUserDevice(payload: credentialDto): Promise { + return this.fidoService.deleteFidoUserDevice(payload); + } - /** + /** * Description: update FIDO user details * @param payload credentialId and deviceName - * + * */ - @MessagePattern({ cmd: 'update-fido-user-device-name' }) - updateFidoUserDeviceName(payload: updateDeviceDto):Promise { - return this.fidoService.updateFidoUserDeviceName(payload); - } + @MessagePattern({ cmd: 'update-fido-user-device-name' }) + updateFidoUserDeviceName(payload: updateDeviceDto): Promise { + return this.fidoService.updateFidoUserDeviceName(payload); + } } diff --git a/apps/user/src/fido/fido.service.spec.ts b/apps/user/src/fido/fido.service.spec.ts index 38daf844f..f86da4d72 100644 --- a/apps/user/src/fido/fido.service.spec.ts +++ b/apps/user/src/fido/fido.service.spec.ts @@ -9,7 +9,9 @@ describe('FidoService passkey device ownership', () => { const userDevicesRepository = { checkUserDeviceByCredentialId: jest.fn(), deleteUserDeviceByCredentialId: jest.fn(), - updateUserDeviceByCredentialId: jest.fn() + updateUserDeviceByCredentialId: jest.fn(), + updateDeviceByCredentialId: jest.fn(), + addCredentialIdAndNameById: jest.fn() }; const service = new FidoService( fidoUserRepository as never, @@ -48,4 +50,18 @@ describe('FidoService passkey device ownership', () => { service.deleteFidoUserDevice({ credentialId: 'credential-id', actorEmail: 'actor@example.com' }) ).resolves.toBe('Device deleted successfully'); }); + + it('does not update a credential owned by another user', async () => { + fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ + id: 'device-id', + userId: 'other-user-id', + deletedAt: null + }); + + await expect( + service.updateUser({ credentialId: 'credential-id', actorEmail: 'actor@example.com' } as never) + ).rejects.toBeDefined(); + expect(userDevicesRepository.updateDeviceByCredentialId).not.toHaveBeenCalled(); + }); }); diff --git a/apps/user/src/fido/fido.service.ts b/apps/user/src/fido/fido.service.ts index d2dab3ff1..fff0ddf15 100644 --- a/apps/user/src/fido/fido.service.ts +++ b/apps/user/src/fido/fido.service.ts @@ -196,17 +196,17 @@ export class FidoService { } } - async updateUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto): Promise { + async updateUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto & { actorEmail: string }): Promise { try { - const updateFidoUserDetails = JSON.stringify(updateFidoUserDetailsDto); - const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId( - updateFidoUserDetailsDto.credentialId - ); + const { actorEmail, credentialId, ...userDetails } = updateFidoUserDetailsDto; + const ownedDevice = await this.assertDeviceOwnership(credentialId, actorEmail); + const updateFidoUserDetails = JSON.stringify({ ...userDetails, credentialId }); + const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId(credentialId); - if (updateFidoUser[0].id) { - await this.userDevicesRepository.addCredentialIdAndNameById(updateFidoUser[0].id, updateFidoUserDetails); + if (updateFidoUser[0]?.id && updateFidoUser[0].id === ownedDevice.id) { + await this.userDevicesRepository.addCredentialIdAndNameById(ownedDevice.id, updateFidoUserDetails); } - if (updateFidoUser[0].id) { + if (updateFidoUser[0]?.id) { return 'User updated.'; } else { throw new InternalServerErrorException(ResponseMessages.fido.error.updateFidoUser); diff --git a/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql index 28a453589..beadcb245 100644 --- a/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql +++ b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql @@ -1,3 +1,27 @@ +-- Fail before enforcing new invariants if legacy rows would violate them. We cannot +-- safely choose a credential allocation to delete because that changes revocation semantics. +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM "issued_oid4vc_credentials" + GROUP BY "listId", "index" + HAVING COUNT(*) > 1 + ) THEN + RAISE EXCEPTION 'Cannot create status-list slot uniqueness constraint: duplicate credential allocations exist'; + END IF; + + IF EXISTS ( + SELECT 1 + FROM "status_list_allocation" + WHERE "isActive" = true + GROUP BY "orgId", "issuerDid" + HAVING COUNT(*) > 1 + ) THEN + RAISE EXCEPTION 'Cannot create active status-list constraint: multiple active lists exist for a tenant and issuer'; + END IF; +END $$; + -- Prevent two service replicas from persisting the same status-list slot. CREATE UNIQUE INDEX "issued_oid4vc_credentials_listId_index_key" ON "issued_oid4vc_credentials"("listId", "index"); From 97c8ea2ab14db12e5e9bcfe41af8c9162b15054f Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 7 Aug 2026 16:22:22 +0800 Subject: [PATCH 51/58] test(security): cover review edge cases Signed-off-by: Mark --- .../src/authz/jwt-issuer.util.spec.ts | 23 +++++++++---------- .../src/status-list-allocator.service.spec.ts | 13 +++++++++-- apps/user/src/fido/fido.service.spec.ts | 18 +++++++++++++++ .../migration.sql | 9 ++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts b/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts index febc912e2..11f22e82f 100644 --- a/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts +++ b/apps/api-gateway/src/authz/jwt-issuer.util.spec.ts @@ -2,9 +2,8 @@ import { getTrustedJwksUri, getTrustedJwtIssuers } from './jwt-issuer.util'; describe('trusted JWT issuers', () => { it('derives the Keycloak realm issuer when an explicit allowlist is not configured', () => { - expect(getTrustedJwtIssuers({ KEYCLOAK_DOMAIN: 'https://identity.example/', KEYCLOAK_REALM: 'platform' })).toEqual([ - 'https://identity.example/realms/platform' - ]); + const env = { KEYCLOAK_DOMAIN: 'https://identity.example/', KEYCLOAK_REALM: 'platform' }; + expect(getTrustedJwtIssuers(env)).toEqual(['https://identity.example/realms/platform']); }); it('supports an explicit issuer allowlist and removes duplicates', () => { @@ -17,12 +16,13 @@ describe('trusted JWT issuers', () => { }); it('builds a JWKS URI only for an allowlisted issuer', () => { - expect( - getTrustedJwksUri('https://identity.example/realms/platform', ['https://identity.example/realms/platform']) - ).toBe('https://identity.example/realms/platform/protocol/openid-connect/certs'); - expect(() => - getTrustedJwksUri('https://attacker.example/realms/platform', ['https://identity.example/realms/platform']) - ).toThrow('JWT issuer is not trusted'); + const issuer = 'https://identity.example/realms/platform'; + const attackerIssuer = 'https://attacker.example/realms/platform'; + const trustedIssuers = [issuer]; + expect(getTrustedJwksUri(issuer, trustedIssuers)).toBe( + 'https://identity.example/realms/platform/protocol/openid-connect/certs' + ); + expect(() => getTrustedJwksUri(attackerIssuer, trustedIssuers)).toThrow('JWT issuer is not trusted'); }); it('fails closed when no trusted issuer configuration exists', () => { @@ -30,9 +30,8 @@ describe('trusted JWT issuers', () => { }); it('allows HTTP only for explicit loopback issuers', () => { - expect(getTrustedJwtIssuers({ JWT_TRUSTED_ISSUERS: 'http://localhost:8080/realms/platform' })).toEqual([ - 'http://localhost:8080/realms/platform' - ]); + const loopbackEnv = { JWT_TRUSTED_ISSUERS: 'http://localhost:8080/realms/platform' }; + expect(getTrustedJwtIssuers(loopbackEnv)).toEqual(['http://localhost:8080/realms/platform']); expect(() => getTrustedJwtIssuers({ JWT_TRUSTED_ISSUERS: 'http://identity.example/realms/platform' })).toThrow( 'Invalid trusted JWT issuer' ); diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts index a0b1d6016..e559a1cec 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts @@ -64,6 +64,7 @@ describe('StatusListAllocatorService', () => { }); it('commits full-list deactivation before reporting the bitmap is full', async () => { + const calls: string[] = []; const activeList = { id: 'allocation-id', orgId: 'org-id', @@ -78,13 +79,20 @@ describe('StatusListAllocatorService', () => { $executeRaw: jest.fn(() => Promise.resolve(1)), status_list_allocation: { findFirst: jest.fn(() => Promise.resolve(activeList)), - update: jest.fn(() => Promise.resolve(activeList)), + update: jest.fn(() => { + calls.push('deactivate'); + return Promise.resolve(activeList); + }), updateMany: jest.fn(() => Promise.resolve({ count: 1 })), create: jest.fn() } }; const prisma = { - $transaction: jest.fn((callback) => callback(tx)) + $transaction: jest.fn(async (callback) => { + const result = await callback(tx); + calls.push('commit'); + return result; + }) }; await expect( @@ -98,6 +106,7 @@ describe('StatusListAllocatorService', () => { where: { id: activeList.id }, data: { isActive: false } }); + expect(calls).toEqual(['deactivate', 'commit']); }); it('removes a persisted credential before releasing its status-list slot', async () => { diff --git a/apps/user/src/fido/fido.service.spec.ts b/apps/user/src/fido/fido.service.spec.ts index f86da4d72..8a3faef99 100644 --- a/apps/user/src/fido/fido.service.spec.ts +++ b/apps/user/src/fido/fido.service.spec.ts @@ -64,4 +64,22 @@ describe('FidoService passkey device ownership', () => { ).rejects.toBeDefined(); expect(userDevicesRepository.updateDeviceByCredentialId).not.toHaveBeenCalled(); }); + + it('does not rename a credential owned by another user', async () => { + fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ + id: 'device-id', + userId: 'other-user-id', + deletedAt: null + }); + + await expect( + service.updateFidoUserDeviceName({ + credentialId: 'credential-id', + deviceName: 'new name', + actorEmail: 'actor@example.com' + }) + ).rejects.toBeDefined(); + expect(userDevicesRepository.updateUserDeviceByCredentialId).not.toHaveBeenCalled(); + }); }); diff --git a/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql index beadcb245..5a30a7d3a 100644 --- a/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql +++ b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql @@ -1,3 +1,10 @@ +BEGIN; + +-- Keep duplicate validation and index creation in the same write-exclusion window. +-- ACCESS SHARE would permit concurrent inserts, so it is not sufficient here. +LOCK TABLE "issued_oid4vc_credentials" IN SHARE ROW EXCLUSIVE MODE; +LOCK TABLE "status_list_allocation" IN SHARE ROW EXCLUSIVE MODE; + -- Fail before enforcing new invariants if legacy rows would violate them. We cannot -- safely choose a credential allocation to delete because that changes revocation semantics. DO $$ @@ -31,3 +38,5 @@ ON "issued_oid4vc_credentials"("listId", "index"); CREATE UNIQUE INDEX "status_list_allocation_one_active_per_issuer_key" ON "status_list_allocation"("orgId", "issuerDid") WHERE "isActive" = true; + +COMMIT; From 0fc4cc4c88b1e2fd67d71f7ea37c006c02940f5c Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 7 Aug 2026 17:08:08 +0800 Subject: [PATCH 52/58] test(security): tighten passkey and allocator coverage Signed-off-by: Mark --- .../src/status-list-allocator.service.spec.ts | 20 ++++++++++++++-- .../src/status-list-allocator.service.ts | 5 ++++ apps/user/src/fido/fido.service.spec.ts | 23 +++++++++++++------ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts index e559a1cec..45a0a34ce 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts @@ -122,7 +122,17 @@ describe('StatusListAllocatorService', () => { isActive: true }; const tx = { - $executeRaw: jest.fn(() => Promise.resolve(1)), + $executeRaw: jest.fn((strings: TemplateStringsArray) => { + const query = strings.join(''); + if (query.includes('issued_oid4vc_credentials')) { + calls.push('lock-credentials'); + } else if (query.includes('status_list_allocation')) { + calls.push('lock-allocation'); + } else { + calls.push('lock-advisory'); + } + return Promise.resolve(1); + }), issued_oid4vc_credentials: { deleteMany: jest.fn(() => { calls.push('delete-credential'); @@ -146,6 +156,12 @@ describe('StatusListAllocatorService', () => { expect(tx.issued_oid4vc_credentials.deleteMany).toHaveBeenCalledWith({ where: { listId: allocation.listId, index: 0 } }); - expect(calls).toEqual(['delete-credential', 'release-slot']); + expect(calls).toEqual([ + 'lock-credentials', + 'lock-allocation', + 'lock-advisory', + 'delete-credential', + 'release-slot' + ]); }); }); diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.ts index a04a16fb5..166b1d895 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.ts @@ -209,6 +209,11 @@ export class StatusListAllocatorService { async release(listId: string, index: number): Promise { await this.prisma.$transaction(async (tx) => { + // Use the same relation lock order as the migration before reading either table. + // ROW SHARE establishes order without serializing normal allocation writes. + await tx.$executeRaw`LOCK TABLE "issued_oid4vc_credentials" IN ROW SHARE MODE`; + await tx.$executeRaw`LOCK TABLE "status_list_allocation" IN ROW SHARE MODE`; + let allocation = await tx.status_list_allocation.findUnique({ where: { listId } }); diff --git a/apps/user/src/fido/fido.service.spec.ts b/apps/user/src/fido/fido.service.spec.ts index 8a3faef99..f74827d2f 100644 --- a/apps/user/src/fido/fido.service.spec.ts +++ b/apps/user/src/fido/fido.service.spec.ts @@ -1,5 +1,6 @@ jest.mock('../user.service', () => ({ UserService: jest.fn() })); +import { ForbiddenException } from '@nestjs/common'; import { FidoService } from './fido.service'; describe('FidoService passkey device ownership', () => { @@ -23,8 +24,15 @@ describe('FidoService passkey device ownership', () => { beforeEach(() => jest.clearAllMocks()); + const mockAuthenticatedActor = (): void => { + async function resolveActor(email: string): Promise<{ id: string } | undefined> { + return 'actor@example.com' === email ? { id: 'actor-id' } : undefined; + } + fidoUserRepository.checkFidoUserExist.mockImplementation(resolveActor); + }; + it('does not delete a credential owned by another user', async () => { - fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + mockAuthenticatedActor(); userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ id: 'device-id', userId: 'other-user-id', @@ -33,12 +41,12 @@ describe('FidoService passkey device ownership', () => { await expect( service.deleteFidoUserDevice({ credentialId: 'credential-id', actorEmail: 'actor@example.com' }) - ).rejects.toBeDefined(); + ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); expect(userDevicesRepository.deleteUserDeviceByCredentialId).not.toHaveBeenCalled(); }); it('deletes a credential owned by the authenticated user', async () => { - fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + mockAuthenticatedActor(); userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ id: 'device-id', userId: 'actor-id', @@ -49,10 +57,11 @@ describe('FidoService passkey device ownership', () => { await expect( service.deleteFidoUserDevice({ credentialId: 'credential-id', actorEmail: 'actor@example.com' }) ).resolves.toBe('Device deleted successfully'); + expect(userDevicesRepository.deleteUserDeviceByCredentialId).toHaveBeenCalledWith('credential-id'); }); it('does not update a credential owned by another user', async () => { - fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + mockAuthenticatedActor(); userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ id: 'device-id', userId: 'other-user-id', @@ -61,12 +70,12 @@ describe('FidoService passkey device ownership', () => { await expect( service.updateUser({ credentialId: 'credential-id', actorEmail: 'actor@example.com' } as never) - ).rejects.toBeDefined(); + ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); expect(userDevicesRepository.updateDeviceByCredentialId).not.toHaveBeenCalled(); }); it('does not rename a credential owned by another user', async () => { - fidoUserRepository.checkFidoUserExist.mockResolvedValue({ id: 'actor-id' }); + mockAuthenticatedActor(); userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ id: 'device-id', userId: 'other-user-id', @@ -79,7 +88,7 @@ describe('FidoService passkey device ownership', () => { deviceName: 'new name', actorEmail: 'actor@example.com' }) - ).rejects.toBeDefined(); + ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); expect(userDevicesRepository.updateUserDeviceByCredentialId).not.toHaveBeenCalled(); }); }); From 443dc52089f1e5a5051ecbf7dad2a089192a00e8 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 21 Aug 2026 21:33:00 +0800 Subject: [PATCH 53/58] fix(security): address passkey and allocation edge cases Signed-off-by: Mark --- apps/api-gateway/src/authz/jwt-issuer.util.ts | 11 +- apps/api-gateway/src/authz/jwt.strategy.ts | 5 +- .../src/authz/mobile-jwt.strategy.ts | 5 +- .../src/status-list-allocator.service.spec.ts | 18 ++- .../src/status-list-allocator.service.ts | 23 +++- .../repositories/user-device.repository.ts | 126 +++++++++--------- apps/user/src/fido/fido.service.spec.ts | 15 ++- apps/user/src/fido/fido.service.ts | 47 ++++--- 8 files changed, 145 insertions(+), 105 deletions(-) diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.ts b/apps/api-gateway/src/authz/jwt-issuer.util.ts index 8153fa039..6a9faac9b 100644 --- a/apps/api-gateway/src/authz/jwt-issuer.util.ts +++ b/apps/api-gateway/src/authz/jwt-issuer.util.ts @@ -4,7 +4,12 @@ const trimTrailingSlashes = (value: string): string => value.trim().replace(/\/+ const validateIssuer = (value: string): string => { const issuer = trimTrailingSlashes(value); - const parsed = new URL(issuer); + let parsed: URL; + try { + parsed = new URL(issuer); + } catch { + throw new Error(`Invalid trusted JWT issuer: ${value}`); + } const isLoopbackHost = ['localhost', '127.0.0.1', '[::1]', '::1'].includes(parsed.hostname.toLowerCase()); const usesAllowedProtocol = 'https:' === parsed.protocol || ('http:' === parsed.protocol && isLoopbackHost); @@ -33,6 +38,10 @@ export const getTrustedJwtIssuers = (env: NodeJS.ProcessEnv = process.env): stri return [...new Set(issuers.map(validateIssuer))]; }; +export const getTrustedJwtIssuerVariants = (trustedIssuers: string[]): string[] => [ + ...new Set(trustedIssuers.flatMap((issuer) => [issuer, `${issuer}/`])) +]; + export const getTrustedJwksUri = (issuer: unknown, trustedIssuers: string[]): string => { if ('string' !== typeof issuer) { throw new Error('JWT issuer is missing'); diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts index 6d57320b9..fdb185fc4 100644 --- a/apps/api-gateway/src/authz/jwt.strategy.ts +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -14,7 +14,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { ResponseMessages } from '@credebl/common/response-messages'; import { UserService } from '../user/user.service'; import { passportJwtSecret } from 'jwks-rsa'; -import { getTrustedJwksUri, getTrustedJwtIssuers } from './jwt-issuer.util'; +import { getTrustedJwksUri, getTrustedJwtIssuerVariants, getTrustedJwtIssuers } from './jwt-issuer.util'; dotenv.config(); @@ -28,6 +28,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { private readonly authzService: AuthzService ) { const trustedIssuers = getTrustedJwtIssuers(); + const trustedIssuerVariants = getTrustedJwtIssuerVariants(trustedIssuers); super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKeyProvider: async (request, jwtToken, done) => { @@ -52,7 +53,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { } }, algorithms: ['RS256'], - issuer: trustedIssuers + issuer: trustedIssuerVariants }); } diff --git a/apps/api-gateway/src/authz/mobile-jwt.strategy.ts b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts index 2a99070be..3f28c3358 100644 --- a/apps/api-gateway/src/authz/mobile-jwt.strategy.ts +++ b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts @@ -6,7 +6,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { passportJwtSecret } from 'jwks-rsa'; -import { getTrustedJwksUri, getTrustedJwtIssuers } from './jwt-issuer.util'; +import { getTrustedJwksUri, getTrustedJwtIssuerVariants, getTrustedJwtIssuers } from './jwt-issuer.util'; dotenv.config(); interface MobileJwtPayload { @@ -19,6 +19,7 @@ interface MobileJwtPayload { export class MobileJwtStrategy extends PassportStrategy(Strategy, 'mobile-jwt') { constructor() { const trustedIssuers = getTrustedJwtIssuers(); + const trustedIssuerVariants = getTrustedJwtIssuerVariants(trustedIssuers); super({ secretOrKeyProvider: (request, jwtToken, done) => { const decodedToken = jwt.decode(jwtToken); @@ -38,7 +39,7 @@ export class MobileJwtStrategy extends PassportStrategy(Strategy, 'mobile-jwt') }, jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), algorithms: ['RS256'], - issuer: trustedIssuers + issuer: trustedIssuerVariants }); } diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts index 45a0a34ce..522e921d7 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.spec.ts @@ -75,6 +75,13 @@ describe('StatusListAllocatorService', () => { bitmap: Buffer.from([0b11111111]), isActive: true }; + const replacementList = { + ...activeList, + id: 'replacement-allocation-id', + listId: '00000000-0000-0000-0000-000000000002', + allocatedCount: 0, + bitmap: Buffer.from([0]) + }; const tx = { $executeRaw: jest.fn(() => Promise.resolve(1)), status_list_allocation: { @@ -84,7 +91,7 @@ describe('StatusListAllocatorService', () => { return Promise.resolve(activeList); }), updateMany: jest.fn(() => Promise.resolve({ count: 1 })), - create: jest.fn() + create: jest.fn(() => Promise.resolve(replacementList)) } }; const prisma = { @@ -101,12 +108,9 @@ describe('StatusListAllocatorService', () => { activeList.issuerDid, activeList.listSize ) - ).rejects.toThrow('Status list bitmap is full'); - expect(tx.status_list_allocation.update).toHaveBeenCalledWith({ - where: { id: activeList.id }, - data: { isActive: false } - }); - expect(calls).toEqual(['deactivate', 'commit']); + ).resolves.toEqual({ listId: replacementList.listId, index: expect.any(Number) }); + expect(tx.status_list_allocation.updateMany).toHaveBeenCalledTimes(1); + expect(calls).toContain('commit'); }); it('removes a persisted credential before releasing its status-list slot', async () => { diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.ts index 166b1d895..bb470f568 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.ts @@ -124,7 +124,7 @@ export class StatusListAllocatorService { orderBy: { createDateTime: 'desc' } }); - if (!activeList || activeList.allocatedCount >= activeList.listSize) { + if (!activeList) { // Mark inactive if full if (activeList) { await tx.status_list_allocation.updateMany({ @@ -146,7 +146,26 @@ export class StatusListAllocatorService { }); } - const allocator = new RandomBitmapIndexAllocator(activeList.listSize, new Uint8Array(activeList.bitmap)); + let allocator = new RandomBitmapIndexAllocator(activeList.listSize, new Uint8Array(activeList.bitmap)); + + if (allocator.getAllocatedCount() >= activeList.listSize) { + await tx.status_list_allocation.updateMany({ + where: { orgId, issuerDid, isActive: true }, + data: { isActive: false } + }); + activeList = await tx.status_list_allocation.create({ + data: { + orgId, + issuerDid, + listId: randomUUID(), + listSize: listSize || defaultListSize, + allocatedCount: 0, + bitmap: Buffer.from(new Uint8Array(Math.ceil((listSize || defaultListSize) / 8))), + isActive: true + } + }); + allocator = new RandomBitmapIndexAllocator(activeList.listSize, new Uint8Array(activeList.bitmap)); + } try { const index = allocator.allocate(); diff --git a/apps/user/repositories/user-device.repository.ts b/apps/user/repositories/user-device.repository.ts index f9935f81b..f329c0278 100644 --- a/apps/user/repositories/user-device.repository.ts +++ b/apps/user/repositories/user-device.repository.ts @@ -16,11 +16,14 @@ type FidoMultiDevicePayload = { }[]; @Injectable() export class UserDevicesRepository { - constructor(private readonly prisma: PrismaService, private readonly logger: Logger) { } + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} /** - * - * @param email + * + * @param email * @returns User exist details */ @@ -29,7 +32,7 @@ export class UserDevicesRepository { try { return this.prisma.user_devices.findFirst({ where: { - userId:String(userId) + userId: String(userId) } }); } catch (error) { @@ -39,25 +42,24 @@ export class UserDevicesRepository { } /** - * - * @param createFidoMultiDevice - * @returns Device details - */ + * + * @param createFidoMultiDevice + * @returns Device details + */ // eslint-disable-next-line camelcase - async createMultiDevice(newDevice: Prisma.JsonValue, userId: string): Promise { + async createMultiDevice(newDevice: Prisma.JsonValue, userId: string, credentialId: string): Promise { try { - const saveResponse = await this.prisma.user_devices.create({ data: { devices: newDevice, - userId:String(userId), + userId: String(userId), createdBy: userId, + credentialId, lastChangedBy: userId } }); return saveResponse; - } catch (error) { this.logger.error(`In Create User Repository: ${JSON.stringify(error)}`); throw error; @@ -65,16 +67,16 @@ export class UserDevicesRepository { } /** - * - * @param userId - * @returns Device details - */ + * + * @param userId + * @returns Device details + */ // eslint-disable-next-line camelcase async fidoMultiDevice(userId: string): Promise { try { const userDetails = await this.prisma.user_devices.findMany({ where: { - userId:String(userId), + userId: String(userId), deletedAt: null }, orderBy: { @@ -90,17 +92,16 @@ export class UserDevicesRepository { } /** - * - * @param userId - * @returns Get all device details - */ + * + * @param userId + * @returns Get all device details + */ // eslint-disable-next-line camelcase, @typescript-eslint/no-explicit-any async getfidoMultiDevice(userId: string): Promise { try { - const fidoMultiDevice = await this.prisma.user_devices.findMany({ where: { - userId:String(userId) + userId: String(userId) } }); return fidoMultiDevice; @@ -111,15 +112,15 @@ export class UserDevicesRepository { } /** - * - * @param userId - * @returns Get all active device details - */ + * + * @param userId + * @returns Get all active device details + */ async getfidoMultiDeviceDetails(userId: string): Promise { try { const fidoMultiDevice = await this.prisma.user_devices.findMany({ where: { - userId:String(userId), + userId: String(userId), deletedAt: null }, select: { @@ -141,10 +142,10 @@ export class UserDevicesRepository { } /** - * - * @param credentialId - * @returns Find device details from credentialID - */ + * + * @param credentialId + * @returns Find device details from credentialID + */ async getFidoUserDeviceDetails(credentialId: string): Promise { try { const getUserDevice = await this.prisma.$queryRaw` @@ -160,11 +161,11 @@ export class UserDevicesRepository { } /** - * - * @param credentialId - * @param loginCounter - * @returns Update Auth counter - */ + * + * @param credentialId + * @param loginCounter + * @returns Update Auth counter + */ async updateFidoAuthCounter(credentialId: string, loginCounter: number): Promise { try { return await this.prisma.user_devices.updateMany({ @@ -175,7 +176,6 @@ export class UserDevicesRepository { authCounter: loginCounter } }); - } catch (error) { this.logger.error(`Not Found: ${JSON.stringify(error)}`); throw new NotFoundException(error); @@ -183,10 +183,10 @@ export class UserDevicesRepository { } /** - * - * @param credentialId - * @returns Device detail for specific credentialId - */ + * + * @param credentialId + * @returns Device detail for specific credentialId + */ // eslint-disable-next-line camelcase async checkUserDeviceByCredentialId(credentialId: string): Promise { try { @@ -202,10 +202,10 @@ export class UserDevicesRepository { } /** - * - * @param credentialId - * @returns Delete device - */ + * + * @param credentialId + * @returns Delete device + */ // eslint-disable-next-line camelcase async deleteUserDeviceByCredentialId(credentialId: string): Promise { try { @@ -223,11 +223,11 @@ export class UserDevicesRepository { } } /** - * - * @param id - * @param deviceName - * @returns Update device name - */ + * + * @param id + * @param deviceName + * @returns Update device name + */ async updateUserDeviceByCredentialId(id: string, deviceName: string): Promise { try { return await this.prisma.user_devices.updateMany({ @@ -245,11 +245,11 @@ export class UserDevicesRepository { } /** - * - * @param credentialId - * @param deviceFriendlyName - * @returns Get device details name for specific credentialId - */ + * + * @param credentialId + * @param deviceFriendlyName + * @returns Get device details name for specific credentialId + */ async updateDeviceByCredentialId(credentialId: string): Promise { try { return await this.prisma.$queryRaw` @@ -263,15 +263,14 @@ export class UserDevicesRepository { } /** - * - * @param id - * @param credentialId - * @param deviceFriendlyName - * @returns Update device name for specific credentialId - */ + * + * @param id + * @param credentialId + * @param deviceFriendlyName + * @returns Update device name for specific credentialId + */ // eslint-disable-next-line camelcase async addCredentialIdAndNameById(id: string, updateFidoUserDetails: string): Promise { - try { return await this.prisma.user_devices.update({ where: { @@ -287,5 +286,4 @@ export class UserDevicesRepository { throw new InternalServerErrorException(error); } } - } diff --git a/apps/user/src/fido/fido.service.spec.ts b/apps/user/src/fido/fido.service.spec.ts index f74827d2f..7f6370204 100644 --- a/apps/user/src/fido/fido.service.spec.ts +++ b/apps/user/src/fido/fido.service.spec.ts @@ -5,7 +5,8 @@ import { FidoService } from './fido.service'; describe('FidoService passkey device ownership', () => { const fidoUserRepository = { - checkFidoUserExist: jest.fn() + checkFidoUserExist: jest.fn(), + getUserDetails: jest.fn() }; const userDevicesRepository = { checkUserDeviceByCredentialId: jest.fn(), @@ -28,7 +29,7 @@ describe('FidoService passkey device ownership', () => { async function resolveActor(email: string): Promise<{ id: string } | undefined> { return 'actor@example.com' === email ? { id: 'actor-id' } : undefined; } - fidoUserRepository.checkFidoUserExist.mockImplementation(resolveActor); + fidoUserRepository.getUserDetails.mockImplementation(resolveActor); }; it('does not delete a credential owned by another user', async () => { @@ -40,7 +41,7 @@ describe('FidoService passkey device ownership', () => { }); await expect( - service.deleteFidoUserDevice({ credentialId: 'credential-id', actorEmail: 'actor@example.com' }) + service.deleteFidoUserDevice({ credentialId: 'Y3JlZGVudGlhbC1pZA', actorEmail: 'actor@example.com' }) ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); expect(userDevicesRepository.deleteUserDeviceByCredentialId).not.toHaveBeenCalled(); }); @@ -55,9 +56,9 @@ describe('FidoService passkey device ownership', () => { userDevicesRepository.deleteUserDeviceByCredentialId.mockResolvedValue({ count: 1 }); await expect( - service.deleteFidoUserDevice({ credentialId: 'credential-id', actorEmail: 'actor@example.com' }) + service.deleteFidoUserDevice({ credentialId: 'Y3JlZGVudGlhbC1pZA', actorEmail: 'actor@example.com' }) ).resolves.toBe('Device deleted successfully'); - expect(userDevicesRepository.deleteUserDeviceByCredentialId).toHaveBeenCalledWith('credential-id'); + expect(userDevicesRepository.deleteUserDeviceByCredentialId).toHaveBeenCalledWith('Y3JlZGVudGlhbC1pZA'); }); it('does not update a credential owned by another user', async () => { @@ -69,7 +70,7 @@ describe('FidoService passkey device ownership', () => { }); await expect( - service.updateUser({ credentialId: 'credential-id', actorEmail: 'actor@example.com' } as never) + service.updateUser({ credentialId: 'Y3JlZGVudGlhbC1pZA', actorEmail: 'actor@example.com' } as never) ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); expect(userDevicesRepository.updateDeviceByCredentialId).not.toHaveBeenCalled(); }); @@ -84,7 +85,7 @@ describe('FidoService passkey device ownership', () => { await expect( service.updateFidoUserDeviceName({ - credentialId: 'credential-id', + credentialId: 'Y3JlZGVudGlhbC1pZA', deviceName: 'new name', actorEmail: 'actor@example.com' }) diff --git a/apps/user/src/fido/fido.service.ts b/apps/user/src/fido/fido.service.ts index fff0ddf15..3d2fede8b 100644 --- a/apps/user/src/fido/fido.service.ts +++ b/apps/user/src/fido/fido.service.ts @@ -104,10 +104,10 @@ export class FidoService { }); if (response?.verified && email.toLowerCase()) { await this.fidoUserRepository.updateUserDetails(email.toLowerCase(), [{ isFidoVerified: true }]); - const credentialID = response.newDevice.credentialID.replace(/=*$/, ''); + const credentialID = this.normalizeCredentialId(response.newDevice.credentialID); response.newDevice.credentialID = credentialID; const getUser = await this.fidoUserRepository.checkFidoUserExist(email.toLowerCase()); - await this.userDevicesRepository.createMultiDevice(response?.newDevice, getUser.id); + await this.userDevicesRepository.createMultiDevice(response?.newDevice, getUser.id, credentialID); return response; } else { throw new InternalServerErrorException(ResponseMessages.fido.error.verification); @@ -155,16 +155,7 @@ export class FidoService { devices: fidoMultiDevice }; - const credentialIdChars = { - '-': '+', - _: '/' - }; - - const verifyAuthenticationId = verifyAuthenticationDetails.id.replace( - /[-_]/g, - (replaceCredentialId) => credentialIdChars[replaceCredentialId] - ); - const credentialId = `${verifyAuthenticationId}`; + const credentialId = this.normalizeCredentialId(verifyAuthenticationDetails.id); const getUserDevice = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); if (getUserDevice) { const loginCounter = getUserDevice?.authCounter + 1; @@ -199,9 +190,10 @@ export class FidoService { async updateUser(updateFidoUserDetailsDto: UpdateFidoUserDetailsDto & { actorEmail: string }): Promise { try { const { actorEmail, credentialId, ...userDetails } = updateFidoUserDetailsDto; - const ownedDevice = await this.assertDeviceOwnership(credentialId, actorEmail); - const updateFidoUserDetails = JSON.stringify({ ...userDetails, credentialId }); - const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId(credentialId); + const normalizedCredentialId = this.normalizeCredentialId(credentialId); + const ownedDevice = await this.assertDeviceOwnership(normalizedCredentialId, actorEmail); + const updateFidoUserDetails = JSON.stringify({ ...userDetails, credentialId: normalizedCredentialId }); + const updateFidoUser = await this.userDevicesRepository.updateDeviceByCredentialId(normalizedCredentialId); if (updateFidoUser[0]?.id && updateFidoUser[0].id === ownedDevice.id) { await this.userDevicesRepository.addCredentialIdAndNameById(ownedDevice.id, updateFidoUserDetails); @@ -238,8 +230,9 @@ export class FidoService { async deleteFidoUserDevice(payload: credentialDto): Promise { try { const { credentialId, actorEmail } = payload; - await this.assertDeviceOwnership(credentialId, actorEmail); - const deleteUserDevice = await this.userDevicesRepository.deleteUserDeviceByCredentialId(credentialId); + const normalizedCredentialId = this.normalizeCredentialId(credentialId); + await this.assertDeviceOwnership(normalizedCredentialId, actorEmail); + const deleteUserDevice = await this.userDevicesRepository.deleteUserDeviceByCredentialId(normalizedCredentialId); if (1 === deleteUserDevice.count) { return 'Device deleted successfully'; } else { @@ -254,7 +247,8 @@ export class FidoService { async updateFidoUserDeviceName(payload: updateDeviceDto): Promise { try { const { credentialId, deviceName, actorEmail } = payload; - const getUserDevice = await this.assertDeviceOwnership(credentialId, actorEmail); + const normalizedCredentialId = this.normalizeCredentialId(credentialId); + const getUserDevice = await this.assertDeviceOwnership(normalizedCredentialId, actorEmail); const updateUserDevice = await this.userDevicesRepository.updateUserDeviceByCredentialId( getUserDevice.id, deviceName @@ -271,13 +265,26 @@ export class FidoService { } private async assertDeviceOwnership(credentialId: string, actorEmail: string): Promise { - const actor = await this.fidoUserRepository.checkFidoUserExist(actorEmail.toLowerCase()); + const actor = await this.fidoUserRepository.getUserDetails(actorEmail.toLowerCase()); const device = await this.userDevicesRepository.checkUserDeviceByCredentialId(credentialId); - if (!device || device.userId !== actor.id || device.deletedAt) { + if (!actor || !device || device.userId !== actor.id || device.deletedAt) { throw new ForbiddenException('Passkey device can only be managed by its owner'); } return device; } + + private normalizeCredentialId(credentialId: string): string { + if ('string' !== typeof credentialId || !/^[A-Za-z0-9+/_-]+={0,2}$/.test(credentialId)) { + throw new BadRequestException('Invalid passkey credential ID'); + } + + const unpaddedCredentialId = credentialId.replace(/=+$/, ''); + if (1 === unpaddedCredentialId.length % 4) { + throw new BadRequestException('Invalid passkey credential ID'); + } + + return unpaddedCredentialId.replace(/\+/g, '-').replace(/\//g, '_'); + } } From 71ffb7a443e4933fdaf93bc67856706962c34ba3 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 21 Aug 2026 21:49:20 +0800 Subject: [PATCH 54/58] fix(security): retain slots after offer creation Signed-off-by: Mark --- apps/api-gateway/src/authz/jwt-issuer.util.ts | 4 +--- .../src/oid4vc-issuance.service.ts | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.ts b/apps/api-gateway/src/authz/jwt-issuer.util.ts index 6a9faac9b..533ba46d4 100644 --- a/apps/api-gateway/src/authz/jwt-issuer.util.ts +++ b/apps/api-gateway/src/authz/jwt-issuer.util.ts @@ -38,9 +38,7 @@ export const getTrustedJwtIssuers = (env: NodeJS.ProcessEnv = process.env): stri return [...new Set(issuers.map(validateIssuer))]; }; -export const getTrustedJwtIssuerVariants = (trustedIssuers: string[]): string[] => [ - ...new Set(trustedIssuers.flatMap((issuer) => [issuer, `${issuer}/`])) -]; +export const getTrustedJwtIssuerVariants = (trustedIssuers: string[]): string[] => [...new Set(trustedIssuers.flatMap((issuer) => [issuer, `${issuer}/`]))]; export const getTrustedJwksUri = (issuer: unknown, trustedIssuers: string[]): string => { if ('string' !== typeof issuer) { diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index 1ba61e870..d38e09f3d 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -650,6 +650,7 @@ export class Oid4vcIssuanceService { ): Promise { let buildOidcCredentialOffer: CredentialOfferPayload; const newlyAllocatedIndices: { listId: string; index: number }[] = []; + let offerCreated = false; try { const filterTemplateIds = extractTemplateIds(createOidcCredentialOffer); if (!filterTemplateIds) { @@ -774,6 +775,7 @@ export class Oid4vcIssuanceService { if (!createCredentialOfferOnAgent) { throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorCreateOffer); } + offerCreated = true; // Logic to add noticeUrl in response from agent if it is present in template or request payload const { response } = createCredentialOfferOnAgent; @@ -823,7 +825,11 @@ export class Oid4vcIssuanceService { return createCredentialOfferOnAgent.response; } catch (error) { - for (const alloc of newlyAllocatedIndices) { + if (offerCreated && 0 < newlyAllocatedIndices.length) { + this.logger.error('Status-list allocations were retained because credential offer creation succeeded before local persistence failed'); + } + const allocationsToRelease = offerCreated ? [] : newlyAllocatedIndices; + for (const alloc of allocationsToRelease) { try { await this.statusListAllocatorService.release(alloc.listId, alloc.index); } catch (releaseErr) { @@ -842,6 +848,7 @@ export class Oid4vcIssuanceService { async createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId: string): Promise { const newlyAllocatedIndices: { listId: string; index: number }[] = []; + let offerCreated = false; try { for (const credential of oidcCredentialD2APayload.credentials) { const { signerOptions } = credential; @@ -910,6 +917,7 @@ export class Oid4vcIssuanceService { if (!createCredentialOfferOnAgent) { throw new NotFoundException(ResponseMessages.oidcIssuerSession.error.errorCreateOffer); } + offerCreated = true; let parsedResponse; if ('string' === typeof createCredentialOfferOnAgent.response) { @@ -941,7 +949,11 @@ export class Oid4vcIssuanceService { return createCredentialOfferOnAgent.response; } catch (error) { - for (const alloc of newlyAllocatedIndices) { + if (offerCreated && 0 < newlyAllocatedIndices.length) { + this.logger.error('Status-list allocations were retained because credential offer creation succeeded before local persistence failed'); + } + const allocationsToRelease = offerCreated ? [] : newlyAllocatedIndices; + for (const alloc of allocationsToRelease) { try { await this.statusListAllocatorService.release(alloc.listId, alloc.index); } catch (releaseErr) { From 816a581da39407d3630d91431f2f9da8357a9ce5 Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 21 Aug 2026 21:59:12 +0800 Subject: [PATCH 55/58] test(security): cover missing and deleted passkey devices Signed-off-by: Mark --- apps/user/src/fido/fido.service.spec.ts | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/user/src/fido/fido.service.spec.ts b/apps/user/src/fido/fido.service.spec.ts index 7f6370204..e8bdb12e3 100644 --- a/apps/user/src/fido/fido.service.spec.ts +++ b/apps/user/src/fido/fido.service.spec.ts @@ -92,4 +92,32 @@ describe('FidoService passkey device ownership', () => { ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); expect(userDevicesRepository.updateUserDeviceByCredentialId).not.toHaveBeenCalled(); }); + + it('rejects a missing device without deleting it', async () => { + mockAuthenticatedActor(); + userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue(null); + + await expect( + service.deleteFidoUserDevice({ credentialId: 'Y3JlZGVudGlhbC1pZA', actorEmail: 'actor@example.com' }) + ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); + expect(userDevicesRepository.deleteUserDeviceByCredentialId).not.toHaveBeenCalled(); + }); + + it('rejects a deleted device without renaming it', async () => { + mockAuthenticatedActor(); + userDevicesRepository.checkUserDeviceByCredentialId.mockResolvedValue({ + id: 'device-id', + userId: 'actor-id', + deletedAt: new Date() + }); + + await expect( + service.updateFidoUserDeviceName({ + credentialId: 'Y3JlZGVudGlhbC1pZA', + deviceName: 'new name', + actorEmail: 'actor@example.com' + }) + ).rejects.toEqual(expect.objectContaining({ error: expect.any(ForbiddenException) })); + expect(userDevicesRepository.updateUserDeviceByCredentialId).not.toHaveBeenCalled(); + }); }); From 63f110982bd5cd9d0c1cfc71de54f07885cdabff Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 21 Aug 2026 22:06:05 +0800 Subject: [PATCH 56/58] docs(security): clarify issuer and migration safeguards Signed-off-by: Mark --- .env.demo | 1 + .../20260806090000_harden_status_list_allocation/migration.sql | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.demo b/.env.demo index 2202d16ae..715360753 100644 --- a/.env.demo +++ b/.env.demo @@ -176,6 +176,7 @@ KEYCLOAK_MANAGEMENT_CLIENT_ID=credeblClient KEYCLOAK_MANAGEMENT_CLIENT_SECRET= KEYCLOAK_REALM=credebl-platform # Optional comma-separated allowlist. When omitted, the issuer is derived from KEYCLOAK_DOMAIN and KEYCLOAK_REALM. +# The API gateway fails closed unless this is set or both KEYCLOAK_DOMAIN and KEYCLOAK_REALM are configured. JWT_TRUSTED_ISSUERS=http://localhost:8080/realms/credebl-platform # Provide schema server URL and token diff --git a/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql index 5a30a7d3a..a1dfe58ea 100644 --- a/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql +++ b/libs/prisma-service/prisma/migrations/20260806090000_harden_status_list_allocation/migration.sql @@ -34,7 +34,8 @@ CREATE UNIQUE INDEX "issued_oid4vc_credentials_listId_index_key" ON "issued_oid4vc_credentials"("listId", "index"); -- There must be at most one active list for a tenant/issuer pair. Prisma does --- not currently model partial indexes, so this invariant lives in SQL. +-- not currently model partial indexes, so this invariant lives in SQL. Keep this index in future +-- Prisma migrations; prisma migrate dev can otherwise propose dropping it from the shadow-schema diff. CREATE UNIQUE INDEX "status_list_allocation_one_active_per_issuer_key" ON "status_list_allocation"("orgId", "issuerDid") WHERE "isActive" = true; From 3b28a10571a1f89ca03c8ec79bfd441deb055e27 Mon Sep 17 00:00:00 2001 From: Ajay Jadhav Date: Sat, 22 Aug 2026 02:14:19 +0530 Subject: [PATCH 57/58] fix(security): scrub demo secrets, backfill legacy passkey rows, drop dead code Address remaining review findings on PR #1706: - Replace the concrete OpenBao AppRole BAO_ROLE_ID / BAO_SECRET_ID values in .env.demo with non-functional zero-UUID placeholders. Real role and secret IDs must be provisioned outside version control via ./openbao-init.sh. - Add migration 20260822000000_backfill_user_devices_credential_id. Devices registered before credentialId was persisted at registration time only carried the value inside devices->>'credentialID', so ownership checks (rename/delete) and passkey authentication lookups by column miss those rows. The backfill normalizes the JSON value to unpadded base64url (strip '=' padding, '+' -> '-', '/' -> '_') to match FidoService.normalizeCredentialId, writes it to the credentialId column, and rewrites the JSON copy so both stay consistent. It also fixes legacy rows whose column held a non-normalized standard-base64 value. A preflight check aborts the migration safely if two devices collide on the same normalized credential ID, instead of failing mid update against the unique constraint. - Remove an unreachable deactivation block in StatusListAllocatorService: inside the no-active-list branch the inner 'if (activeList)' can never run; list rotation for full bitmaps is already handled below. Verified: migration smoke-tested on Postgres 16 against legacy, renamed-legacy, normalized, and collision-shaped rows; affected jest suites pass (4 suites, 21 tests). Signed-off-by: Ajay Jadhav --- .env.demo | 8 +-- .../src/status-list-allocator.service.ts | 8 --- .../migration.sql | 55 +++++++++++++++++++ 3 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql diff --git a/.env.demo b/.env.demo index 715360753..50a3c5589 100644 --- a/.env.demo +++ b/.env.demo @@ -395,8 +395,8 @@ SECRETS_PROVIDER=openbao BAO_URL=http://127.0.0.1:8200 # Default KV v2 path fetched at boot and injected into process.env BAO_SECRET_PATH=secret/data/credebl_resend_api_key -# AppRole credentials. Generate your own via: ./openbao-init.sh -# (values below are for the local 'openbao' container created by docker-compose.openbao.yml) -BAO_ROLE_ID=ca2a350b-1871-0851-8557-42b36aa9c623 -BAO_SECRET_ID=3f0d8d45-c232-f5c1-2984-fe58e5144be8 +# AppRole credentials. Provision your own outside version control via: ./openbao-init.sh +# The values below are non-functional placeholders; never commit real role/secret IDs. +BAO_ROLE_ID=00000000-0000-0000-0000-000000000000 +BAO_SECRET_ID=00000000-0000-0000-0000-000000000000 HIDE_EXPERIMENTAL_OIDC_CONTROLLERS=false diff --git a/apps/oid4vc-issuance/src/status-list-allocator.service.ts b/apps/oid4vc-issuance/src/status-list-allocator.service.ts index bb470f568..82ca25e72 100644 --- a/apps/oid4vc-issuance/src/status-list-allocator.service.ts +++ b/apps/oid4vc-issuance/src/status-list-allocator.service.ts @@ -125,14 +125,6 @@ export class StatusListAllocatorService { }); if (!activeList) { - // Mark inactive if full - if (activeList) { - await tx.status_list_allocation.updateMany({ - where: { orgId, issuerDid, isActive: true }, - data: { isActive: false } - }); - } - activeList = await tx.status_list_allocation.create({ data: { orgId, diff --git a/libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql b/libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql new file mode 100644 index 000000000..bdf26a780 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql @@ -0,0 +1,55 @@ +-- Backfill user_devices.credentialId from the credential embedded in the devices JSON. +-- +-- Devices registered before credentialId was persisted at registration time only carried +-- the value inside devices->>'credentialID'. Ownership checks and authentication lookups +-- query the column, so those legacy rows are unreachable for rename/delete and passkey login. +-- +-- Legacy JSON values were stored padding-stripped but not URL-safe encoded. They are +-- normalized to unpadded base64url (strip '=' padding, '+' -> '-', '/' -> '_') to match +-- what FidoService.normalizeCredentialId produces before every lookup. + +BEGIN; + +-- The credentialId column has a unique constraint. Fail with a clear message instead of +-- letting the UPDATE hit a constraint violation mid-migration. +DO $$ +DECLARE + collision_count int; +BEGIN + SELECT COUNT(*) + INTO collision_count + FROM ( + SELECT translate(regexp_replace("devices"->>'credentialID', '=+$', ''), '+/', '-_') AS normalized_id + FROM "user_devices" + WHERE "devices" ? 'credentialID' + AND "devices"->>'credentialID' IS NOT NULL + GROUP BY 1 + HAVING COUNT(*) > 1 + ) collisions; + + IF collision_count > 0 THEN + RAISE EXCEPTION 'Cannot backfill user_devices.credentialId: % device(s) collide on the same credential ID after normalization; resolve manually first', collision_count; + END IF; +END $$; + +WITH normalized AS ( + SELECT + "id", + translate(regexp_replace("devices"->>'credentialID', '=+$', ''), '+/', '-_') AS normalized_id + FROM "user_devices" + WHERE "devices" ? 'credentialID' + AND "devices"->>'credentialID' IS NOT NULL +) +UPDATE "user_devices" ud +SET + "credentialId" = n.normalized_id, + "devices" = jsonb_set(ud."devices", '{credentialID}', to_jsonb(n.normalized_id)), + "lastChangedDateTime" = now() +FROM normalized n +WHERE ud."id" = n."id" + AND ( + ud."credentialId" IS DISTINCT FROM n.normalized_id + OR ud."devices"->>'credentialID' IS DISTINCT FROM n.normalized_id + ); + +COMMIT; From c87576d92eb8553398797d492c3d35afb154802c Mon Sep 17 00:00:00 2001 From: Ajay Jadhav Date: Sun, 23 Aug 2026 01:11:12 +0530 Subject: [PATCH 58/58] fix(security): resolve SonarCloud code smells across auth and migration Address all open SonarCloud issues on PR #1706: - plsql:S1192 (critical): collapse the backfill migration's collision preflight into an exception handler around the single atomic UPDATE, removing the duplicated 'credentialID' literals and the redundant jsonb '?' key check. The statement-level atomicity plus the aborted transaction preserve the fail-safe rollback behavior. - typescript:S8786 (major): remove the super-linear backtracking regexes: validate the passkey credential charset after stripping base64url padding, strip the padding with a single backward scan, and trim trailing slashes in jwt-issuer.util without a regex. - typescript:S3358 (major): extract the nested ternary selecting trusted issuers into if statements. - typescript:S7786 (minor): throw TypeError instead of Error for the non-string JWT issuer type check. - typescript:S2486 (minor): log caught JWKS resolution exceptions in both JWT strategies before mapping them to Unauthorized responses. Verified: backfill migration smoke-tested on Postgres 16 (legacy rows, renamed rows, idempotent rerun, collision rollback); affected jest suites pass (4 suites, 21 tests). Signed-off-by: Ajay Jadhav --- apps/api-gateway/src/authz/jwt-issuer.util.ts | 24 +++++--- apps/api-gateway/src/authz/jwt.strategy.ts | 1 + .../src/authz/mobile-jwt.strategy.ts | 5 +- apps/user/src/fido/fido.service.ts | 12 +++- .../migration.sql | 56 +++++++------------ 5 files changed, 51 insertions(+), 47 deletions(-) diff --git a/apps/api-gateway/src/authz/jwt-issuer.util.ts b/apps/api-gateway/src/authz/jwt-issuer.util.ts index 533ba46d4..df0359f42 100644 --- a/apps/api-gateway/src/authz/jwt-issuer.util.ts +++ b/apps/api-gateway/src/authz/jwt-issuer.util.ts @@ -1,6 +1,13 @@ import { CommonConstants } from '@credebl/common/common.constant'; -const trimTrailingSlashes = (value: string): string => value.trim().replace(/\/+$/, ''); +const trimTrailingSlashes = (value: string): string => { + const trimmedValue = value.trim(); + let endIndex = trimmedValue.length; + while (0 < endIndex && '/' === trimmedValue.charAt(endIndex - 1)) { + endIndex -= 1; + } + return trimmedValue.slice(0, endIndex); +}; const validateIssuer = (value: string): string => { const issuer = trimTrailingSlashes(value); @@ -25,11 +32,10 @@ export const getTrustedJwtIssuers = (env: NodeJS.ProcessEnv = process.env): stri .map((issuer) => issuer.trim()) .filter(Boolean); - const issuers = configuredIssuers?.length - ? configuredIssuers - : env.KEYCLOAK_DOMAIN && env.KEYCLOAK_REALM - ? [`${trimTrailingSlashes(env.KEYCLOAK_DOMAIN)}/realms/${env.KEYCLOAK_REALM.trim()}`] - : []; + let issuers = [...(configuredIssuers ?? [])]; + if (0 === issuers.length && env.KEYCLOAK_DOMAIN && env.KEYCLOAK_REALM) { + issuers = [`${trimTrailingSlashes(env.KEYCLOAK_DOMAIN)}/realms/${env.KEYCLOAK_REALM.trim()}`]; + } if (0 === issuers.length) { throw new Error('JWT_TRUSTED_ISSUERS or both KEYCLOAK_DOMAIN and KEYCLOAK_REALM must be configured'); @@ -38,11 +44,13 @@ export const getTrustedJwtIssuers = (env: NodeJS.ProcessEnv = process.env): stri return [...new Set(issuers.map(validateIssuer))]; }; -export const getTrustedJwtIssuerVariants = (trustedIssuers: string[]): string[] => [...new Set(trustedIssuers.flatMap((issuer) => [issuer, `${issuer}/`]))]; +export const getTrustedJwtIssuerVariants = (trustedIssuers: string[]): string[] => [ + ...new Set(trustedIssuers.flatMap((issuer) => [issuer, `${issuer}/`])) +]; export const getTrustedJwksUri = (issuer: unknown, trustedIssuers: string[]): string => { if ('string' !== typeof issuer) { - throw new Error('JWT issuer is missing'); + throw new TypeError('JWT issuer is missing'); } const normalizedIssuer = trimTrailingSlashes(issuer); diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts index fdb185fc4..89d5867da 100644 --- a/apps/api-gateway/src/authz/jwt.strategy.ts +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -49,6 +49,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { const secretprovider = passportJwtSecret(jwtOptions); secretprovider(request, jwtToken, (err, data) => done(err, data)); } catch (error) { + this.logger.error(`Error resolving JWKS secret:::${error}`); return done(new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken), null); } }, diff --git a/apps/api-gateway/src/authz/mobile-jwt.strategy.ts b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts index 3f28c3358..0b3efd607 100644 --- a/apps/api-gateway/src/authz/mobile-jwt.strategy.ts +++ b/apps/api-gateway/src/authz/mobile-jwt.strategy.ts @@ -2,7 +2,7 @@ import * as dotenv from 'dotenv'; import * as jwt from 'jsonwebtoken'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { passportJwtSecret } from 'jwks-rsa'; @@ -17,6 +17,8 @@ interface MobileJwtPayload { @Injectable() export class MobileJwtStrategy extends PassportStrategy(Strategy, 'mobile-jwt') { + private readonly logger = new Logger('MobileJwt Strategy'); + constructor() { const trustedIssuers = getTrustedJwtIssuers(); const trustedIssuerVariants = getTrustedJwtIssuerVariants(trustedIssuers); @@ -34,6 +36,7 @@ export class MobileJwtStrategy extends PassportStrategy(Strategy, 'mobile-jwt') const secretprovider = passportJwtSecret(jwtOptions); secretprovider(request, jwtToken, (err, data) => done(err, data)); } catch (error) { + this.logger.error(`Error resolving JWKS secret:::${error}`); return done(new UnauthorizedException('Authorization header contains an invalid token'), null); } }, diff --git a/apps/user/src/fido/fido.service.ts b/apps/user/src/fido/fido.service.ts index 3d2fede8b..5ace79224 100644 --- a/apps/user/src/fido/fido.service.ts +++ b/apps/user/src/fido/fido.service.ts @@ -276,11 +276,19 @@ export class FidoService { } private normalizeCredentialId(credentialId: string): string { - if ('string' !== typeof credentialId || !/^[A-Za-z0-9+/_-]+={0,2}$/.test(credentialId)) { + if ('string' !== typeof credentialId) { + throw new BadRequestException('Invalid passkey credential ID'); + } + + let endIndex = credentialId.length; + while (0 < endIndex && '=' === credentialId.charAt(endIndex - 1)) { + endIndex -= 1; + } + const unpaddedCredentialId = credentialId.slice(0, endIndex); + if (!/^[A-Za-z0-9+/_-]+$/.test(unpaddedCredentialId)) { throw new BadRequestException('Invalid passkey credential ID'); } - const unpaddedCredentialId = credentialId.replace(/=+$/, ''); if (1 === unpaddedCredentialId.length % 4) { throw new BadRequestException('Invalid passkey credential ID'); } diff --git a/libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql b/libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql index bdf26a780..097b8a57b 100644 --- a/libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql +++ b/libs/prisma-service/prisma/migrations/20260822000000_backfill_user_devices_credential_id/migration.sql @@ -7,49 +7,33 @@ -- Legacy JSON values were stored padding-stripped but not URL-safe encoded. They are -- normalized to unpadded base64url (strip '=' padding, '+' -> '-', '/' -> '_') to match -- what FidoService.normalizeCredentialId produces before every lookup. +-- +-- The UPDATE below is a single statement, so it is atomic: if two devices collide on the +-- same normalized credential ID (the column has a unique constraint), the exception +-- handler replaces the constraint error with an actionable message and nothing is written. BEGIN; --- The credentialId column has a unique constraint. Fail with a clear message instead of --- letting the UPDATE hit a constraint violation mid-migration. DO $$ -DECLARE - collision_count int; BEGIN - SELECT COUNT(*) - INTO collision_count - FROM ( - SELECT translate(regexp_replace("devices"->>'credentialID', '=+$', ''), '+/', '-_') AS normalized_id + WITH normalized AS ( + SELECT + "id", + translate(regexp_replace("devices"->>'credentialID', '=+$', ''), '+/', '-_') AS normalized_id FROM "user_devices" - WHERE "devices" ? 'credentialID' - AND "devices"->>'credentialID' IS NOT NULL - GROUP BY 1 - HAVING COUNT(*) > 1 - ) collisions; + WHERE "devices"->>'credentialID' IS NOT NULL + ) + UPDATE "user_devices" ud + SET + "credentialId" = n.normalized_id, + "devices" = jsonb_set(ud."devices", '{credentialID}', to_jsonb(n.normalized_id)), + "lastChangedDateTime" = now() + FROM normalized n + WHERE ud."id" = n."id"; - IF collision_count > 0 THEN - RAISE EXCEPTION 'Cannot backfill user_devices.credentialId: % device(s) collide on the same credential ID after normalization; resolve manually first', collision_count; - END IF; +EXCEPTION + WHEN unique_violation THEN + RAISE EXCEPTION 'Cannot backfill user_devices.credentialId: devices collide on the same credential ID after normalization; resolve manually first'; END $$; -WITH normalized AS ( - SELECT - "id", - translate(regexp_replace("devices"->>'credentialID', '=+$', ''), '+/', '-_') AS normalized_id - FROM "user_devices" - WHERE "devices" ? 'credentialID' - AND "devices"->>'credentialID' IS NOT NULL -) -UPDATE "user_devices" ud -SET - "credentialId" = n.normalized_id, - "devices" = jsonb_set(ud."devices", '{credentialID}', to_jsonb(n.normalized_id)), - "lastChangedDateTime" = now() -FROM normalized n -WHERE ud."id" = n."id" - AND ( - ud."credentialId" IS DISTINCT FROM n.normalized_id - OR ud."devices"->>'credentialID' IS DISTINCT FROM n.normalized_id - ); - COMMIT;