From ed7c167f15428b8e35ea49c6b0032f08cd06e110 Mon Sep 17 00:00:00 2001 From: ramirez Date: Thu, 26 Feb 2026 16:27:37 +0900 Subject: [PATCH] feat: integrate OpenBao vault for secret management in BFF Replace fragile .env backup/restore with Vault-based secret injection. Secrets are preloaded via --import hook before NestJS modules evaluate, with a 30s refresh loop and event-driven cache invalidation for services that read secrets at init (JWT, CSRF, WHMCS). Co-Authored-By: Claude Opus 4.6 --- apps/bff/package.json | 1 + apps/bff/src/app.module.ts | 25 ++- apps/bff/src/core/config/env.validation.ts | 6 + .../core/security/services/csrf.service.ts | 16 +- apps/bff/src/core/vault/index.ts | 11 ++ apps/bff/src/core/vault/vault.client.ts | 57 +++++++ apps/bff/src/core/vault/vault.constants.ts | 2 + apps/bff/src/core/vault/vault.module.ts | 23 +++ apps/bff/src/core/vault/vault.service.ts | 157 ++++++++++++++++++ apps/bff/src/core/vault/vault.types.ts | 44 +++++ .../connection/config/whmcs-config.service.ts | 16 +- .../auth/infra/token/jose-jwt.service.ts | 29 +++- apps/bff/src/vault-preload.ts | 64 +++++++ pnpm-lock.yaml | 26 +++ 14 files changed, 470 insertions(+), 7 deletions(-) create mode 100644 apps/bff/src/core/vault/index.ts create mode 100644 apps/bff/src/core/vault/vault.client.ts create mode 100644 apps/bff/src/core/vault/vault.constants.ts create mode 100644 apps/bff/src/core/vault/vault.module.ts create mode 100644 apps/bff/src/core/vault/vault.service.ts create mode 100644 apps/bff/src/core/vault/vault.types.ts create mode 100644 apps/bff/src/vault-preload.ts diff --git a/apps/bff/package.json b/apps/bff/package.json index ef81ad9a..119f48ee 100644 --- a/apps/bff/package.json +++ b/apps/bff/package.json @@ -37,6 +37,7 @@ "@nestjs/common": "^11.1.12", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.1.12", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/platform-express": "^11.1.12", "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^11.2.5", diff --git a/apps/bff/src/app.module.ts b/apps/bff/src/app.module.ts index c902118c..277590e9 100644 --- a/apps/bff/src/app.module.ts +++ b/apps/bff/src/app.module.ts @@ -1,13 +1,17 @@ -import { Module } from "@nestjs/common"; +import { Module, type DynamicModule, type Type } from "@nestjs/common"; import { APP_INTERCEPTOR, APP_PIPE, RouterModule } from "@nestjs/core"; import { ConfigModule } from "@nestjs/config"; import { ScheduleModule } from "@nestjs/schedule"; +import { EventEmitterModule } from "@nestjs/event-emitter"; import { ZodSerializerInterceptor, ZodValidationPipe } from "nestjs-zod"; // Configuration import { appConfig } from "@bff/core/config/app.config.js"; import { apiRoutes } from "@bff/core/config/router.config.js"; +// Vault (conditional — only when VAULT_ADDR is set) +import { VaultModule } from "@bff/core/vault/index.js"; + // Core Modules import { LoggingModule } from "@bff/core/logging/logging.module.js"; import { SecurityModule } from "@bff/core/security/security.module.js"; @@ -46,6 +50,22 @@ import { AddressModule } from "@bff/modules/address/address.module.js"; // System Modules import { HealthModule } from "@bff/modules/health/health.module.js"; +function conditionalVaultImports(): Array { + const addr = process.env["VAULT_ADDR"]; + const roleId = process.env["VAULT_ROLE_ID"]; + const secretId = process.env["VAULT_SECRET_ID"]; + const secretPath = process.env["VAULT_SECRET_PATH"] ?? "kv/data/services/portal-backend"; + + if (!addr || !roleId || !secretId) { + return []; + } + + return [ + EventEmitterModule.forRoot(), + VaultModule.forRoot({ address: addr, roleId, secretId, secretPath }), + ]; +} + /** * Main application module * @@ -64,6 +84,9 @@ import { HealthModule } from "@bff/modules/health/health.module.js"; ConfigModule.forRoot(appConfig), ScheduleModule.forRoot(), + // === VAULT (conditional) === + ...conditionalVaultImports(), + // === INFRASTRUCTURE === LoggingModule, SecurityModule, diff --git a/apps/bff/src/core/config/env.validation.ts b/apps/bff/src/core/config/env.validation.ts index 8f6fd014..f082a006 100644 --- a/apps/bff/src/core/config/env.validation.ts +++ b/apps/bff/src/core/config/env.validation.ts @@ -1,6 +1,12 @@ import { z } from "zod"; export const envSchema = z.object({ + // Vault integration (optional — set in systemd EnvironmentFile, not .env) + VAULT_ADDR: z.string().url().optional(), + VAULT_ROLE_ID: z.string().optional(), + VAULT_SECRET_ID: z.string().optional(), + VAULT_SECRET_PATH: z.string().default("kv/data/services/portal-backend"), + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), BFF_PORT: z.coerce.number().int().positive().max(65535).default(4000), LOG_LEVEL: z.enum(["error", "warn", "info", "debug", "trace"]).default("info"), diff --git a/apps/bff/src/core/security/services/csrf.service.ts b/apps/bff/src/core/security/services/csrf.service.ts index bdc731e4..c8d70ce5 100644 --- a/apps/bff/src/core/security/services/csrf.service.ts +++ b/apps/bff/src/core/security/services/csrf.service.ts @@ -1,7 +1,9 @@ import { Injectable, Inject } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import { OnEvent } from "@nestjs/event-emitter"; import { Logger } from "nestjs-pino"; import * as crypto from "crypto"; +import { VAULT_REFRESH_EVENT } from "@bff/core/vault/vault.constants.js"; export interface CsrfTokenData { token: string; @@ -32,7 +34,7 @@ export interface CsrfTokenStats { export class CsrfService { private readonly tokenExpiry: number; - private readonly secretKey: string; + private secretKey: string; private readonly cookieName: string; @@ -154,6 +156,18 @@ export class CsrfService { }; } + @OnEvent(VAULT_REFRESH_EVENT) + handleVaultRefresh({ changedKeys }: { changedKeys: string[] }): void { + if (!changedKeys.includes("CSRF_SECRET_KEY")) return; + + const newKey = + this.configService.get("CSRF_SECRET_KEY") ?? process.env["CSRF_SECRET_KEY"]; + if (newKey) { + this.secretKey = newKey; + this.logger.log("CSRF secret key updated from Vault"); + } + } + private generateSecret(): string { return crypto.randomBytes(32).toString("base64url"); } diff --git a/apps/bff/src/core/vault/index.ts b/apps/bff/src/core/vault/index.ts new file mode 100644 index 00000000..f6cdb50a --- /dev/null +++ b/apps/bff/src/core/vault/index.ts @@ -0,0 +1,11 @@ +export { VaultModule } from "./vault.module.js"; +export { VaultService } from "./vault.service.js"; +export { VaultClient } from "./vault.client.js"; +export { VAULT_MODULE_OPTIONS, VAULT_REFRESH_EVENT } from "./vault.constants.js"; +export type { + VaultConfig, + VaultTokenState, + VaultAuthResponse, + VaultSecretResponse, + VaultRenewResponse, +} from "./vault.types.js"; diff --git a/apps/bff/src/core/vault/vault.client.ts b/apps/bff/src/core/vault/vault.client.ts new file mode 100644 index 00000000..4b1d4e3a --- /dev/null +++ b/apps/bff/src/core/vault/vault.client.ts @@ -0,0 +1,57 @@ +import type { VaultAuthResponse, VaultSecretResponse, VaultRenewResponse } from "./vault.types.js"; + +const DEFAULT_TIMEOUT_MS = 5000; + +export class VaultClient { + constructor( + private readonly address: string, + private readonly timeoutMs = DEFAULT_TIMEOUT_MS + ) {} + + async login(roleId: string, secretId: string): Promise { + return this.request("/v1/auth/approle/login", { + method: "POST", + body: JSON.stringify({ role_id: roleId, secret_id: secretId }), + }); + } + + async readSecret(path: string, token: string): Promise { + return this.request(`/v1/${path}`, { + method: "GET", + headers: { "X-Vault-Token": token }, + }); + } + + async renewToken(token: string): Promise { + return this.request("/v1/auth/token/renew-self", { + method: "POST", + headers: { "X-Vault-Token": token }, + }); + } + + private async request(path: string, init: RequestInit): Promise { + const url = `${this.address}${path}`; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.timeoutMs); + + try { + const response = await fetch(url, { + ...init, + signal: controller.signal, + headers: { + "Content-Type": "application/json", + ...init.headers, + }, + }); + + if (!response.ok) { + const body = await response.text().catch(() => ""); + throw new Error(`Vault ${init.method} ${path} failed: ${response.status} ${body}`); + } + + return (await response.json()) as T; + } finally { + clearTimeout(timeout); + } + } +} diff --git a/apps/bff/src/core/vault/vault.constants.ts b/apps/bff/src/core/vault/vault.constants.ts new file mode 100644 index 00000000..757f1f60 --- /dev/null +++ b/apps/bff/src/core/vault/vault.constants.ts @@ -0,0 +1,2 @@ +export const VAULT_MODULE_OPTIONS = Symbol("VAULT_MODULE_OPTIONS"); +export const VAULT_REFRESH_EVENT = "vault.secrets.refreshed"; diff --git a/apps/bff/src/core/vault/vault.module.ts b/apps/bff/src/core/vault/vault.module.ts new file mode 100644 index 00000000..036e6c1f --- /dev/null +++ b/apps/bff/src/core/vault/vault.module.ts @@ -0,0 +1,23 @@ +import { type DynamicModule, Global, Module } from "@nestjs/common"; + +import { VAULT_MODULE_OPTIONS } from "./vault.constants.js"; +import { VaultService } from "./vault.service.js"; +import type { VaultConfig } from "./vault.types.js"; + +@Global() +@Module({}) +export class VaultModule { + static forRoot(config: VaultConfig): DynamicModule { + return { + module: VaultModule, + providers: [ + { + provide: VAULT_MODULE_OPTIONS, + useValue: config, + }, + VaultService, + ], + exports: [VaultService], + }; + } +} diff --git a/apps/bff/src/core/vault/vault.service.ts b/apps/bff/src/core/vault/vault.service.ts new file mode 100644 index 00000000..98a8104d --- /dev/null +++ b/apps/bff/src/core/vault/vault.service.ts @@ -0,0 +1,157 @@ +import { + Inject, + Injectable, + Logger, + type OnModuleInit, + type OnModuleDestroy, +} from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { EventEmitter2 } from "@nestjs/event-emitter"; + +import { VAULT_MODULE_OPTIONS, VAULT_REFRESH_EVENT } from "./vault.constants.js"; +import { VaultClient } from "./vault.client.js"; +import type { VaultConfig, VaultTokenState } from "./vault.types.js"; + +const DEFAULT_REFRESH_INTERVAL_MS = 30_000; +const DEFAULT_TOKEN_RENEW_THRESHOLD = 0.75; + +@Injectable() +export class VaultService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(VaultService.name); + private readonly client: VaultClient; + private readonly refreshIntervalMs: number; + private readonly tokenRenewThreshold: number; + + private tokenState: VaultTokenState | null = null; + private lastKvVersion: number | null = null; + private refreshTimer: ReturnType | null = null; + private renewTimer: ReturnType | null = null; + + constructor( + @Inject(VAULT_MODULE_OPTIONS) private readonly config: VaultConfig, + private readonly configService: ConfigService, + private readonly eventEmitter: EventEmitter2 + ) { + this.client = new VaultClient(config.address, config.requestTimeoutMs); + this.refreshIntervalMs = config.refreshIntervalMs ?? DEFAULT_REFRESH_INTERVAL_MS; + this.tokenRenewThreshold = config.tokenRenewThreshold ?? DEFAULT_TOKEN_RENEW_THRESHOLD; + } + + async onModuleInit(): Promise { + await this.authenticate(); + await this.fetchAndApplySecrets(); + this.startRefreshLoop(); + this.scheduleTokenRenewal(); + this.logger.log("Vault secrets loaded and refresh loop started"); + } + + onModuleDestroy(): void { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + if (this.renewTimer) { + clearTimeout(this.renewTimer); + this.renewTimer = null; + } + } + + private async authenticate(): Promise { + const response = await this.client.login(this.config.roleId, this.config.secretId); + this.tokenState = { + clientToken: response.auth.client_token, + leaseDuration: response.auth.lease_duration, + authenticatedAt: Date.now(), + }; + this.logger.log(`Authenticated with Vault (TTL: ${response.auth.lease_duration}s)`); + } + + private async fetchAndApplySecrets(): Promise { + if (!this.tokenState) { + throw new Error("Vault: not authenticated"); + } + + const response = await this.client.readSecret( + this.config.secretPath, + this.tokenState.clientToken + ); + + const currentVersion = response.data.metadata.version; + + if (this.lastKvVersion !== null && currentVersion === this.lastKvVersion) { + return false; + } + + const secrets = response.data.data; + const changedKeys: string[] = []; + + for (const [key, value] of Object.entries(secrets)) { + const current = this.configService.get(key); + if (current !== value) { + changedKeys.push(key); + // Update process.env so ConfigService picks up the new value + process.env[key] = value; + } + } + + this.lastKvVersion = currentVersion; + + if (changedKeys.length > 0) { + this.logger.log(`Vault secrets updated (v${currentVersion}): ${changedKeys.join(", ")}`); + this.eventEmitter.emit(VAULT_REFRESH_EVENT, { changedKeys }); + return true; + } + + return false; + } + + private startRefreshLoop(): void { + this.refreshTimer = setInterval(() => { + void this.refreshSecrets(); + }, this.refreshIntervalMs); + } + + private async refreshSecrets(): Promise { + try { + await this.fetchAndApplySecrets(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn(`Vault refresh failed, keeping cached values: ${message}`); + } + } + + private scheduleTokenRenewal(): void { + if (!this.tokenState) return; + + const renewAfterMs = this.tokenState.leaseDuration * 1000 * this.tokenRenewThreshold; + + this.renewTimer = setTimeout(() => { + void this.renewToken(); + }, renewAfterMs); + } + + private async renewToken(): Promise { + if (!this.tokenState) return; + + try { + const response = await this.client.renewToken(this.tokenState.clientToken); + this.tokenState = { + clientToken: response.auth.client_token, + leaseDuration: response.auth.lease_duration, + authenticatedAt: Date.now(), + }; + this.logger.log(`Vault token renewed (TTL: ${response.auth.lease_duration}s)`); + this.scheduleTokenRenewal(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn(`Vault token renewal failed, re-authenticating: ${message}`); + try { + await this.authenticate(); + this.scheduleTokenRenewal(); + } catch (authError) { + const authMessage = authError instanceof Error ? authError.message : String(authError); + this.logger.error(`Vault re-authentication failed: ${authMessage}`); + } + } + } +} diff --git a/apps/bff/src/core/vault/vault.types.ts b/apps/bff/src/core/vault/vault.types.ts new file mode 100644 index 00000000..ead3875e --- /dev/null +++ b/apps/bff/src/core/vault/vault.types.ts @@ -0,0 +1,44 @@ +export interface VaultConfig { + address: string; + roleId: string; + secretId: string; + secretPath: string; + refreshIntervalMs?: number; + tokenRenewThreshold?: number; + requestTimeoutMs?: number; +} + +export interface VaultTokenState { + clientToken: string; + leaseDuration: number; + authenticatedAt: number; +} + +export interface VaultAuthResponse { + auth: { + client_token: string; + lease_duration: number; + renewable: boolean; + policies: string[]; + }; +} + +export interface VaultSecretResponse { + data: { + data: Record; + metadata: { + version: number; + created_time: string; + deletion_time: string; + destroyed: boolean; + }; + }; +} + +export interface VaultRenewResponse { + auth: { + client_token: string; + lease_duration: number; + renewable: boolean; + }; +} diff --git a/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts b/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts index 817d56cc..ca0f4ae4 100644 --- a/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts +++ b/apps/bff/src/integrations/whmcs/connection/config/whmcs-config.service.ts @@ -1,6 +1,8 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import { OnEvent } from "@nestjs/event-emitter"; import type { WhmcsApiConfig } from "../types/connection.types.js"; +import { VAULT_REFRESH_EVENT } from "@bff/core/vault/vault.constants.js"; /** * Service for managing WHMCS API configuration @@ -8,7 +10,8 @@ import type { WhmcsApiConfig } from "../types/connection.types.js"; */ @Injectable() export class WhmcsConfigService { - private readonly config: WhmcsApiConfig; + private readonly logger = new Logger(WhmcsConfigService.name); + private config: WhmcsApiConfig; constructor(private readonly configService: ConfigService) { this.config = this.loadConfiguration(); @@ -44,6 +47,15 @@ export class WhmcsConfigService { } } + @OnEvent(VAULT_REFRESH_EVENT) + handleVaultRefresh({ changedKeys }: { changedKeys: string[] }): void { + const whmcsKeys = ["WHMCS_API_IDENTIFIER", "WHMCS_API_SECRET", "WHMCS_BASE_URL"]; + if (!changedKeys.some(k => whmcsKeys.includes(k))) return; + + this.config = this.loadConfiguration(); + this.logger.log("WHMCS config reloaded from Vault"); + } + /** * Load configuration from environment variables */ diff --git a/apps/bff/src/modules/auth/infra/token/jose-jwt.service.ts b/apps/bff/src/modules/auth/infra/token/jose-jwt.service.ts index 9e151769..54323560 100644 --- a/apps/bff/src/modules/auth/infra/token/jose-jwt.service.ts +++ b/apps/bff/src/modules/auth/infra/token/jose-jwt.service.ts @@ -1,16 +1,21 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; +import { OnEvent } from "@nestjs/event-emitter"; import { SignJWT, decodeJwt, jwtVerify, errors, type JWTPayload } from "jose"; import { parseJwtExpiry } from "../../utils/jwt-expiry.util.js"; +import { VAULT_REFRESH_EVENT } from "@bff/core/vault/vault.constants.js"; @Injectable() export class JoseJwtService { - private readonly signingKey: Uint8Array; - private readonly verificationKeys: Uint8Array[]; + private readonly logger = new Logger(JoseJwtService.name); + private signingKey: Uint8Array; + private verificationKeys: Uint8Array[]; private readonly issuer: string | undefined; private readonly audience: string | string[] | undefined; + private readonly configService: ConfigService; constructor(configService: ConfigService) { + this.configService = configService; const secret = configService.get("JWT_SECRET"); if (!secret) { throw new Error("JWT_SECRET is required in environment variables"); @@ -136,4 +141,22 @@ export class JoseJwtService { return null; } } + + @OnEvent(VAULT_REFRESH_EVENT) + handleVaultRefresh({ changedKeys }: { changedKeys: string[] }): void { + if (!changedKeys.includes("JWT_SECRET")) return; + + const secret = this.configService.get("JWT_SECRET") ?? process.env["JWT_SECRET"]; + if (!secret) return; + + this.signingKey = new TextEncoder().encode(secret); + const previousRaw = this.configService.get("JWT_SECRET_PREVIOUS"); + const previousSecrets = this.parsePreviousSecrets(previousRaw).filter(s => s !== secret); + this.verificationKeys = [ + this.signingKey, + ...previousSecrets.map(s => new TextEncoder().encode(s)), + ]; + + this.logger.log("JWT signing/verification keys re-derived from Vault"); + } } diff --git a/apps/bff/src/vault-preload.ts b/apps/bff/src/vault-preload.ts new file mode 100644 index 00000000..965f6307 --- /dev/null +++ b/apps/bff/src/vault-preload.ts @@ -0,0 +1,64 @@ +/** + * Vault secret preloader — runs via `node --import ./dist/vault-preload.js` + * BEFORE the main entry point loads. This ensures process.env is populated + * before any NestJS modules evaluate (ConfigModule.forRoot → Zod validate). + * + * Skipped when VAULT_ADDR is not set (local dev). + */ + +const addr = process.env["VAULT_ADDR"]; +const roleId = process.env["VAULT_ROLE_ID"]; +const secretId = process.env["VAULT_SECRET_ID"]; +const secretPath = process.env["VAULT_SECRET_PATH"] ?? "kv/data/services/portal-backend"; + +if (addr && roleId && secretId) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + try { + // AppRole login + const authRes = await fetch(`${addr}/v1/auth/approle/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ role_id: roleId, secret_id: secretId }), + signal: controller.signal, + }); + + if (!authRes.ok) { + throw new Error(`Vault login failed: ${authRes.status} ${await authRes.text()}`); + } + + const authData = (await authRes.json()) as { auth: { client_token: string } }; + const token = authData.auth.client_token; + + // Read secrets + const secretRes = await fetch(`${addr}/v1/${secretPath}`, { + method: "GET", + headers: { "Content-Type": "application/json", "X-Vault-Token": token }, + signal: controller.signal, + }); + + if (!secretRes.ok) { + throw new Error(`Vault read failed: ${secretRes.status} ${await secretRes.text()}`); + } + + const secretData = (await secretRes.json()) as { + data: { data: Record; metadata: { version: number } }; + }; + + const secrets = secretData.data.data; + for (const [key, value] of Object.entries(secrets)) { + process.env[key] = value; + } + + console.warn( + `[Vault] Secrets preloaded (${Object.keys(secrets).length} keys, v${secretData.data.metadata.version})` + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`[Vault] FATAL: Failed to preload secrets: ${message}`); + process.exit(1); + } finally { + clearTimeout(timeout); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47cab8da..f621e275 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,9 @@ importers: "@nestjs/core": specifier: ^11.1.12 version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + "@nestjs/event-emitter": + specifier: ^3.0.1 + version: 3.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) "@nestjs/platform-express": specifier: ^11.1.12 version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) @@ -1880,6 +1883,15 @@ packages: "@nestjs/websockets": optional: true + "@nestjs/event-emitter@3.0.1": + resolution: + { + integrity: sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==, + } + peerDependencies: + "@nestjs/common": ^10.0.0 || ^11.0.0 + "@nestjs/core": ^10.0.0 || ^11.0.0 + "@nestjs/mapped-types@2.1.0": resolution: { @@ -4829,6 +4841,12 @@ packages: } engines: { node: ">= 0.6" } + eventemitter2@6.4.9: + resolution: + { + integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==, + } + eventemitter3@5.0.4: resolution: { @@ -9444,6 +9462,12 @@ snapshots: optionalDependencies: "@nestjs/platform-express": 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12) + "@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)": + dependencies: + "@nestjs/common": 11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + "@nestjs/core": 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) + eventemitter2: 6.4.9 + "@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)": dependencies: "@nestjs/common": 11.1.12(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -11240,6 +11264,8 @@ snapshots: etag@1.8.1: {} + eventemitter2@6.4.9: {} + eventemitter3@5.0.4: {} events-universal@1.0.1: