@vritti/api-sdk 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
3
3
 
4
4
  // src/auth/auth-config.module.ts
5
5
  import { Global as Global2, Module as Module2 } from "@nestjs/common";
6
- import { ConfigModule, ConfigService as ConfigService2 } from "@nestjs/config";
6
+ import { ConfigModule, ConfigService as ConfigService3 } from "@nestjs/config";
7
7
  import { APP_GUARD, Reflector as Reflector2 } from "@nestjs/core";
8
8
  import { JwtModule } from "@nestjs/jwt";
9
9
 
@@ -27,8 +27,7 @@ var defaultConfig = {
27
27
  jwt: {
28
28
  accessTokenExpiry: "15m",
29
29
  refreshTokenExpiry: "30d",
30
- onboardingTokenExpiry: "24h",
31
- validateTokenBinding: true
30
+ onboardingTokenExpiry: "24h"
32
31
  },
33
32
  guard: {
34
33
  tenantHeaderName: "x-tenant-id",
@@ -119,11 +118,7 @@ var RequestService = class {
119
118
  constructor(request) {
120
119
  this.request = request;
121
120
  }
122
- /**
123
- * Extract tenant identifier from request headers
124
- * Priority: x-tenant-id > x-subdomain
125
- * @returns Tenant identifier or null if not found
126
- */
121
+ // Extracts tenant identifier from x-tenant-id or x-subdomain request header
127
122
  getTenantIdentifier() {
128
123
  const getHeader = /* @__PURE__ */ __name((key) => {
129
124
  const value = this.request.headers?.[key];
@@ -131,11 +126,7 @@ var RequestService = class {
131
126
  }, "getHeader");
132
127
  return getHeader("x-tenant-id") || getHeader("x-subdomain") || null;
133
128
  }
134
- /**
135
- * Extract access token from Authorization header
136
- * Format: "Bearer <token>"
137
- * @returns Access token or null if not found
138
- */
129
+ // Extracts the bearer access token from the Authorization header
139
130
  getAccessToken() {
140
131
  const authHeader = this.request.headers?.authorization;
141
132
  if (!authHeader) {
@@ -144,11 +135,7 @@ var RequestService = class {
144
135
  const [type, token] = authHeader.split(" ") ?? [];
145
136
  return type === "Bearer" && token ? token : null;
146
137
  }
147
- /**
148
- * Extract refresh token from httpOnly cookie
149
- * Cookie name is configurable via api-sdk config
150
- * @returns Refresh token or null if not found
151
- */
138
+ // Extracts the refresh token from the configured httpOnly cookie
152
139
  getRefreshToken() {
153
140
  try {
154
141
  const cookies = this.request.cookies;
@@ -164,18 +151,11 @@ var RequestService = class {
164
151
  return null;
165
152
  }
166
153
  }
167
- /**
168
- * Get a specific header value
169
- * @param key Header key
170
- * @returns Header value (string, array, or undefined)
171
- */
154
+ // Returns the value of a specific request header by key
172
155
  getHeader(key) {
173
156
  return this.request.headers?.[key];
174
157
  }
175
- /**
176
- * Get all headers
177
- * @returns Record of all headers
178
- */
158
+ // Returns all request headers
179
159
  getAllHeaders() {
180
160
  return this.request.headers || {};
181
161
  }
@@ -217,252 +197,21 @@ RequestModule = _ts_decorate2([
217
197
  ], RequestModule);
218
198
 
219
199
  // src/auth/guards/vritti-auth.guard.ts
220
- import { ForbiddenException, Injectable as Injectable3, Logger as Logger3, Scope as Scope2, UnauthorizedException } from "@nestjs/common";
200
+ import { ForbiddenException, Injectable as Injectable2, Logger as Logger2, Scope as Scope2, UnauthorizedException } from "@nestjs/common";
201
+ import { SSE_METADATA } from "@nestjs/common/constants";
221
202
  import { ConfigService } from "@nestjs/config";
222
203
  import { Reflector } from "@nestjs/core";
223
204
  import { JwtService } from "@nestjs/jwt";
224
205
 
225
- // src/auth/decorators/skip-csrf.decorator.ts
206
+ // src/auth/decorators/reset.decorator.ts
226
207
  import { SetMetadata } from "@nestjs/common";
227
- var SKIP_CSRF_KEY = "skipCsrf";
228
- var SkipCsrf = /* @__PURE__ */ __name(() => SetMetadata(SKIP_CSRF_KEY, true), "SkipCsrf");
229
-
230
- // src/database/services/primary-database.service.ts
231
- import { Inject as Inject2, Injectable as Injectable2, InternalServerErrorException, Logger as Logger2 } from "@nestjs/common";
232
- import { eq, or } from "drizzle-orm";
233
- import { drizzle } from "drizzle-orm/node-postgres";
234
- import { Pool } from "pg";
235
-
236
- // src/database/constants.ts
237
- var DATABASE_MODULE_OPTIONS = Symbol("DATABASE_MODULE_OPTIONS");
208
+ var RESET_KEY = "isReset";
209
+ var Reset = /* @__PURE__ */ __name(() => SetMetadata(RESET_KEY, true), "Reset");
238
210
 
239
- // src/database/services/primary-database.service.ts
240
- function _ts_decorate3(decorators, target, key, desc) {
241
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
242
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
243
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
244
- return c > 3 && r && Object.defineProperty(target, key, r), r;
245
- }
246
- __name(_ts_decorate3, "_ts_decorate");
247
- function _ts_metadata2(k, v) {
248
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
249
- }
250
- __name(_ts_metadata2, "_ts_metadata");
251
- function _ts_param2(paramIndex, decorator) {
252
- return function(target, key) {
253
- decorator(target, key, paramIndex);
254
- };
255
- }
256
- __name(_ts_param2, "_ts_param");
257
- var PrimaryDatabaseService = class _PrimaryDatabaseService {
258
- static {
259
- __name(this, "PrimaryDatabaseService");
260
- }
261
- options;
262
- logger = new Logger2(_PrimaryDatabaseService.name);
263
- /** PostgreSQL connection pool */
264
- pool = null;
265
- /** Drizzle database instance */
266
- db = null;
267
- /** In-memory cache: Map<tenantIdentifier, TenantConfig> */
268
- tenantConfigCache = /* @__PURE__ */ new Map();
269
- /** Cache TTL in milliseconds */
270
- cacheTTL;
271
- constructor(options) {
272
- this.options = options;
273
- this.cacheTTL = options.connectionCacheTTL || 3e5;
274
- }
275
- async onModuleInit() {
276
- if (this.options.primaryDb) {
277
- await this.initializeDrizzleClient();
278
- }
279
- }
280
- /**
281
- * Initialize connection to primary database using Drizzle
282
- */
283
- async initializeDrizzleClient() {
284
- try {
285
- const databaseUrl = this.buildPrimaryDbUrl();
286
- this.pool = new Pool({
287
- connectionString: databaseUrl,
288
- max: this.options.maxConnections || 10
289
- });
290
- this.logger.debug(`Schema keys passed to drizzle: [${Object.keys(this.options.drizzleSchema || {}).join(", ")}]`);
291
- this.logger.debug(`Relations keys passed to drizzle: [${Object.keys(this.options.drizzleRelations || {}).join(", ")}]`);
292
- this.db = drizzle({
293
- client: this.pool,
294
- schema: this.options.drizzleSchema,
295
- relations: this.options.drizzleRelations
296
- });
297
- this.logger.debug(`Drizzle query keys after init: [${Object.keys(this.db.query || {}).join(", ")}]`);
298
- await this.pool.query("SELECT 1");
299
- this.logger.log("Connected to primary database (tenant registry)");
300
- } catch (error) {
301
- this.logger.error("Failed to connect to primary database", error);
302
- throw new InternalServerErrorException("Failed to initialize tenant registry");
303
- }
304
- }
305
- /**
306
- * Build connection URL from primary database properties
307
- */
308
- buildPrimaryDbUrl() {
309
- if (!this.options.primaryDb) {
310
- throw new Error("Primary database configuration not provided");
311
- }
312
- const { host, port = 5432, username, password, database, schema = "public", sslMode = "require" } = this.options.primaryDb;
313
- let url = `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
314
- const params = new URLSearchParams();
315
- if (schema) {
316
- params.set("schema", schema);
317
- }
318
- params.set("sslmode", sslMode);
319
- const queryString = params.toString();
320
- if (queryString) {
321
- url += `?${queryString}`;
322
- }
323
- this.logger.debug(`Primary DB connection URL: ${this.maskPassword(url)}`);
324
- return url;
325
- }
326
- /**
327
- * Mask password in connection URL for logging
328
- */
329
- maskPassword(url) {
330
- return url.replace(/:([^@]+)@/, ":****@");
331
- }
332
- /**
333
- * Get tenant configuration by identifier (ID or subdomain)
334
- *
335
- * @param tenantIdentifier Tenant ID or subdomain
336
- * @returns Tenant configuration or null if not found
337
- */
338
- async getTenantInfo(tenantIdentifier) {
339
- const cached = this.tenantConfigCache.get(tenantIdentifier);
340
- if (cached) {
341
- this.logger.debug(`Cache hit for tenant: ${tenantIdentifier}`);
342
- return cached;
343
- }
344
- try {
345
- if (!this.db) {
346
- throw new Error("Primary database client not initialized");
347
- }
348
- this.logger.debug(`Querying primary database for tenant: ${tenantIdentifier}`);
349
- const schema = this.options.drizzleSchema;
350
- const { tenants, tenantDatabaseConfigs } = schema;
351
- const result = await this.db.select().from(tenants).leftJoin(tenantDatabaseConfigs, eq(tenants.id, tenantDatabaseConfigs.tenantId)).where(or(eq(tenants.id, tenantIdentifier), eq(tenants.subdomain, tenantIdentifier))).limit(1);
352
- if (!result.length) {
353
- this.logger.warn(`Tenant not found: ${tenantIdentifier}`);
354
- return null;
355
- }
356
- const row = result[0];
357
- const tenant = row.tenants;
358
- const config = row.tenant_database_configs;
359
- if (tenant.status !== "ACTIVE") {
360
- this.logger.warn(`Tenant not active: ${tenantIdentifier}`);
361
- return null;
362
- }
363
- const info = {
364
- id: tenant.id,
365
- subdomain: tenant.subdomain,
366
- type: tenant.dbType,
367
- status: tenant.status,
368
- // For SHARED tenants: schema name
369
- schemaName: config?.dbSchema || void 0,
370
- // For DEDICATED tenants: database configuration from TenantDatabaseConfig table
371
- databaseName: config?.dbName || void 0,
372
- databaseHost: config?.dbHost || void 0,
373
- databasePort: config?.dbPort || void 0,
374
- databaseUsername: config?.dbUsername ? this.decrypt(config.dbUsername) : void 0,
375
- databasePassword: config?.dbPassword ? this.decrypt(config.dbPassword) : void 0,
376
- databaseSslMode: config?.dbSslMode || void 0,
377
- connectionPoolSize: config?.connectionPoolSize || void 0
378
- };
379
- this.cacheInfo(info);
380
- return info;
381
- } catch (error) {
382
- this.logger.error(`Failed to fetch tenant info: ${tenantIdentifier}`, error);
383
- throw new InternalServerErrorException("Failed to resolve tenant");
384
- }
385
- }
386
- /**
387
- * Cache tenant information with TTL
388
- */
389
- cacheInfo(info) {
390
- this.tenantConfigCache.set(info.id, info);
391
- this.tenantConfigCache.set(info.subdomain, info);
392
- setTimeout(() => {
393
- this.tenantConfigCache.delete(info.id);
394
- this.tenantConfigCache.delete(info.subdomain);
395
- this.logger.debug(`Cache expired for tenant: ${info.subdomain}`);
396
- }, this.cacheTTL);
397
- }
398
- /**
399
- * Clear cached tenant information
400
- *
401
- * Useful when tenant settings are updated and cache needs to be invalidated
402
- *
403
- * @param tenantIdentifier Tenant ID or subdomain
404
- */
405
- clearTenantCache(tenantIdentifier) {
406
- const config = this.tenantConfigCache.get(tenantIdentifier);
407
- if (config) {
408
- this.tenantConfigCache.delete(config.id);
409
- this.tenantConfigCache.delete(config.subdomain);
410
- this.logger.log(`Cleared cache for tenant: ${tenantIdentifier}`);
411
- }
412
- }
413
- /**
414
- * Clear all cached tenant configurations
415
- */
416
- clearAllCaches() {
417
- const size = this.tenantConfigCache.size;
418
- this.tenantConfigCache.clear();
419
- this.logger.log(`Cleared ${size} cached tenant configs`);
420
- }
421
- /**
422
- * Get the Drizzle database instance for the primary database.
423
- * This is a synchronous property that returns the initialized Drizzle client.
424
- *
425
- * @returns Primary database Drizzle instance
426
- * @throws Error if primary database client is not initialized
427
- */
428
- get drizzleClient() {
429
- if (!this.db) {
430
- throw new Error("Primary database client not initialized");
431
- }
432
- return this.db;
433
- }
434
- /**
435
- * Get the Drizzle schema
436
- */
437
- get schema() {
438
- return this.options.drizzleSchema;
439
- }
440
- /**
441
- * Decrypt database credentials
442
- *
443
- * Override this method to implement your encryption strategy
444
- *
445
- * @param encrypted Encrypted value
446
- * @returns Decrypted value
447
- */
448
- decrypt(encrypted) {
449
- return encrypted;
450
- }
451
- async onModuleDestroy() {
452
- if (this.pool) {
453
- await this.pool.end();
454
- this.logger.log("Disconnected from primary database");
455
- }
456
- }
457
- };
458
- PrimaryDatabaseService = _ts_decorate3([
459
- Injectable2(),
460
- _ts_param2(0, Inject2(DATABASE_MODULE_OPTIONS)),
461
- _ts_metadata2("design:type", Function),
462
- _ts_metadata2("design:paramtypes", [
463
- typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions
464
- ])
465
- ], PrimaryDatabaseService);
211
+ // src/auth/decorators/skip-csrf.decorator.ts
212
+ import { SetMetadata as SetMetadata2 } from "@nestjs/common";
213
+ var SKIP_CSRF_KEY = "skipCsrf";
214
+ var SkipCsrf = /* @__PURE__ */ __name(() => SetMetadata2(SKIP_CSRF_KEY, true), "SkipCsrf");
466
215
 
467
216
  // src/auth/utils/token-hash.util.ts
468
217
  import * as crypto from "crypto";
@@ -478,17 +227,17 @@ function verifyTokenHash(token, expectedHash) {
478
227
  __name(verifyTokenHash, "verifyTokenHash");
479
228
 
480
229
  // src/auth/guards/vritti-auth.guard.ts
481
- function _ts_decorate4(decorators, target, key, desc) {
230
+ function _ts_decorate3(decorators, target, key, desc) {
482
231
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
483
232
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
484
233
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
485
234
  return c > 3 && r && Object.defineProperty(target, key, r), r;
486
235
  }
487
- __name(_ts_decorate4, "_ts_decorate");
488
- function _ts_metadata3(k, v) {
236
+ __name(_ts_decorate3, "_ts_decorate");
237
+ function _ts_metadata2(k, v) {
489
238
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
490
239
  }
491
- __name(_ts_metadata3, "_ts_metadata");
240
+ __name(_ts_metadata2, "_ts_metadata");
492
241
  var VrittiAuthGuard = class _VrittiAuthGuard {
493
242
  static {
494
243
  __name(this, "VrittiAuthGuard");
@@ -496,14 +245,12 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
496
245
  reflector;
497
246
  _configService;
498
247
  jwtService;
499
- primaryDatabase;
500
248
  requestService;
501
- logger = new Logger3(_VrittiAuthGuard.name);
502
- constructor(reflector, _configService, jwtService, primaryDatabase, requestService) {
249
+ logger = new Logger2(_VrittiAuthGuard.name);
250
+ constructor(reflector, _configService, jwtService, requestService) {
503
251
  this.reflector = reflector;
504
252
  this._configService = _configService;
505
253
  this.jwtService = jwtService;
506
- this.primaryDatabase = primaryDatabase;
507
254
  this.requestService = requestService;
508
255
  }
509
256
  async canActivate(context) {
@@ -521,69 +268,44 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
521
268
  context.getClass()
522
269
  ]);
523
270
  if (isPublic) {
524
- this.logger.debug("Public endpoint detected, skipping authentication");
525
271
  return true;
526
272
  }
527
273
  const isOnboarding = this.reflector.getAllAndOverride("isOnboarding", [
528
274
  context.getHandler(),
529
275
  context.getClass()
530
276
  ]);
277
+ const isReset = this.reflector.getAllAndOverride(RESET_KEY, [
278
+ context.getHandler(),
279
+ context.getClass()
280
+ ]);
281
+ const isSseEndpoint = this.reflector.get(SSE_METADATA, context.getHandler());
282
+ if (isSseEndpoint) {
283
+ return this.handleSseAuth(request, isOnboarding);
284
+ }
531
285
  try {
532
286
  const accessToken = this.requestService.getAccessToken();
533
287
  if (!accessToken) {
534
- this.logger.warn("Access token not found in Authorization header");
535
288
  throw new UnauthorizedException("Access token not found");
536
289
  }
537
- const decodedToken = this.jwtService.decode(accessToken);
538
- if (!decodedToken) {
539
- this.logger.warn("Failed to decode access token");
540
- throw new UnauthorizedException("Invalid token format");
541
- }
542
- if (isOnboarding) {
543
- if (decodedToken.type !== "onboarding") {
544
- this.logger.warn("Onboarding endpoint requires onboarding token");
545
- throw new UnauthorizedException("This endpoint requires an onboarding token");
546
- }
547
- const validatedToken2 = this.validateAccessToken(accessToken);
548
- this.logger.debug("Onboarding token validated successfully");
549
- this.validateRefreshTokenBinding(context, validatedToken2);
550
- const userId2 = validatedToken2.userId;
551
- request.user = {
552
- id: userId2
553
- };
554
- return true;
555
- }
556
- if (decodedToken.type === "onboarding") {
557
- this.logger.warn("Regular endpoint accessed with onboarding token");
558
- throw new UnauthorizedException("Onboarding tokens cannot access this endpoint");
290
+ const decodedAccessToken = this.validateAccessToken(accessToken);
291
+ if (decodedAccessToken.tokenType !== "access") {
292
+ throw new UnauthorizedException("Invalid token type");
559
293
  }
560
- const validatedToken = this.validateAccessToken(accessToken);
561
- this.logger.debug("Access token validated successfully");
562
- this.validateRefreshTokenBinding(context, validatedToken);
563
- const userId = validatedToken.userId;
564
- request.user = {
565
- id: userId
566
- };
567
- const tenantIdentifier = this.requestService.getTenantIdentifier();
568
- if (!tenantIdentifier) {
569
- this.logger.warn("Tenant identifier not found in request");
570
- throw new UnauthorizedException("Tenant identifier not found");
571
- }
572
- this.logger.debug(`Tenant identifier extracted: ${tenantIdentifier}`);
573
- if (tenantIdentifier === "cloud") {
574
- this.logger.debug("Platform admin access detected, skipping tenant database validation");
575
- return true;
294
+ this.validateRefreshTokenBinding(decodedAccessToken);
295
+ if (isOnboarding && decodedAccessToken.sessionType !== "ONBOARDING") {
296
+ throw new UnauthorizedException("This endpoint requires an onboarding session");
576
297
  }
577
- const tenantInfo = await this.primaryDatabase.getTenantInfo(tenantIdentifier);
578
- if (!tenantInfo) {
579
- this.logger.warn(`Invalid tenant: ${tenantIdentifier}`);
580
- throw new UnauthorizedException("Invalid tenant");
298
+ if (isReset && decodedAccessToken.sessionType !== "RESET") {
299
+ throw new UnauthorizedException("This endpoint requires a reset session");
581
300
  }
582
- if (tenantInfo.status !== "ACTIVE") {
583
- this.logger.warn(`Tenant ${tenantIdentifier} has status: ${tenantInfo.status}`);
584
- throw new UnauthorizedException(`Tenant is ${tenantInfo.status}`);
301
+ if (!isOnboarding && !isReset && (decodedAccessToken.sessionType === "ONBOARDING" || decodedAccessToken.sessionType === "RESET")) {
302
+ throw new UnauthorizedException(`${decodedAccessToken.sessionType} sessions cannot access this endpoint`);
585
303
  }
586
- this.logger.debug(`Tenant validated: ${tenantInfo.subdomain} (${tenantInfo.type})`);
304
+ request.sessionInfo = {
305
+ userId: decodedAccessToken.userId,
306
+ sessionId: decodedAccessToken.sessionId,
307
+ sessionType: decodedAccessToken.sessionType
308
+ };
587
309
  return true;
588
310
  } catch (error) {
589
311
  if (error instanceof UnauthorizedException) {
@@ -593,109 +315,91 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
593
315
  throw new UnauthorizedException("Authentication failed");
594
316
  }
595
317
  }
596
- /**
597
- * Validate access token with proper expiry checks
598
- * Throws UnauthorizedException if token is invalid or expired
599
- */
318
+ // Validates JWT signature, expiry, and not-before claims
600
319
  validateAccessToken(token) {
601
320
  try {
602
- const decoded = this.jwtService.verify(token);
603
- this.logger.debug(`Access token decoded for user: ${decoded.userId}`);
604
- if (decoded.exp) {
605
- const expiryTime = decoded.exp * 1e3;
606
- const currentTime = Date.now();
607
- const timeRemaining = expiryTime - currentTime;
608
- this.logger.debug(`Access token valid for ${Math.floor(timeRemaining / 1e3)} more seconds`);
609
- }
610
- return decoded;
321
+ return this.jwtService.verify(token);
611
322
  } catch (error) {
612
- if (error instanceof UnauthorizedException) {
613
- throw error;
614
- }
323
+ if (error instanceof UnauthorizedException) throw error;
615
324
  const jwtError = error;
616
325
  if (jwtError?.name === "TokenExpiredError") {
617
- this.logger.warn(`Access token expired at: ${jwtError?.expiredAt}`);
618
326
  throw new UnauthorizedException("Access token has expired");
619
327
  }
620
328
  if (jwtError?.name === "JsonWebTokenError") {
621
- this.logger.warn(`Access token verification failed: ${jwtError?.message}`);
622
329
  throw new UnauthorizedException("Invalid access token");
623
330
  }
624
331
  if (jwtError?.name === "NotBeforeError") {
625
- this.logger.warn("Access token used before valid (nbf claim)");
626
332
  throw new UnauthorizedException("Access token not yet valid");
627
333
  }
628
- this.logger.error("Unexpected error validating access token", error);
629
334
  throw new UnauthorizedException("Access token validation failed");
630
335
  }
631
336
  }
632
- /**
633
- * Validate that the access token is bound to the refresh token in the cookie.
634
- * This prevents token theft - a stolen access token is useless without the
635
- * corresponding refresh token cookie.
636
- *
637
- * @param context - The execution context containing the request
638
- * @param validatedToken - The decoded and validated JWT token
639
- * @throws UnauthorizedException if token binding validation fails
640
- */
641
- validateRefreshTokenBinding(context, validatedToken) {
642
- const config = getConfig();
643
- if (!config.jwt.validateTokenBinding) {
644
- this.logger.debug("Token binding validation is disabled");
645
- return;
646
- }
647
- if (!validatedToken.refreshTokenHash) {
648
- this.logger.debug("Token does not contain refreshTokenHash, skipping binding validation");
649
- return;
337
+ // Validates that the access token is bound to the refresh token in the cookie
338
+ validateRefreshTokenBinding(decodedAccessToken) {
339
+ if (!decodedAccessToken.refreshTokenHash) {
340
+ throw new UnauthorizedException("Token missing refresh token binding");
650
341
  }
651
- const request = context.switchToHttp().getRequest();
652
- const cookies = request.cookies || {};
653
- const refreshToken = cookies[config.cookie.refreshCookieName];
342
+ const refreshToken = this.requestService.getRefreshToken();
654
343
  if (!refreshToken) {
655
- this.logger.warn("Session validation failed - refresh token cookie not found");
656
344
  throw new UnauthorizedException("Session validation failed");
657
345
  }
658
- if (!verifyTokenHash(refreshToken, validatedToken.refreshTokenHash)) {
659
- this.logger.warn("Session validation failed - token binding mismatch");
346
+ if (!verifyTokenHash(refreshToken, decodedAccessToken.refreshTokenHash)) {
660
347
  throw new UnauthorizedException("Session validation failed");
661
348
  }
662
- this.logger.debug("Token binding validated successfully");
663
349
  }
664
- /**
665
- * Validate CSRF token for state-changing requests
666
- * Uses Fastify's csrf-protection plugin for token validation
667
- *
668
- * @param request - Fastify request object
669
- * @param reply - Fastify reply object
670
- * @throws ForbiddenException if CSRF validation fails
671
- */
350
+ // Authenticates SSE connections using the refresh token httpOnly cookie
351
+ handleSseAuth(request, isOnboarding) {
352
+ const refreshToken = this.requestService.getRefreshToken();
353
+ if (!refreshToken) {
354
+ throw new UnauthorizedException("Authentication required");
355
+ }
356
+ let decoded;
357
+ try {
358
+ decoded = this.jwtService.verify(refreshToken);
359
+ } catch {
360
+ throw new UnauthorizedException("Invalid or expired session");
361
+ }
362
+ if (decoded.tokenType !== "refresh") {
363
+ throw new UnauthorizedException("Invalid token type");
364
+ }
365
+ if (isOnboarding && decoded.sessionType !== "ONBOARDING") {
366
+ throw new UnauthorizedException("This endpoint requires an onboarding session");
367
+ }
368
+ request.sessionInfo = {
369
+ userId: decoded.userId,
370
+ sessionId: decoded.sessionId,
371
+ sessionType: decoded.sessionType
372
+ };
373
+ return true;
374
+ }
375
+ // Validates CSRF token for state-changing requests
672
376
  async validateCsrf(request, reply) {
673
377
  const safeMethods = [
674
378
  "GET",
675
379
  "HEAD",
676
380
  "OPTIONS"
677
381
  ];
678
- if (safeMethods.includes(request.method)) {
679
- return;
680
- }
382
+ if (safeMethods.includes(request.method)) return;
681
383
  try {
682
384
  const fastifyInstance = request.server;
683
- if (!fastifyInstance.csrfProtection) {
684
- this.logger.error("CSRF protection plugin not found. Ensure @fastify/csrf-protection is registered.");
385
+ const csrfProtection = fastifyInstance.csrfProtection;
386
+ if (!csrfProtection) {
685
387
  throw new ForbiddenException("CSRF protection not configured");
686
388
  }
687
389
  await new Promise((resolve, reject) => {
688
- fastifyInstance.csrfProtection(request, reply, (err) => {
689
- if (err) {
690
- reject(err);
691
- } else {
692
- resolve();
693
- }
390
+ const originalSend = reply.send.bind(reply);
391
+ reply.send = () => {
392
+ reply.send = originalSend;
393
+ reject(new Error("CSRF validation failed"));
394
+ return reply;
395
+ };
396
+ csrfProtection(request, reply, (err) => {
397
+ reply.send = originalSend;
398
+ if (err) reject(err);
399
+ else resolve();
694
400
  });
695
401
  });
696
- this.logger.debug(`CSRF validation successful for ${request.method} ${request.url}`);
697
402
  } catch (error) {
698
- this.logger.warn(`CSRF validation failed for ${request.method} ${request.url}: ${error instanceof Error ? error.message : "Unknown error"}`);
699
403
  throw new ForbiddenException({
700
404
  errors: [
701
405
  {
@@ -708,55 +412,167 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
708
412
  }
709
413
  }
710
414
  };
711
- VrittiAuthGuard = _ts_decorate4([
712
- Injectable3({
415
+ VrittiAuthGuard = _ts_decorate3([
416
+ Injectable2({
713
417
  scope: Scope2.REQUEST
714
418
  }),
715
- _ts_metadata3("design:type", Function),
716
- _ts_metadata3("design:paramtypes", [
419
+ _ts_metadata2("design:type", Function),
420
+ _ts_metadata2("design:paramtypes", [
717
421
  typeof Reflector === "undefined" ? Object : Reflector,
718
422
  typeof ConfigService === "undefined" ? Object : ConfigService,
719
423
  typeof JwtService === "undefined" ? Object : JwtService,
720
- typeof PrimaryDatabaseService === "undefined" ? Object : PrimaryDatabaseService,
721
424
  typeof RequestService === "undefined" ? Object : RequestService
722
425
  ])
723
426
  ], VrittiAuthGuard);
724
427
 
725
- // src/auth/auth-config.module.ts
726
- function _ts_decorate5(decorators, target, key, desc) {
428
+ // src/auth/services/jwt-auth.service.ts
429
+ import { Injectable as Injectable3, Logger as Logger3 } from "@nestjs/common";
430
+ import { ConfigService as ConfigService2 } from "@nestjs/config";
431
+ import { JwtService as NestJwtService } from "@nestjs/jwt";
432
+
433
+ // src/utils/time.utils.ts
434
+ function parseExpiryToMs(expiry) {
435
+ const match = expiry.match(/^(\d+)([smhdwy])$/);
436
+ if (!match) throw new Error(`Invalid expiry format: ${expiry}`);
437
+ const value = Number.parseInt(match[1], 10);
438
+ const multipliers = {
439
+ s: 1e3,
440
+ m: 6e4,
441
+ h: 36e5,
442
+ d: 864e5,
443
+ w: 6048e5,
444
+ y: 31536e6
445
+ };
446
+ return value * multipliers[match[2]];
447
+ }
448
+ __name(parseExpiryToMs, "parseExpiryToMs");
449
+
450
+ // src/auth/jwt.config.ts
451
+ var jwtConfigFactory = /* @__PURE__ */ __name((configService) => ({
452
+ secret: configService.getOrThrow("JWT_SECRET"),
453
+ signOptions: {
454
+ issuer: "vritti-api"
455
+ }
456
+ }), "jwtConfigFactory");
457
+ var getTokenExpiry = /* @__PURE__ */ __name((configService) => ({
458
+ access: configService.getOrThrow("ACCESS_TOKEN_EXPIRY"),
459
+ refresh: configService.getOrThrow("REFRESH_TOKEN_EXPIRY")
460
+ }), "getTokenExpiry");
461
+ var TokenType = /* @__PURE__ */ (function(TokenType2) {
462
+ TokenType2["ACCESS"] = "access";
463
+ TokenType2["REFRESH"] = "refresh";
464
+ return TokenType2;
465
+ })({});
466
+
467
+ // src/auth/services/jwt-auth.service.ts
468
+ function _ts_decorate4(decorators, target, key, desc) {
727
469
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
728
470
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
729
471
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
730
472
  return c > 3 && r && Object.defineProperty(target, key, r), r;
731
473
  }
732
- __name(_ts_decorate5, "_ts_decorate");
733
- var AuthConfigModule = class _AuthConfigModule {
474
+ __name(_ts_decorate4, "_ts_decorate");
475
+ function _ts_metadata3(k, v) {
476
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
477
+ }
478
+ __name(_ts_metadata3, "_ts_metadata");
479
+ var JwtAuthService = class _JwtAuthService {
734
480
  static {
735
- __name(this, "AuthConfigModule");
481
+ __name(this, "JwtAuthService");
736
482
  }
737
- /**
738
- * Register the auth module with async configuration
739
- *
740
- * This method:
741
- * 1. Configures JwtModule with JWT_SECRET from ConfigService
742
- * 2. Provides VrittiAuthGuard globally (applies to all routes)
743
- * 3. Exports JwtModule for use in other modules (e.g., for signing tokens)
744
- *
745
- * @returns Dynamic module configuration
746
- */
747
- static forRootAsync() {
748
- return {
749
- module: _AuthConfigModule,
750
- imports: [
751
- ConfigModule,
752
- RequestModule,
753
- JwtModule.registerAsync({
754
- imports: [
755
- ConfigModule
756
- ],
757
- inject: [
758
- ConfigService2
759
- ],
483
+ jwtService;
484
+ configService;
485
+ logger = new Logger3(_JwtAuthService.name);
486
+ tokenExpiry;
487
+ constructor(jwtService, configService) {
488
+ this.jwtService = jwtService;
489
+ this.configService = configService;
490
+ this.tokenExpiry = getTokenExpiry(configService);
491
+ }
492
+ // Generates an access token bound to the given refresh token
493
+ generateAccessToken(userId, sessionId, sessionType, refreshToken) {
494
+ return this.jwtService.sign({
495
+ sessionType,
496
+ tokenType: TokenType.ACCESS,
497
+ userId,
498
+ sessionId,
499
+ refreshTokenHash: hashToken(refreshToken)
500
+ }, {
501
+ expiresIn: this.tokenExpiry.access
502
+ });
503
+ }
504
+ // Generates a refresh token for session persistence
505
+ generateRefreshToken(userId, sessionId, sessionType) {
506
+ return this.jwtService.sign({
507
+ sessionType,
508
+ tokenType: TokenType.REFRESH,
509
+ userId,
510
+ sessionId
511
+ }, {
512
+ expiresIn: this.tokenExpiry.refresh
513
+ });
514
+ }
515
+ // Signs an arbitrary payload with optional JWT options
516
+ sign(payload, options) {
517
+ return this.jwtService.sign(payload, options);
518
+ }
519
+ // Verifies a token and ensures it matches the expected token type
520
+ verify(token, expectedType) {
521
+ try {
522
+ const payload = this.jwtService.verify(token);
523
+ if (payload.tokenType !== expectedType) {
524
+ throw new Error(`Expected ${expectedType} token, got ${payload.tokenType}`);
525
+ }
526
+ return payload;
527
+ } catch (error) {
528
+ this.logger.error(`Failed to verify ${expectedType} token`, error);
529
+ throw error;
530
+ }
531
+ }
532
+ // Returns the expiry as a Date for the given token type
533
+ getExpiryTime(type) {
534
+ return new Date(Date.now() + parseExpiryToMs(this.tokenExpiry[type]));
535
+ }
536
+ // Returns the token lifetime in seconds for the given type
537
+ getExpiryInSeconds(type) {
538
+ return Math.floor(parseExpiryToMs(this.tokenExpiry[type]) / 1e3);
539
+ }
540
+ };
541
+ JwtAuthService = _ts_decorate4([
542
+ Injectable3(),
543
+ _ts_metadata3("design:type", Function),
544
+ _ts_metadata3("design:paramtypes", [
545
+ typeof NestJwtService === "undefined" ? Object : NestJwtService,
546
+ typeof ConfigService2 === "undefined" ? Object : ConfigService2
547
+ ])
548
+ ], JwtAuthService);
549
+
550
+ // src/auth/auth-config.module.ts
551
+ function _ts_decorate5(decorators, target, key, desc) {
552
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
553
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
554
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
555
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
556
+ }
557
+ __name(_ts_decorate5, "_ts_decorate");
558
+ var AuthConfigModule = class _AuthConfigModule {
559
+ static {
560
+ __name(this, "AuthConfigModule");
561
+ }
562
+ // Registers JWT and global VrittiAuthGuard with async config
563
+ static forRootAsync() {
564
+ return {
565
+ module: _AuthConfigModule,
566
+ imports: [
567
+ ConfigModule,
568
+ RequestModule,
569
+ JwtModule.registerAsync({
570
+ imports: [
571
+ ConfigModule
572
+ ],
573
+ inject: [
574
+ ConfigService3
575
+ ],
760
576
  useFactory: /* @__PURE__ */ __name((config) => ({
761
577
  secret: config.get("JWT_SECRET"),
762
578
  signOptions: {
@@ -774,10 +590,12 @@ var AuthConfigModule = class _AuthConfigModule {
774
590
  {
775
591
  provide: APP_GUARD,
776
592
  useClass: VrittiAuthGuard
777
- }
593
+ },
594
+ JwtAuthService
778
595
  ],
779
596
  exports: [
780
- JwtModule
597
+ JwtModule,
598
+ JwtAuthService
781
599
  ]
782
600
  };
783
601
  }
@@ -787,28 +605,69 @@ AuthConfigModule = _ts_decorate5([
787
605
  Module2({})
788
606
  ], AuthConfigModule);
789
607
 
608
+ // src/auth/decorators/access-token.decorator.ts
609
+ import { createParamDecorator } from "@nestjs/common";
610
+ var AccessToken = createParamDecorator((_data, ctx) => {
611
+ const request = ctx.switchToHttp().getRequest();
612
+ const authHeader = request.headers.authorization;
613
+ return authHeader?.replace("Bearer ", "") || "";
614
+ });
615
+
790
616
  // src/auth/decorators/onboarding.decorator.ts
791
- import { SetMetadata as SetMetadata2 } from "@nestjs/common";
792
- var Onboarding = /* @__PURE__ */ __name(() => SetMetadata2("isOnboarding", true), "Onboarding");
617
+ import { SetMetadata as SetMetadata3 } from "@nestjs/common";
618
+ var Onboarding = /* @__PURE__ */ __name(() => SetMetadata3("isOnboarding", true), "Onboarding");
793
619
 
794
620
  // src/auth/decorators/public.decorator.ts
795
- import { SetMetadata as SetMetadata3 } from "@nestjs/common";
796
- var Public = /* @__PURE__ */ __name(() => SetMetadata3("isPublic", true), "Public");
621
+ import { SetMetadata as SetMetadata4 } from "@nestjs/common";
622
+ var Public = /* @__PURE__ */ __name(() => SetMetadata4("isPublic", true), "Public");
623
+
624
+ // src/auth/decorators/refresh-token-cookie.decorator.ts
625
+ import { createParamDecorator as createParamDecorator2 } from "@nestjs/common";
626
+ var RefreshTokenCookie = createParamDecorator2((_data, ctx) => {
627
+ const request = ctx.switchToHttp().getRequest();
628
+ const cookies = request.cookies ?? {};
629
+ const config = getConfig();
630
+ return cookies[config.cookie.refreshCookieName];
631
+ });
632
+
633
+ // src/auth/decorators/session-data.decorator.ts
634
+ import { createParamDecorator as createParamDecorator3 } from "@nestjs/common";
635
+ var SessionData = createParamDecorator3((_data, ctx) => {
636
+ const request = ctx.switchToHttp().getRequest();
637
+ const sessionInfo = request.sessionInfo;
638
+ if (!sessionInfo?.sessionId) {
639
+ throw new Error("Session info not found on request. Ensure route is protected by auth guard.");
640
+ }
641
+ return {
642
+ userId: sessionInfo.userId,
643
+ sessionId: sessionInfo.sessionId,
644
+ sessionType: sessionInfo.sessionType
645
+ };
646
+ });
797
647
 
798
648
  // src/auth/decorators/user-id.decorator.ts
799
- import { createParamDecorator } from "@nestjs/common";
800
- var UserId = createParamDecorator((_data, ctx) => {
649
+ import { createParamDecorator as createParamDecorator4 } from "@nestjs/common";
650
+ var UserId = createParamDecorator4((_data, ctx) => {
801
651
  const request = ctx.switchToHttp().getRequest();
802
- const user = request.user;
803
- if (!user?.id) {
652
+ const sessionInfo = request.sessionInfo;
653
+ if (!sessionInfo?.userId) {
804
654
  throw new Error("User ID not found on request. Ensure route is protected by auth guard.");
805
655
  }
806
- return user.id;
656
+ return sessionInfo.userId;
807
657
  });
808
658
 
809
- // src/auth/guards/sse-auth.guard.ts
810
- import { Injectable as Injectable4, Logger as Logger4, Scope as Scope3, UnauthorizedException as UnauthorizedException2 } from "@nestjs/common";
811
- import { JwtService as JwtService2 } from "@nestjs/jwt";
659
+ // src/database/database.module.ts
660
+ import { Global as Global3, Module as Module3 } from "@nestjs/common";
661
+ import { Reflector as Reflector3 } from "@nestjs/core";
662
+
663
+ // src/database/constants.ts
664
+ var DATABASE_MODULE_OPTIONS = Symbol("DATABASE_MODULE_OPTIONS");
665
+
666
+ // src/database/services/primary-database.service.ts
667
+ import { Inject as Inject2, Injectable as Injectable4, InternalServerErrorException, Logger as Logger4 } from "@nestjs/common";
668
+ import { eq, or } from "drizzle-orm";
669
+ import { drizzle } from "drizzle-orm/node-postgres";
670
+ import { Pool } from "pg";
812
671
  function _ts_decorate6(decorators, target, key, desc) {
813
672
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
814
673
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -820,102 +679,184 @@ function _ts_metadata4(k, v) {
820
679
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
821
680
  }
822
681
  __name(_ts_metadata4, "_ts_metadata");
823
- var SSE_ALLOWED_ORIGINS = [
824
- "http://localhost:5173",
825
- "http://localhost:3001",
826
- "http://localhost:3012",
827
- "http://localhost:5174",
828
- "http://local.vrittiai.com:3012",
829
- "http://cloud.local.vrittiai.com:3012",
830
- "https://local.vrittiai.com:3012",
831
- "https://cloud.local.vrittiai.com:3012"
832
- ];
833
- var SseAuthGuard = class _SseAuthGuard {
682
+ function _ts_param2(paramIndex, decorator) {
683
+ return function(target, key) {
684
+ decorator(target, key, paramIndex);
685
+ };
686
+ }
687
+ __name(_ts_param2, "_ts_param");
688
+ var PrimaryDatabaseService = class _PrimaryDatabaseService {
834
689
  static {
835
- __name(this, "SseAuthGuard");
690
+ __name(this, "PrimaryDatabaseService");
836
691
  }
837
- jwtService;
838
- logger = new Logger4(_SseAuthGuard.name);
839
- constructor(jwtService) {
840
- this.jwtService = jwtService;
692
+ options;
693
+ logger = new Logger4(_PrimaryDatabaseService.name);
694
+ pool = null;
695
+ db = null;
696
+ tenantConfigCache = /* @__PURE__ */ new Map();
697
+ cacheTTL;
698
+ constructor(options) {
699
+ this.options = options;
700
+ this.cacheTTL = options.connectionCacheTTL || 3e5;
841
701
  }
842
- async canActivate(context) {
843
- const request = context.switchToHttp().getRequest();
844
- const response = context.switchToHttp().getResponse();
845
- this.setCorsHeaders(request, response);
846
- const token = request.query?.token;
847
- if (!token) {
848
- this.logger.warn("SSE authentication failed: token not found in query params");
849
- throw new UnauthorizedException2("Authentication required");
702
+ async onModuleInit() {
703
+ if (this.options.primaryDb) {
704
+ await this.initializeDrizzleClient();
850
705
  }
706
+ }
707
+ // Initializes connection to primary database using Drizzle
708
+ async initializeDrizzleClient() {
851
709
  try {
852
- const decodedToken = this.jwtService.decode(token);
853
- if (!decodedToken) {
854
- this.logger.warn("SSE authentication failed: invalid token format");
855
- throw new UnauthorizedException2("Invalid token format");
856
- }
857
- if (decodedToken.type !== "onboarding") {
858
- this.logger.warn("SSE authentication failed: endpoint requires onboarding token");
859
- throw new UnauthorizedException2("This endpoint requires an onboarding token");
860
- }
861
- const validatedToken = this.jwtService.verify(token);
862
- this.logger.debug(`SSE token validated for user: ${validatedToken.userId}`);
863
- request.user = {
864
- id: validatedToken.userId
865
- };
866
- return true;
710
+ const databaseUrl = this.buildPrimaryDbUrl();
711
+ this.pool = new Pool({
712
+ connectionString: databaseUrl,
713
+ max: this.options.maxConnections || 10
714
+ });
715
+ this.logger.debug(`Schema keys passed to drizzle: [${Object.keys(this.options.drizzleSchema || {}).join(", ")}]`);
716
+ this.logger.debug(`Relations keys passed to drizzle: [${Object.keys(this.options.drizzleRelations || {}).join(", ")}]`);
717
+ this.db = drizzle({
718
+ client: this.pool,
719
+ schema: this.options.drizzleSchema,
720
+ relations: this.options.drizzleRelations
721
+ });
722
+ this.logger.debug(`Drizzle query keys after init: [${Object.keys(this.db.query || {}).join(", ")}]`);
723
+ await this.pool.query("SELECT 1");
724
+ this.logger.log("Connected to primary database (tenant registry)");
867
725
  } catch (error) {
868
- if (error instanceof UnauthorizedException2) {
869
- throw error;
726
+ this.logger.error("Failed to connect to primary database", error);
727
+ throw new InternalServerErrorException("Failed to initialize tenant registry");
728
+ }
729
+ }
730
+ // Builds the PostgreSQL connection URL from primary database config properties
731
+ buildPrimaryDbUrl() {
732
+ if (!this.options.primaryDb) {
733
+ throw new Error("Primary database configuration not provided");
734
+ }
735
+ const { host, port = 5432, username, password, database, schema = "public", sslMode = "require" } = this.options.primaryDb;
736
+ let url = `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
737
+ const params = new URLSearchParams();
738
+ if (schema) {
739
+ params.set("schema", schema);
740
+ }
741
+ params.set("sslmode", sslMode);
742
+ const queryString = params.toString();
743
+ if (queryString) {
744
+ url += `?${queryString}`;
745
+ }
746
+ this.logger.debug(`Primary DB connection URL: ${this.maskPassword(url)}`);
747
+ return url;
748
+ }
749
+ // Masks password in connection URL for safe logging
750
+ maskPassword(url) {
751
+ return url.replace(/:([^@]+)@/, ":****@");
752
+ }
753
+ // Retrieves tenant configuration by ID or subdomain, with in-memory caching
754
+ async getTenantInfo(tenantIdentifier) {
755
+ const cached = this.tenantConfigCache.get(tenantIdentifier);
756
+ if (cached) {
757
+ this.logger.debug(`Cache hit for tenant: ${tenantIdentifier}`);
758
+ return cached;
759
+ }
760
+ try {
761
+ if (!this.db) {
762
+ throw new Error("Primary database client not initialized");
870
763
  }
871
- const jwtError = error;
872
- if (jwtError?.name === "TokenExpiredError") {
873
- this.logger.warn("SSE authentication failed: token expired");
874
- throw new UnauthorizedException2("Token has expired");
764
+ this.logger.debug(`Querying primary database for tenant: ${tenantIdentifier}`);
765
+ const schema = this.options.drizzleSchema;
766
+ const { tenants, tenantDatabaseConfigs } = schema;
767
+ const result = await this.db.select().from(tenants).leftJoin(tenantDatabaseConfigs, eq(tenants.id, tenantDatabaseConfigs.tenantId)).where(or(eq(tenants.id, tenantIdentifier), eq(tenants.subdomain, tenantIdentifier))).limit(1);
768
+ if (!result.length) {
769
+ this.logger.warn(`Tenant not found: ${tenantIdentifier}`);
770
+ return null;
875
771
  }
876
- if (jwtError?.name === "JsonWebTokenError") {
877
- this.logger.warn(`SSE authentication failed: ${jwtError?.message}`);
878
- throw new UnauthorizedException2("Invalid token");
772
+ const row = result[0];
773
+ const tenant = row.tenants;
774
+ const config = row.tenant_database_configs;
775
+ if (tenant.status !== "ACTIVE") {
776
+ this.logger.warn(`Tenant not active: ${tenantIdentifier}`);
777
+ return null;
879
778
  }
880
- this.logger.error("Unexpected error in SSE auth guard", error);
881
- throw new UnauthorizedException2("Authentication failed");
779
+ const info = {
780
+ id: tenant.id,
781
+ subdomain: tenant.subdomain,
782
+ type: tenant.dbType,
783
+ status: tenant.status,
784
+ // For SHARED tenants: schema name
785
+ schemaName: config?.dbSchema || void 0,
786
+ // For DEDICATED tenants: database configuration from TenantDatabaseConfig table
787
+ databaseName: config?.dbName || void 0,
788
+ databaseHost: config?.dbHost || void 0,
789
+ databasePort: config?.dbPort || void 0,
790
+ databaseUsername: config?.dbUsername ? this.decrypt(config.dbUsername) : void 0,
791
+ databasePassword: config?.dbPassword ? this.decrypt(config.dbPassword) : void 0,
792
+ databaseSslMode: config?.dbSslMode || void 0,
793
+ connectionPoolSize: config?.connectionPoolSize || void 0
794
+ };
795
+ this.cacheInfo(info);
796
+ return info;
797
+ } catch (error) {
798
+ this.logger.error(`Failed to fetch tenant info: ${tenantIdentifier}`, error);
799
+ throw new InternalServerErrorException("Failed to resolve tenant");
800
+ }
801
+ }
802
+ // Caches tenant info by both ID and subdomain with TTL expiration
803
+ cacheInfo(info) {
804
+ this.tenantConfigCache.set(info.id, info);
805
+ this.tenantConfigCache.set(info.subdomain, info);
806
+ setTimeout(() => {
807
+ this.tenantConfigCache.delete(info.id);
808
+ this.tenantConfigCache.delete(info.subdomain);
809
+ this.logger.debug(`Cache expired for tenant: ${info.subdomain}`);
810
+ }, this.cacheTTL);
811
+ }
812
+ // Clears cached tenant info for the given ID or subdomain
813
+ clearTenantCache(tenantIdentifier) {
814
+ const config = this.tenantConfigCache.get(tenantIdentifier);
815
+ if (config) {
816
+ this.tenantConfigCache.delete(config.id);
817
+ this.tenantConfigCache.delete(config.subdomain);
818
+ this.logger.log(`Cleared cache for tenant: ${tenantIdentifier}`);
882
819
  }
883
820
  }
884
- /**
885
- * Set CORS headers for SSE responses
886
- * Must be called before any potential exceptions
887
- */
888
- setCorsHeaders(request, response) {
889
- const origin = request.headers.origin;
890
- if (origin && SSE_ALLOWED_ORIGINS.includes(origin)) {
891
- response.header("Access-Control-Allow-Origin", origin);
892
- response.header("Access-Control-Allow-Credentials", "true");
893
- this.logger.debug(`CORS headers set for origin: ${origin}`);
894
- } else if (origin) {
895
- this.logger.warn(`SSE request from unauthorized origin: ${origin}`);
821
+ // Clears all cached tenant configurations
822
+ clearAllCaches() {
823
+ const size = this.tenantConfigCache.size;
824
+ this.tenantConfigCache.clear();
825
+ this.logger.log(`Cleared ${size} cached tenant configs`);
826
+ }
827
+ // Returns the initialized Drizzle client, throwing if not yet initialized
828
+ get drizzleClient() {
829
+ if (!this.db) {
830
+ throw new Error("Primary database client not initialized");
831
+ }
832
+ return this.db;
833
+ }
834
+ // Returns the Drizzle schema passed in module options
835
+ get schema() {
836
+ return this.options.drizzleSchema;
837
+ }
838
+ // Decrypts a database credential value (placeholder for actual decryption)
839
+ decrypt(encrypted) {
840
+ return encrypted;
841
+ }
842
+ async onModuleDestroy() {
843
+ if (this.pool) {
844
+ await this.pool.end();
845
+ this.logger.log("Disconnected from primary database");
896
846
  }
897
847
  }
898
848
  };
899
- SseAuthGuard = _ts_decorate6([
900
- Injectable4({
901
- scope: Scope3.REQUEST
902
- }),
849
+ PrimaryDatabaseService = _ts_decorate6([
850
+ Injectable4(),
851
+ _ts_param2(0, Inject2(DATABASE_MODULE_OPTIONS)),
903
852
  _ts_metadata4("design:type", Function),
904
853
  _ts_metadata4("design:paramtypes", [
905
- typeof JwtService2 === "undefined" ? Object : JwtService2
854
+ typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions
906
855
  ])
907
- ], SseAuthGuard);
908
-
909
- // src/database/database.module.ts
910
- import { Global as Global3, Module as Module3 } from "@nestjs/common";
911
- import { APP_INTERCEPTOR, Reflector as Reflector4 } from "@nestjs/core";
912
-
913
- // src/database/interceptors/message-tenant-context.interceptor.ts
914
- import { Injectable as Injectable6, Logger as Logger5, Scope as Scope5 } from "@nestjs/common";
915
- import { tap } from "rxjs/operators";
856
+ ], PrimaryDatabaseService);
916
857
 
917
858
  // src/database/services/tenant-context.service.ts
918
- import { Injectable as Injectable5, Scope as Scope4, UnauthorizedException as UnauthorizedException3 } from "@nestjs/common";
859
+ import { Injectable as Injectable5, Scope as Scope3, UnauthorizedException as UnauthorizedException2 } from "@nestjs/common";
919
860
  function _ts_decorate7(decorators, target, key, desc) {
920
861
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
921
862
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -928,79 +869,47 @@ var TenantContextService = class {
928
869
  __name(this, "TenantContextService");
929
870
  }
930
871
  tenantInfo = null;
931
- /**
932
- * Set tenant information for this request/message
933
- *
934
- * This is typically called by:
935
- * - TenantContextInterceptor (for HTTP requests in gateway)
936
- * - MessageTenantContextInterceptor (for RabbitMQ messages in microservices)
937
- * - Manual context setup in message handlers
938
- *
939
- * @param tenantInfo Complete tenant information
940
- * @throws Error if tenant context is already set (prevents accidental overwrites)
941
- */
872
+ // Sets tenant info for this request, throwing if already set to prevent overwrites
942
873
  setTenant(tenantInfo) {
943
874
  if (this.tenantInfo) {
944
875
  throw new Error("Tenant context already set for this request");
945
876
  }
946
877
  this.tenantInfo = tenantInfo;
947
878
  }
948
- /**
949
- * Get tenant information for this request/message
950
- *
951
- * @returns Tenant information
952
- * @throws UnauthorizedException if tenant context hasn't been set
953
- */
879
+ // Returns the tenant info for this request, throwing if context is not set
954
880
  getTenant() {
955
881
  if (!this.tenantInfo) {
956
- throw new UnauthorizedException3("Tenant context not set");
882
+ throw new UnauthorizedException2("Tenant context not set");
957
883
  }
958
884
  return this.tenantInfo;
959
885
  }
960
- /**
961
- * Check if tenant context has been set
962
- *
963
- * @returns true if tenant context is available
964
- */
886
+ // Returns true if tenant context has been set for this request
965
887
  hasTenant() {
966
888
  return this.tenantInfo !== null;
967
889
  }
968
- /**
969
- * Clear tenant context
970
- *
971
- * This is useful for cleanup in RabbitMQ message handlers
972
- * after the message has been processed.
973
- *
974
- * HTTP requests don't need manual cleanup as the service
975
- * instance is destroyed when the request ends.
976
- */
890
+ // Clears the tenant context (useful for RabbitMQ message handler cleanup)
977
891
  clearTenant() {
978
892
  this.tenantInfo = null;
979
893
  }
980
- /**
981
- * Get tenant ID safely (returns null if not set)
982
- *
983
- * @returns Tenant ID or null
984
- */
894
+ // Returns the tenant ID or null if context is not set
985
895
  getTenantIdSafe() {
986
896
  return this.tenantInfo?.id ?? null;
987
897
  }
988
- /**
989
- * Get tenant subdomain safely (returns null if not set)
990
- *
991
- * @returns Tenant subdomain or null
992
- */
898
+ // Returns the tenant subdomain or null if context is not set
993
899
  getTenantSubdomainSafe() {
994
900
  return this.tenantInfo?.subdomain ?? null;
995
901
  }
996
902
  };
997
903
  TenantContextService = _ts_decorate7([
998
904
  Injectable5({
999
- scope: Scope4.REQUEST
905
+ scope: Scope3.REQUEST
1000
906
  })
1001
907
  ], TenantContextService);
1002
908
 
1003
- // src/database/interceptors/message-tenant-context.interceptor.ts
909
+ // src/database/services/tenant-database.service.ts
910
+ import { Inject as Inject3, Injectable as Injectable6, InternalServerErrorException as InternalServerErrorException2, Logger as Logger5 } from "@nestjs/common";
911
+ import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
912
+ import { Pool as Pool2 } from "pg";
1004
913
  function _ts_decorate8(decorators, target, key, desc) {
1005
914
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1006
915
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1012,217 +921,36 @@ function _ts_metadata5(k, v) {
1012
921
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1013
922
  }
1014
923
  __name(_ts_metadata5, "_ts_metadata");
1015
- var MessageTenantContextInterceptor = class _MessageTenantContextInterceptor {
924
+ function _ts_param3(paramIndex, decorator) {
925
+ return function(target, key) {
926
+ decorator(target, key, paramIndex);
927
+ };
928
+ }
929
+ __name(_ts_param3, "_ts_param");
930
+ var TenantDatabaseService = class _TenantDatabaseService {
1016
931
  static {
1017
- __name(this, "MessageTenantContextInterceptor");
932
+ __name(this, "TenantDatabaseService");
1018
933
  }
934
+ options;
1019
935
  tenantContext;
1020
- logger = new Logger5(_MessageTenantContextInterceptor.name);
1021
- constructor(tenantContext) {
1022
- this.tenantContext = tenantContext;
1023
- }
1024
- intercept(context, next) {
1025
- const contextType = context.getType();
1026
- if (contextType === "rpc") {
1027
- const rpcContext = context.switchToRpc();
1028
- const payload = rpcContext.getData();
1029
- if (payload?.tenant) {
1030
- const tenant = payload.tenant;
1031
- this.logger.debug(`Setting tenant context from message: ${tenant.subdomain}`);
1032
- try {
1033
- this.tenantContext.setTenant(tenant);
1034
- this.logger.log(`Tenant context set: ${tenant.subdomain} (${tenant.type})`);
1035
- } catch (error) {
1036
- this.logger.error("Failed to set tenant context from message", error);
1037
- }
1038
- } else {
1039
- this.logger.warn("Message payload missing tenant information");
1040
- }
1041
- }
1042
- return next.handle().pipe(tap({
1043
- next: /* @__PURE__ */ __name(() => {
1044
- this.cleanupContext();
1045
- }, "next"),
1046
- error: /* @__PURE__ */ __name(() => {
1047
- this.cleanupContext();
1048
- }, "error"),
1049
- complete: /* @__PURE__ */ __name(() => {
1050
- this.cleanupContext();
1051
- }, "complete")
1052
- }));
1053
- }
1054
- /**
1055
- * Clean up tenant context after message is processed
1056
- */
1057
- cleanupContext() {
1058
- if (this.tenantContext.hasTenant()) {
1059
- const tenant = this.tenantContext.getTenantIdSafe();
1060
- this.tenantContext.clearTenant();
1061
- this.logger.debug(`Cleaned up tenant context: ${tenant}`);
1062
- }
1063
- }
1064
- };
1065
- MessageTenantContextInterceptor = _ts_decorate8([
1066
- Injectable6({
1067
- scope: Scope5.REQUEST
1068
- }),
1069
- _ts_metadata5("design:type", Function),
1070
- _ts_metadata5("design:paramtypes", [
1071
- typeof TenantContextService === "undefined" ? Object : TenantContextService
1072
- ])
1073
- ], MessageTenantContextInterceptor);
1074
-
1075
- // src/database/interceptors/tenant-context.interceptor.ts
1076
- import { Injectable as Injectable7, Logger as Logger6, Scope as Scope6, UnauthorizedException as UnauthorizedException4 } from "@nestjs/common";
1077
- import { Reflector as Reflector3 } from "@nestjs/core";
1078
- function _ts_decorate9(decorators, target, key, desc) {
1079
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1080
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1081
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1082
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1083
- }
1084
- __name(_ts_decorate9, "_ts_decorate");
1085
- function _ts_metadata6(k, v) {
1086
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1087
- }
1088
- __name(_ts_metadata6, "_ts_metadata");
1089
- var TenantContextInterceptor = class _TenantContextInterceptor {
1090
- static {
1091
- __name(this, "TenantContextInterceptor");
1092
- }
1093
- reflector;
1094
- tenantContext;
1095
- primaryDatabase;
1096
- requestService;
1097
- logger = new Logger6(_TenantContextInterceptor.name);
1098
- constructor(reflector, tenantContext, primaryDatabase, requestService) {
1099
- this.reflector = reflector;
1100
- this.tenantContext = tenantContext;
1101
- this.primaryDatabase = primaryDatabase;
1102
- this.requestService = requestService;
1103
- }
1104
- async intercept(context, next) {
1105
- const request = context.switchToHttp().getRequest();
1106
- this.logger.debug(`Processing request: ${request.method} ${request.url}`);
1107
- const isPublic = this.reflector.getAllAndOverride("isPublic", [
1108
- context.getHandler(),
1109
- context.getClass()
1110
- ]);
1111
- try {
1112
- const tenantIdentifier = this.requestService.getTenantIdentifier();
1113
- if (isPublic && !tenantIdentifier) {
1114
- this.logger.debug("Public endpoint without tenant identifier, skipping tenant context setup");
1115
- return next.handle();
1116
- }
1117
- if (!tenantIdentifier) {
1118
- throw new UnauthorizedException4("Tenant identifier not found in request");
1119
- }
1120
- this.logger.debug(`Tenant identifier extracted: ${tenantIdentifier}`);
1121
- if (tenantIdentifier === "cloud") {
1122
- this.logger.log("Cloud platform access detected, skipping tenant context setup");
1123
- return next.handle();
1124
- }
1125
- const tenantInfo = await this.primaryDatabase.getTenantInfo(tenantIdentifier);
1126
- if (!tenantInfo) {
1127
- this.logger.warn(`Invalid tenant: ${tenantIdentifier}`);
1128
- throw new UnauthorizedException4("Invalid tenant");
1129
- }
1130
- if (tenantInfo.status !== "ACTIVE") {
1131
- this.logger.warn(`Tenant ${tenantIdentifier} has status: ${tenantInfo.status}`);
1132
- throw new UnauthorizedException4(`Tenant is ${tenantInfo.status}`);
1133
- }
1134
- this.logger.debug(`Tenant config loaded: ${tenantInfo.subdomain} (${tenantInfo.type})`);
1135
- this.tenantContext.setTenant(tenantInfo);
1136
- request.tenant = tenantInfo;
1137
- this.logger.log(`Tenant context set: ${tenantInfo.subdomain}`);
1138
- } catch (error) {
1139
- this.logger.error("Failed to set tenant context", error);
1140
- throw error;
1141
- }
1142
- return next.handle();
1143
- }
1144
- };
1145
- TenantContextInterceptor = _ts_decorate9([
1146
- Injectable7({
1147
- scope: Scope6.REQUEST
1148
- }),
1149
- _ts_metadata6("design:type", Function),
1150
- _ts_metadata6("design:paramtypes", [
1151
- typeof Reflector3 === "undefined" ? Object : Reflector3,
1152
- typeof TenantContextService === "undefined" ? Object : TenantContextService,
1153
- typeof PrimaryDatabaseService === "undefined" ? Object : PrimaryDatabaseService,
1154
- typeof RequestService === "undefined" ? Object : RequestService
1155
- ])
1156
- ], TenantContextInterceptor);
1157
-
1158
- // src/database/services/tenant-database.service.ts
1159
- import { Inject as Inject3, Injectable as Injectable8, InternalServerErrorException as InternalServerErrorException2, Logger as Logger7 } from "@nestjs/common";
1160
- import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
1161
- import { Pool as Pool2 } from "pg";
1162
- function _ts_decorate10(decorators, target, key, desc) {
1163
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1164
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1165
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1166
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1167
- }
1168
- __name(_ts_decorate10, "_ts_decorate");
1169
- function _ts_metadata7(k, v) {
1170
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1171
- }
1172
- __name(_ts_metadata7, "_ts_metadata");
1173
- function _ts_param3(paramIndex, decorator) {
1174
- return function(target, key) {
1175
- decorator(target, key, paramIndex);
1176
- };
1177
- }
1178
- __name(_ts_param3, "_ts_param");
1179
- var TenantDatabaseService = class _TenantDatabaseService {
1180
- static {
1181
- __name(this, "TenantDatabaseService");
1182
- }
1183
- options;
1184
- tenantContext;
1185
- logger = new Logger7(_TenantDatabaseService.name);
1186
- /** Connection pool: Map<cacheKey, TenantConnection> */
1187
- clients = /* @__PURE__ */ new Map();
1188
- /** Track last usage time for idle connection cleanup */
1189
- clientLastUsed = /* @__PURE__ */ new Map();
1190
- /** Cleanup interval timer */
1191
- cleanupInterval;
1192
- constructor(options, tenantContext) {
1193
- this.options = options;
936
+ logger = new Logger5(_TenantDatabaseService.name);
937
+ clients = /* @__PURE__ */ new Map();
938
+ clientLastUsed = /* @__PURE__ */ new Map();
939
+ cleanupInterval;
940
+ constructor(options, tenantContext) {
941
+ this.options = options;
1194
942
  this.tenantContext = tenantContext;
1195
943
  this.startConnectionCleaner();
1196
944
  }
1197
- /**
1198
- * Get the Drizzle client for the current tenant's database.
1199
- * This returns the tenant-scoped database client.
1200
- *
1201
- * @returns Tenant-scoped Drizzle database instance
1202
- * @throws UnauthorizedException if tenant context not set
1203
- * @throws InternalServerErrorException if connection fails
1204
- */
945
+ // Returns the Drizzle client scoped to the current tenant's database
1205
946
  get drizzleClient() {
1206
947
  return this.getDbClient();
1207
948
  }
1208
- /**
1209
- * Get the Drizzle schema
1210
- */
949
+ // Returns the Drizzle schema passed in module options
1211
950
  get schema() {
1212
951
  return this.options.drizzleSchema;
1213
952
  }
1214
- /**
1215
- * Get tenant-scoped database client for the current request/message
1216
- *
1217
- * This method:
1218
- * 1. Gets tenant info from TenantContextService
1219
- * 2. Builds a connection URL based on tenant type
1220
- * 3. Returns cached client if exists, otherwise creates new one
1221
- *
1222
- * @returns Drizzle database instance
1223
- * @throws UnauthorizedException if tenant context not set
1224
- * @throws InternalServerErrorException if connection fails
1225
- */
953
+ // Returns a cached or new Drizzle client for the current tenant context
1226
954
  getDbClient() {
1227
955
  const tenant = this.tenantContext.getTenant();
1228
956
  const cacheKey = this.buildCacheKey(tenant);
@@ -1238,9 +966,7 @@ var TenantDatabaseService = class _TenantDatabaseService {
1238
966
  this.clientLastUsed.set(cacheKey, Date.now());
1239
967
  return connection.db;
1240
968
  }
1241
- /**
1242
- * Create a new database client for the given tenant (synchronous)
1243
- */
969
+ // Creates a new pool and Drizzle client for the given tenant
1244
970
  createDbClientSync(tenant) {
1245
971
  try {
1246
972
  const databaseUrl = this.buildTenantDbUrl(tenant);
@@ -1262,9 +988,7 @@ var TenantDatabaseService = class _TenantDatabaseService {
1262
988
  throw new InternalServerErrorException2("Failed to connect to tenant database");
1263
989
  }
1264
990
  }
1265
- /**
1266
- * Build connection URL for tenant (dedicated database)
1267
- */
991
+ // Builds the PostgreSQL connection URL for a dedicated tenant database
1268
992
  buildTenantDbUrl(tenant) {
1269
993
  const { databaseHost, databasePort, databaseName, databaseUsername, databasePassword, databaseSslMode } = tenant;
1270
994
  if (!databaseHost || !databaseName || !databaseUsername) {
@@ -1276,15 +1000,11 @@ var TenantDatabaseService = class _TenantDatabaseService {
1276
1000
  this.logger.debug(`Tenant connection URL: ${this.maskPassword(connectionUrl)}`);
1277
1001
  return connectionUrl;
1278
1002
  }
1279
- /**
1280
- * Build cache key for connection pooling
1281
- */
1003
+ // Builds a cache key for connection pooling from tenant database coordinates
1282
1004
  buildCacheKey(tenant) {
1283
1005
  return `${tenant.type}:${tenant.databaseName}@${tenant.databaseHost}`;
1284
1006
  }
1285
- /**
1286
- * Start periodic cleanup of idle connections
1287
- */
1007
+ // Starts a periodic interval to close idle database connections
1288
1008
  startConnectionCleaner() {
1289
1009
  const interval = this.options.connectionCacheTTL || 3e5;
1290
1010
  this.cleanupInterval = setInterval(() => {
@@ -1292,9 +1012,7 @@ var TenantDatabaseService = class _TenantDatabaseService {
1292
1012
  }, interval);
1293
1013
  this.logger.log(`Connection cleanup scheduled every ${interval / 1e3} seconds`);
1294
1014
  }
1295
- /**
1296
- * Clean up idle connections that haven't been used recently
1297
- */
1015
+ // Closes and removes connections that have been idle beyond the TTL
1298
1016
  async cleanupIdleConnections() {
1299
1017
  const now = Date.now();
1300
1018
  const maxIdle = this.options.connectionCacheTTL || 3e5;
@@ -1319,18 +1037,14 @@ var TenantDatabaseService = class _TenantDatabaseService {
1319
1037
  this.logger.log(`Cleaned up ${cleaned} idle connections`);
1320
1038
  }
1321
1039
  }
1322
- /**
1323
- * Get current connection pool statistics
1324
- */
1040
+ // Returns the current number of active pooled connections and their tenant keys
1325
1041
  getPoolStats() {
1326
1042
  return {
1327
1043
  activeConnections: this.clients.size,
1328
1044
  tenants: Array.from(this.clients.keys())
1329
1045
  };
1330
1046
  }
1331
- /**
1332
- * Mask password in connection URL for logging
1333
- */
1047
+ // Masks password in connection URL for safe logging
1334
1048
  maskPassword(url) {
1335
1049
  return url.replace(/:([^@]+)@/, ":****@");
1336
1050
  }
@@ -1351,87 +1065,37 @@ var TenantDatabaseService = class _TenantDatabaseService {
1351
1065
  this.logger.log("All database connections closed");
1352
1066
  }
1353
1067
  };
1354
- TenantDatabaseService = _ts_decorate10([
1355
- Injectable8(),
1068
+ TenantDatabaseService = _ts_decorate8([
1069
+ Injectable6(),
1356
1070
  _ts_param3(0, Inject3(DATABASE_MODULE_OPTIONS)),
1357
- _ts_metadata7("design:type", Function),
1358
- _ts_metadata7("design:paramtypes", [
1071
+ _ts_metadata5("design:type", Function),
1072
+ _ts_metadata5("design:paramtypes", [
1359
1073
  typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions,
1360
1074
  typeof TenantContextService === "undefined" ? Object : TenantContextService
1361
1075
  ])
1362
1076
  ], TenantDatabaseService);
1363
1077
 
1364
1078
  // src/database/database.module.ts
1365
- function _ts_decorate11(decorators, target, key, desc) {
1079
+ function _ts_decorate9(decorators, target, key, desc) {
1366
1080
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1367
1081
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1368
1082
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1369
1083
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1370
1084
  }
1371
- __name(_ts_decorate11, "_ts_decorate");
1085
+ __name(_ts_decorate9, "_ts_decorate");
1372
1086
  var DatabaseModule = class _DatabaseModule {
1373
1087
  static {
1374
1088
  __name(this, "DatabaseModule");
1375
1089
  }
1376
- /**
1377
- * Configure DatabaseModule for Gateway/HTTP mode (multi-tenant web servers)
1378
- *
1379
- * This mode is for API Gateways that handle HTTP requests:
1380
- * - Automatically registers TenantContextInterceptor
1381
- * - Extracts tenant from subdomain or x-tenant-id header
1382
- * - Queries primary database for tenant configuration
1383
- * - Provides PrimaryDatabaseService for tenant lookup
1384
- *
1385
- * @param options Async configuration options
1386
- * @returns Dynamic module configuration with HTTP interceptor
1387
- *
1388
- * @example
1389
- * DatabaseModule.forServer({
1390
- * inject: [ConfigService],
1391
- * useFactory: (config: ConfigService) => ({
1392
- * primaryDb: {
1393
- * host: config.get('PRIMARY_DB_HOST'),
1394
- * port: config.get('PRIMARY_DB_PORT'),
1395
- * username: config.get('PRIMARY_DB_USERNAME'),
1396
- * password: config.get('PRIMARY_DB_PASSWORD'),
1397
- * database: config.get('PRIMARY_DB_DATABASE'),
1398
- * },
1399
- * prismaClientConstructor: PrismaClient,
1400
- * }),
1401
- * })
1402
- */
1090
+ // Configures the module for gateway/HTTP mode with TenantContextInterceptor
1403
1091
  static forServer(options) {
1404
1092
  return _DatabaseModule.createDynamicModule(options, "server");
1405
1093
  }
1406
- /**
1407
- * Configure DatabaseModule for Microservice/Messaging mode (RabbitMQ workers)
1408
- *
1409
- * This mode is for microservices that process messages from queues:
1410
- * - Automatically registers MessageTenantContextInterceptor
1411
- * - Extracts tenant from RabbitMQ message patterns
1412
- * - No primary database needed (tenant comes from message context)
1413
- *
1414
- * @param options Async configuration options
1415
- * @returns Dynamic module configuration with message interceptor
1416
- *
1417
- * @example
1418
- * DatabaseModule.forMicroservice({
1419
- * inject: [ConfigService],
1420
- * useFactory: (config: ConfigService) => ({
1421
- * prismaClientConstructor: PrismaClient,
1422
- * }),
1423
- * })
1424
- */
1094
+ // Configures the module for microservice mode with MessageTenantContextInterceptor
1425
1095
  static forMicroservice(options) {
1426
1096
  return _DatabaseModule.createDynamicModule(options, "microservice");
1427
1097
  }
1428
- /**
1429
- * Internal helper to create dynamic module with conditional interceptor registration
1430
- *
1431
- * @param options Configuration options
1432
- * @param mode Mode of operation (gateway or microservice)
1433
- * @returns Dynamic module configuration
1434
- */
1098
+ // Creates the dynamic module configuration with the appropriate interceptor for the given mode
1435
1099
  static createDynamicModule(options, mode) {
1436
1100
  const asyncProvider = {
1437
1101
  provide: DATABASE_MODULE_OPTIONS,
@@ -1441,25 +1105,14 @@ var DatabaseModule = class _DatabaseModule {
1441
1105
  const providers = [
1442
1106
  // Required for external packages - NestJS global Reflector not available
1443
1107
  {
1444
- provide: Reflector4,
1445
- useClass: Reflector4
1108
+ provide: Reflector3,
1109
+ useClass: Reflector3
1446
1110
  },
1447
1111
  asyncProvider,
1448
1112
  TenantContextService,
1449
1113
  PrimaryDatabaseService,
1450
1114
  TenantDatabaseService
1451
1115
  ];
1452
- if (mode === "server") {
1453
- providers.push({
1454
- provide: APP_INTERCEPTOR,
1455
- useClass: TenantContextInterceptor
1456
- });
1457
- } else {
1458
- providers.push({
1459
- provide: APP_INTERCEPTOR,
1460
- useClass: MessageTenantContextInterceptor
1461
- });
1462
- }
1463
1116
  return {
1464
1117
  module: _DatabaseModule,
1465
1118
  imports: [
@@ -1475,14 +1128,14 @@ var DatabaseModule = class _DatabaseModule {
1475
1128
  };
1476
1129
  }
1477
1130
  };
1478
- DatabaseModule = _ts_decorate11([
1131
+ DatabaseModule = _ts_decorate9([
1479
1132
  Global3(),
1480
1133
  Module3({})
1481
1134
  ], DatabaseModule);
1482
1135
 
1483
1136
  // src/database/decorators/tenant.decorator.ts
1484
- import { createParamDecorator as createParamDecorator2 } from "@nestjs/common";
1485
- var Tenant = createParamDecorator2((_data, ctx) => {
1137
+ import { createParamDecorator as createParamDecorator5 } from "@nestjs/common";
1138
+ var Tenant = createParamDecorator5((_data, ctx) => {
1486
1139
  const request = ctx.switchToHttp().getRequest();
1487
1140
  const tenantContext = request.app?.get?.(TenantContextService);
1488
1141
  if (!tenantContext) {
@@ -1491,9 +1144,118 @@ var Tenant = createParamDecorator2((_data, ctx) => {
1491
1144
  return tenantContext.getTenant();
1492
1145
  });
1493
1146
 
1147
+ // src/database/dto/select-options-query.dto.ts
1148
+ import { ApiPropertyOptional } from "@nestjs/swagger";
1149
+ import { Type } from "class-transformer";
1150
+ import { IsInt, IsOptional, IsString, Min } from "class-validator";
1151
+ function _ts_decorate10(decorators, target, key, desc) {
1152
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1153
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1154
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1155
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1156
+ }
1157
+ __name(_ts_decorate10, "_ts_decorate");
1158
+ function _ts_metadata6(k, v) {
1159
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1160
+ }
1161
+ __name(_ts_metadata6, "_ts_metadata");
1162
+ var SelectOptionsQueryDto = class {
1163
+ static {
1164
+ __name(this, "SelectOptionsQueryDto");
1165
+ }
1166
+ search;
1167
+ limit;
1168
+ offset;
1169
+ values;
1170
+ excludeIds;
1171
+ valueKey;
1172
+ labelKey;
1173
+ groupIdKey;
1174
+ };
1175
+ _ts_decorate10([
1176
+ ApiPropertyOptional({
1177
+ description: "Search term to filter by label",
1178
+ example: "united"
1179
+ }),
1180
+ IsOptional(),
1181
+ IsString(),
1182
+ _ts_metadata6("design:type", String)
1183
+ ], SelectOptionsQueryDto.prototype, "search", void 0);
1184
+ _ts_decorate10([
1185
+ ApiPropertyOptional({
1186
+ description: "Maximum number of results",
1187
+ example: 20,
1188
+ default: 20
1189
+ }),
1190
+ IsOptional(),
1191
+ Type(() => Number),
1192
+ IsInt(),
1193
+ Min(1),
1194
+ _ts_metadata6("design:type", Number)
1195
+ ], SelectOptionsQueryDto.prototype, "limit", void 0);
1196
+ _ts_decorate10([
1197
+ ApiPropertyOptional({
1198
+ description: "Number of results to skip",
1199
+ example: 0,
1200
+ default: 0
1201
+ }),
1202
+ IsOptional(),
1203
+ Type(() => Number),
1204
+ IsInt(),
1205
+ Min(0),
1206
+ _ts_metadata6("design:type", Number)
1207
+ ], SelectOptionsQueryDto.prototype, "offset", void 0);
1208
+ _ts_decorate10([
1209
+ ApiPropertyOptional({
1210
+ description: "Comma-separated values to fetch specific options",
1211
+ example: "1,2,3"
1212
+ }),
1213
+ IsOptional(),
1214
+ IsString(),
1215
+ _ts_metadata6("design:type", String)
1216
+ ], SelectOptionsQueryDto.prototype, "values", void 0);
1217
+ _ts_decorate10([
1218
+ ApiPropertyOptional({
1219
+ description: "Comma-separated IDs to exclude from results (already selected)",
1220
+ example: "5,10"
1221
+ }),
1222
+ IsOptional(),
1223
+ IsString(),
1224
+ _ts_metadata6("design:type", String)
1225
+ ], SelectOptionsQueryDto.prototype, "excludeIds", void 0);
1226
+ _ts_decorate10([
1227
+ ApiPropertyOptional({
1228
+ description: "Column name for option value",
1229
+ example: "id",
1230
+ default: "id"
1231
+ }),
1232
+ IsOptional(),
1233
+ IsString(),
1234
+ _ts_metadata6("design:type", String)
1235
+ ], SelectOptionsQueryDto.prototype, "valueKey", void 0);
1236
+ _ts_decorate10([
1237
+ ApiPropertyOptional({
1238
+ description: "Column name for option label",
1239
+ example: "name",
1240
+ default: "name"
1241
+ }),
1242
+ IsOptional(),
1243
+ IsString(),
1244
+ _ts_metadata6("design:type", String)
1245
+ ], SelectOptionsQueryDto.prototype, "labelKey", void 0);
1246
+ _ts_decorate10([
1247
+ ApiPropertyOptional({
1248
+ description: "Column name for group ID",
1249
+ example: "regionId"
1250
+ }),
1251
+ IsOptional(),
1252
+ IsString(),
1253
+ _ts_metadata6("design:type", String)
1254
+ ], SelectOptionsQueryDto.prototype, "groupIdKey", void 0);
1255
+
1494
1256
  // src/database/repositories/primary-base.repository.ts
1495
- import { Logger as Logger8 } from "@nestjs/common";
1496
- import { eq as eq2, getTableName, sql } from "drizzle-orm";
1257
+ import { Logger as Logger6 } from "@nestjs/common";
1258
+ import { and, asc, eq as eq2, getTableName, ilike, inArray, notInArray, sql } from "drizzle-orm";
1497
1259
  function snakeToCamel(str) {
1498
1260
  return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1499
1261
  }
@@ -1505,34 +1267,10 @@ var PrimaryBaseRepository = class {
1505
1267
  database;
1506
1268
  table;
1507
1269
  logger;
1508
- /**
1509
- * The table name extracted from the Drizzle table at runtime.
1510
- * Stored in camelCase to match Drizzle's query object keys.
1511
- * Example: 'email_verifications' -> 'emailVerifications'
1512
- */
1513
1270
  tableName;
1514
- /**
1515
- * Lazy getter for Drizzle client.
1516
- * Accesses the client from the database service only when needed,
1517
- * avoiding initialization timing issues with NestJS lifecycle.
1518
- */
1519
1271
  get db() {
1520
1272
  return this.database.drizzleClient;
1521
1273
  }
1522
- /**
1523
- * Model query API for THIS repository's table (Drizzle v2 relational queries)
1524
- * Scoped to only the table this repository manages.
1525
- * Returns a type-safe wrapper around Drizzle's RelationalQueryBuilder.
1526
- *
1527
- * @example
1528
- * ```typescript
1529
- * // Use relational queries with v2 object-based where syntax
1530
- * const user = await this.model.findFirst({
1531
- * where: { id },
1532
- * with: { posts: true, profile: true }
1533
- * });
1534
- * ```
1535
- */
1536
1274
  get model() {
1537
1275
  const query = this.database.drizzleClient.query;
1538
1276
  const queryKeys = Object.keys(query || {});
@@ -1543,60 +1281,24 @@ var PrimaryBaseRepository = class {
1543
1281
  }
1544
1282
  return model;
1545
1283
  }
1546
- /**
1547
- * Create a new repository instance
1548
- *
1549
- * @param database - The primary database service
1550
- * @param table - The Drizzle table schema object
1551
- *
1552
- * @example
1553
- * ```typescript
1554
- * import { users } from '@/db/schema';
1555
- *
1556
- * constructor(database: PrimaryDatabaseService) {
1557
- * super(database, users);
1558
- * }
1559
- * ```
1560
- */
1561
1284
  constructor(database, table) {
1562
1285
  this.database = database;
1563
1286
  this.table = table;
1564
1287
  const dbTableName = getTableName(table);
1565
1288
  this.tableName = snakeToCamel(dbTableName);
1566
- this.logger = new Logger8(this.constructor.name);
1289
+ this.logger = new Logger6(this.constructor.name);
1567
1290
  this.logger.debug(`Initialized ${this.constructor.name}`);
1568
1291
  this.logger.debug(`Table name: '${dbTableName}' -> query key: '${this.tableName}'`);
1569
1292
  }
1570
- /**
1571
- * Create a new record
1572
- *
1573
- * @param data - The data to create the record with
1574
- * @returns Promise resolving to the created record
1575
- *
1576
- * @example
1577
- * ```typescript
1578
- * const user = await userRepository.create({
1579
- * email: 'user@example.com',
1580
- * firstName: 'John'
1581
- * });
1582
- * ```
1583
- */
1293
+ // Creates a new record and returns it
1584
1294
  async create(data) {
1585
1295
  this.logger.log("Creating record");
1586
1296
  const results = await this.db.insert(this.table).values(data).returning();
1587
- return results[0];
1588
- }
1589
- /**
1590
- * Find a single record by ID
1591
- *
1592
- * @param id - The record ID
1593
- * @returns Promise resolving to the record or undefined if not found
1594
- *
1595
- * @example
1596
- * ```typescript
1597
- * const user = await userRepository.findById('user-id-123');
1598
- * ```
1599
- */
1297
+ const record = results[0];
1298
+ if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
1299
+ return record;
1300
+ }
1301
+ // Finds a single record by primary key ID
1600
1302
  async findById(id) {
1601
1303
  this.logger.debug(`Finding record by ID: ${id}`);
1602
1304
  return this.model.findFirst({
@@ -1605,105 +1307,29 @@ var PrimaryBaseRepository = class {
1605
1307
  }
1606
1308
  });
1607
1309
  }
1608
- /**
1609
- * Find a single record with custom where clause (Drizzle v2 object-based syntax)
1610
- *
1611
- * @param where - Object-based filter condition
1612
- * @returns Promise resolving to the record or undefined if not found
1613
- *
1614
- * @example
1615
- * ```typescript
1616
- * // Simple equality
1617
- * const user = await userRepository.findOne({ email: 'user@example.com' });
1618
- *
1619
- * // With operators
1620
- * const user = await userRepository.findOne({ age: { gte: 18 } });
1621
- *
1622
- * // Multiple conditions (AND)
1623
- * const user = await userRepository.findOne({
1624
- * email: 'user@example.com',
1625
- * status: 'ACTIVE'
1626
- * });
1627
- * ```
1628
- */
1310
+ // Finds a single record matching the given where filter
1629
1311
  async findOne(where) {
1630
1312
  this.logger.debug("Finding record with custom query");
1631
1313
  return this.model.findFirst({
1632
1314
  where
1633
1315
  });
1634
1316
  }
1635
- /**
1636
- * Find multiple records (Drizzle v2 object-based syntax)
1637
- *
1638
- * @param options - Query options (where, orderBy, limit, offset)
1639
- * @returns Promise resolving to an array of records
1640
- *
1641
- * @example
1642
- * ```typescript
1643
- * // Find all users
1644
- * const users = await userRepository.findMany();
1645
- *
1646
- * // Find with filtering and pagination (v2 object syntax)
1647
- * const users = await userRepository.findMany({
1648
- * where: { accountStatus: 'ACTIVE' },
1649
- * orderBy: { createdAt: 'desc' },
1650
- * limit: 10,
1651
- * offset: 0
1652
- * });
1653
- *
1654
- * // Multiple conditions
1655
- * const users = await userRepository.findMany({
1656
- * where: {
1657
- * AND: [
1658
- * { status: 'ACTIVE' },
1659
- * { age: { gte: 18 } }
1660
- * ]
1661
- * }
1662
- * });
1663
- * ```
1664
- */
1317
+ // Finds multiple records with optional filtering, ordering, and pagination
1665
1318
  async findMany(options) {
1666
1319
  this.logger.debug("Finding multiple records");
1667
1320
  return this.model.findMany(options);
1668
1321
  }
1669
- /**
1670
- * Update a record by ID
1671
- *
1672
- * @param id - The record ID
1673
- * @param data - The data to update
1674
- * @returns Promise resolving to the updated record
1675
- *
1676
- * @example
1677
- * ```typescript
1678
- * const user = await userRepository.update('user-id-123', {
1679
- * firstName: 'Jane'
1680
- * });
1681
- * ```
1682
- */
1322
+ // Updates a record by ID and returns the updated record
1683
1323
  async update(id, data) {
1684
1324
  this.logger.log(`Updating record with ID: ${id}`);
1685
1325
  const idColumn = this.table.id;
1326
+ if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
1686
1327
  const results = await this.db.update(this.table).set(data).where(eq2(idColumn, id)).returning();
1687
- return results[0];
1688
- }
1689
- /**
1690
- * Update multiple records
1691
- *
1692
- * @param where - SQL condition to match records
1693
- * @param data - The data to update
1694
- * @returns Promise resolving to the count of updated records
1695
- *
1696
- * @example
1697
- * ```typescript
1698
- * import { eq } from 'drizzle-orm';
1699
- *
1700
- * const result = await userRepository.updateMany(
1701
- * eq(users.accountStatus, 'PENDING'),
1702
- * { accountStatus: 'ACTIVE' }
1703
- * );
1704
- * console.log(`Updated ${result.count} users`);
1705
- * ```
1706
- */
1328
+ const record = results[0];
1329
+ if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
1330
+ return record;
1331
+ }
1332
+ // Updates all records matching the SQL condition and returns the affected count
1707
1333
  async updateMany(where, data) {
1708
1334
  this.logger.log("Updating multiple records");
1709
1335
  const result = await this.db.update(this.table).set(data).where(where);
@@ -1711,39 +1337,17 @@ var PrimaryBaseRepository = class {
1711
1337
  count: result.rowCount ?? 0
1712
1338
  };
1713
1339
  }
1714
- /**
1715
- * Delete a record by ID
1716
- *
1717
- * @param id - The record ID
1718
- * @returns Promise resolving to the deleted record
1719
- *
1720
- * @example
1721
- * ```typescript
1722
- * const user = await userRepository.delete('user-id-123');
1723
- * ```
1724
- */
1340
+ // Deletes a record by ID and returns the deleted record
1725
1341
  async delete(id) {
1726
1342
  this.logger.log(`Deleting record with ID: ${id}`);
1727
1343
  const idColumn = this.table.id;
1344
+ if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
1728
1345
  const results = await this.db.delete(this.table).where(eq2(idColumn, id)).returning();
1729
- return results[0];
1730
- }
1731
- /**
1732
- * Delete multiple records
1733
- *
1734
- * @param where - SQL condition to match records
1735
- * @returns Promise resolving to the count of deleted records
1736
- *
1737
- * @example
1738
- * ```typescript
1739
- * import { lt } from 'drizzle-orm';
1740
- *
1741
- * const result = await userRepository.deleteMany(
1742
- * lt(users.createdAt, new Date('2020-01-01'))
1743
- * );
1744
- * console.log(`Deleted ${result.count} users`);
1745
- * ```
1746
- */
1346
+ const record = results[0];
1347
+ if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
1348
+ return record;
1349
+ }
1350
+ // Deletes all records matching the SQL condition and returns the affected count
1747
1351
  async deleteMany(where) {
1748
1352
  this.logger.log("Deleting multiple records");
1749
1353
  const result = await this.db.delete(this.table).where(where);
@@ -1751,25 +1355,7 @@ var PrimaryBaseRepository = class {
1751
1355
  count: result.rowCount ?? 0
1752
1356
  };
1753
1357
  }
1754
- /**
1755
- * Count records
1756
- *
1757
- * @param where - Optional SQL condition to filter records
1758
- * @returns Promise resolving to the count of records
1759
- *
1760
- * @example
1761
- * ```typescript
1762
- * import { eq } from 'drizzle-orm';
1763
- *
1764
- * // Count all users
1765
- * const total = await userRepository.count();
1766
- *
1767
- * // Count active users
1768
- * const activeCount = await userRepository.count(
1769
- * eq(users.accountStatus, 'ACTIVE')
1770
- * );
1771
- * ```
1772
- */
1358
+ // Counts records matching the optional SQL condition
1773
1359
  async count(where) {
1774
1360
  this.logger.debug("Counting records");
1775
1361
  let query = this.db.select({
@@ -1781,30 +1367,125 @@ var PrimaryBaseRepository = class {
1781
1367
  const results = await query;
1782
1368
  return results[0].count;
1783
1369
  }
1784
- /**
1785
- * Check if a record exists
1786
- *
1787
- * @param where - SQL condition to match records
1788
- * @returns Promise resolving to true if at least one record exists, false otherwise
1789
- *
1790
- * @example
1791
- * ```typescript
1792
- * import { eq } from 'drizzle-orm';
1793
- *
1794
- * const emailExists = await userRepository.exists(
1795
- * eq(users.email, 'user@example.com')
1796
- * );
1797
- * ```
1798
- */
1370
+ // Returns true if at least one record matches the SQL condition
1799
1371
  async exists(where) {
1800
1372
  const count = await this.count(where);
1801
1373
  return count > 0;
1802
1374
  }
1375
+ // Finds records formatted as select dropdown options with optional search, pagination, and grouping
1376
+ async findForSelect(config) {
1377
+ this.logger.debug("Finding records for select dropdown");
1378
+ const parsedValues = typeof config.values === "string" ? config.values.split(",").map((v) => v.trim()).filter(Boolean) : config.values;
1379
+ const parsedExcludeIds = typeof config.excludeIds === "string" ? config.excludeIds.split(",").map((v) => v.trim()).filter(Boolean) : config.excludeIds ?? [];
1380
+ const tableColumns = this.table;
1381
+ const valueCol = tableColumns[config.value];
1382
+ if (!valueCol) throw new Error(`Column '${config.value}' not found in table '${this.tableName}'`);
1383
+ const labelCol = tableColumns[config.label];
1384
+ if (!labelCol) throw new Error(`Column '${config.label}' not found in table '${this.tableName}'`);
1385
+ if (parsedValues && parsedValues.length > 0) {
1386
+ const selectCols = {
1387
+ value: valueCol,
1388
+ label: labelCol
1389
+ };
1390
+ if (config.groupId) {
1391
+ const groupIdCol = tableColumns[config.groupId];
1392
+ if (groupIdCol) selectCols.groupId = groupIdCol;
1393
+ }
1394
+ const rows2 = await this.db.select(selectCols).from(this.table).where(inArray(valueCol, parsedValues));
1395
+ return {
1396
+ options: rows2.map((row) => ({
1397
+ value: row.value,
1398
+ label: String(row.label),
1399
+ ...config.groupId && row.groupId != null ? {
1400
+ groupId: row.groupId
1401
+ } : {}
1402
+ })),
1403
+ hasMore: false,
1404
+ ...config.groups ? {
1405
+ groups: config.groups
1406
+ } : {}
1407
+ };
1408
+ }
1409
+ const selectFields = {
1410
+ value: valueCol,
1411
+ label: labelCol,
1412
+ totalCount: sql`count(*) over()`.mapWith(Number)
1413
+ };
1414
+ if (config.groupId) {
1415
+ const groupIdCol = tableColumns[config.groupId];
1416
+ if (groupIdCol) selectFields.groupId = groupIdCol;
1417
+ }
1418
+ const conditions = [];
1419
+ if (config.search) {
1420
+ conditions.push(ilike(labelCol, `%${config.search}%`));
1421
+ }
1422
+ if (parsedExcludeIds.length > 0) {
1423
+ conditions.push(notInArray(valueCol, parsedExcludeIds));
1424
+ }
1425
+ if (config.where) {
1426
+ for (const [field, val] of Object.entries(config.where)) {
1427
+ const column = tableColumns[field];
1428
+ if (column) {
1429
+ conditions.push(eq2(column, val));
1430
+ }
1431
+ }
1432
+ }
1433
+ const orderByKey = config.orderBy ? Object.keys(config.orderBy)[0] : void 0;
1434
+ const orderByCol = orderByKey ? tableColumns[orderByKey] ?? labelCol : labelCol;
1435
+ const limit = Number(config.limit) || 20;
1436
+ const offset = Number(config.offset) || 0;
1437
+ let query = this.db.select(selectFields).from(this.table).$dynamic();
1438
+ if (conditions.length > 0) {
1439
+ query = query.where(conditions.length === 1 ? conditions[0] : and(...conditions));
1440
+ }
1441
+ const orderClauses = [];
1442
+ if (config.groupId) {
1443
+ const groupIdCol = tableColumns[config.groupId];
1444
+ if (groupIdCol) orderClauses.push(asc(groupIdCol));
1445
+ }
1446
+ orderClauses.push(asc(orderByCol));
1447
+ query = query.orderBy(...orderClauses).limit(limit).offset(offset);
1448
+ const rows = await query;
1449
+ const totalCount = rows.length > 0 ? rows[0].totalCount : 0;
1450
+ const options = rows.map((row) => ({
1451
+ value: row.value,
1452
+ label: String(row.label),
1453
+ ...config.groupId && row.groupId != null ? {
1454
+ groupId: row.groupId
1455
+ } : {}
1456
+ }));
1457
+ let resolvedGroups = config.groups;
1458
+ if (config.groupTable && config.groupId) {
1459
+ const groupTableColumns = config.groupTable;
1460
+ const groupIdKey = config.groupIdKey ?? "id";
1461
+ const groupNameKey = config.groupLabelKey ?? "name";
1462
+ const groupIdCol = groupTableColumns[groupIdKey];
1463
+ if (!groupIdCol) throw new Error(`Column '${groupIdKey}' not found in group table`);
1464
+ const groupNameCol = groupTableColumns[groupNameKey];
1465
+ if (!groupNameCol) throw new Error(`Column '${groupNameKey}' not found in group table`);
1466
+ const groupRows = await this.db.select({
1467
+ id: groupIdCol,
1468
+ name: groupNameCol
1469
+ }).from(config.groupTable).orderBy(asc(groupNameCol));
1470
+ resolvedGroups = groupRows.map((r) => ({
1471
+ id: r.id,
1472
+ name: String(r.name)
1473
+ }));
1474
+ }
1475
+ return {
1476
+ options,
1477
+ hasMore: offset + limit < totalCount,
1478
+ totalCount,
1479
+ ...resolvedGroups ? {
1480
+ groups: resolvedGroups
1481
+ } : {}
1482
+ };
1483
+ }
1803
1484
  };
1804
1485
 
1805
1486
  // src/database/repositories/tenant-base.repository.ts
1806
- import { Logger as Logger9 } from "@nestjs/common";
1807
- import { eq as eq3, getTableName as getTableName2, sql as sql2 } from "drizzle-orm";
1487
+ import { Logger as Logger7 } from "@nestjs/common";
1488
+ import { and as and2, asc as asc2, eq as eq3, getTableName as getTableName2, ilike as ilike2, inArray as inArray2, notInArray as notInArray2, sql as sql2 } from "drizzle-orm";
1808
1489
  var TenantBaseRepository = class {
1809
1490
  static {
1810
1491
  __name(this, "TenantBaseRepository");
@@ -1812,133 +1493,43 @@ var TenantBaseRepository = class {
1812
1493
  database;
1813
1494
  table;
1814
1495
  logger;
1815
- /**
1816
- * The table name extracted from the Drizzle table at runtime.
1817
- * Used to access the query API for this repository's table.
1818
- */
1819
1496
  tableName;
1820
- /**
1821
- * Lazy getter for Drizzle client.
1822
- * Accesses the client from the database service only when needed,
1823
- * avoiding initialization timing issues with NestJS lifecycle.
1824
- */
1825
1497
  get db() {
1826
1498
  return this.database.drizzleClient;
1827
1499
  }
1828
- /**
1829
- * Model query API for THIS repository's table (Prisma-like syntax)
1830
- * Scoped to only the table this repository manages
1831
- *
1832
- * @example
1833
- * ```typescript
1834
- * // Use relational queries with type safety
1835
- * const product = await this.model.findFirst({
1836
- * where: eq(products.id, id),
1837
- * with: { category: true, variants: true }
1838
- * });
1839
- * ```
1840
- */
1841
1500
  get model() {
1842
1501
  return this.database.drizzleClient.query[this.tableName];
1843
1502
  }
1844
- /**
1845
- * Create a new repository instance
1846
- *
1847
- * @param database - The tenant database service
1848
- * @param table - The Drizzle table schema object
1849
- *
1850
- * @example
1851
- * ```typescript
1852
- * import { products } from '@/db/schema';
1853
- *
1854
- * constructor(database: TenantDatabaseService) {
1855
- * super(database, products);
1856
- * }
1857
- * ```
1858
- */
1859
1503
  constructor(database, table) {
1860
1504
  this.database = database;
1861
1505
  this.table = table;
1862
1506
  this.tableName = getTableName2(table);
1863
- this.logger = new Logger9(this.constructor.name);
1507
+ this.logger = new Logger7(this.constructor.name);
1864
1508
  this.logger.debug(`Initialized ${this.constructor.name}`);
1865
1509
  }
1866
- /**
1867
- * Create a new record
1868
- *
1869
- * @param data - The data to create the record with
1870
- * @returns Promise resolving to the created record
1871
- *
1872
- * @example
1873
- * ```typescript
1874
- * const product = await productRepository.create({
1875
- * name: 'Widget',
1876
- * sku: 'WDG-001',
1877
- * price: 9.99
1878
- * });
1879
- * ```
1880
- */
1510
+ // Creates a new record and returns it
1881
1511
  async create(data) {
1882
1512
  this.logger.log("Creating record");
1883
1513
  const results = await this.db.insert(this.table).values(data).returning();
1884
- return results[0];
1885
- }
1886
- /**
1887
- * Find a single record by ID
1888
- *
1889
- * @param id - The record ID
1890
- * @returns Promise resolving to the record or null if not found
1891
- *
1892
- * @example
1893
- * ```typescript
1894
- * const product = await productRepository.findById('product-id-123');
1895
- * ```
1896
- */
1514
+ const record = results[0];
1515
+ if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
1516
+ return record;
1517
+ }
1518
+ // Finds a single record by primary key ID
1897
1519
  async findById(id) {
1898
1520
  this.logger.debug(`Finding record by ID: ${id}`);
1899
1521
  const idColumn = this.table.id;
1522
+ if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
1900
1523
  const results = await this.db.select().from(this.table).where(eq3(idColumn, id)).limit(1);
1901
1524
  return results[0] ?? null;
1902
1525
  }
1903
- /**
1904
- * Find a single record with custom where clause
1905
- *
1906
- * @param where - SQL condition
1907
- * @returns Promise resolving to the record or null if not found
1908
- *
1909
- * @example
1910
- * ```typescript
1911
- * import { eq } from 'drizzle-orm';
1912
- * const product = await productRepository.findOne(eq(products.sku, 'WDG-001'));
1913
- * ```
1914
- */
1526
+ // Finds a single record matching the given SQL condition
1915
1527
  async findOne(where) {
1916
1528
  this.logger.debug("Finding record with custom query");
1917
1529
  const results = await this.db.select().from(this.table).where(where).limit(1);
1918
1530
  return results[0] ?? null;
1919
1531
  }
1920
- /**
1921
- * Find multiple records
1922
- *
1923
- * @param options - Query options (where, orderBy, limit, offset)
1924
- * @returns Promise resolving to an array of records
1925
- *
1926
- * @example
1927
- * ```typescript
1928
- * import { eq, desc } from 'drizzle-orm';
1929
- *
1930
- * // Find all products
1931
- * const products = await productRepository.findMany();
1932
- *
1933
- * // Find with filtering and pagination
1934
- * const products = await productRepository.findMany({
1935
- * where: eq(products.status, 'ACTIVE'),
1936
- * orderBy: desc(products.createdAt),
1937
- * limit: 10,
1938
- * offset: 0
1939
- * });
1940
- * ```
1941
- */
1532
+ // Finds multiple records with optional SQL filtering, ordering, and pagination
1942
1533
  async findMany(options) {
1943
1534
  this.logger.debug("Finding multiple records");
1944
1535
  let query = this.db.select().from(this.table).$dynamic();
@@ -1956,44 +1547,17 @@ var TenantBaseRepository = class {
1956
1547
  }
1957
1548
  return await query;
1958
1549
  }
1959
- /**
1960
- * Update a record by ID
1961
- *
1962
- * @param id - The record ID
1963
- * @param data - The data to update
1964
- * @returns Promise resolving to the updated record
1965
- *
1966
- * @example
1967
- * ```typescript
1968
- * const product = await productRepository.update('product-id-123', {
1969
- * price: 12.99
1970
- * });
1971
- * ```
1972
- */
1550
+ // Updates a record by ID and returns the updated record
1973
1551
  async update(id, data) {
1974
1552
  this.logger.log(`Updating record with ID: ${id}`);
1975
1553
  const idColumn = this.table.id;
1554
+ if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
1976
1555
  const results = await this.db.update(this.table).set(data).where(eq3(idColumn, id)).returning();
1977
- return results[0];
1978
- }
1979
- /**
1980
- * Update multiple records
1981
- *
1982
- * @param where - SQL condition to match records
1983
- * @param data - The data to update
1984
- * @returns Promise resolving to the count of updated records
1985
- *
1986
- * @example
1987
- * ```typescript
1988
- * import { eq } from 'drizzle-orm';
1989
- *
1990
- * const result = await productRepository.updateMany(
1991
- * eq(products.status, 'PENDING'),
1992
- * { status: 'ACTIVE' }
1993
- * );
1994
- * console.log(`Updated ${result.count} products`);
1995
- * ```
1996
- */
1556
+ const record = results[0];
1557
+ if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
1558
+ return record;
1559
+ }
1560
+ // Updates all records matching the SQL condition and returns the affected count
1997
1561
  async updateMany(where, data) {
1998
1562
  this.logger.log("Updating multiple records");
1999
1563
  const result = await this.db.update(this.table).set(data).where(where);
@@ -2001,39 +1565,17 @@ var TenantBaseRepository = class {
2001
1565
  count: result.rowCount ?? 0
2002
1566
  };
2003
1567
  }
2004
- /**
2005
- * Delete a record by ID
2006
- *
2007
- * @param id - The record ID
2008
- * @returns Promise resolving to the deleted record
2009
- *
2010
- * @example
2011
- * ```typescript
2012
- * const product = await productRepository.delete('product-id-123');
2013
- * ```
2014
- */
1568
+ // Deletes a record by ID and returns the deleted record
2015
1569
  async delete(id) {
2016
1570
  this.logger.log(`Deleting record with ID: ${id}`);
2017
1571
  const idColumn = this.table.id;
1572
+ if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
2018
1573
  const results = await this.db.delete(this.table).where(eq3(idColumn, id)).returning();
2019
- return results[0];
2020
- }
2021
- /**
2022
- * Delete multiple records
2023
- *
2024
- * @param where - SQL condition to match records
2025
- * @returns Promise resolving to the count of deleted records
2026
- *
2027
- * @example
2028
- * ```typescript
2029
- * import { lt } from 'drizzle-orm';
2030
- *
2031
- * const result = await productRepository.deleteMany(
2032
- * lt(products.createdAt, new Date('2020-01-01'))
2033
- * );
2034
- * console.log(`Deleted ${result.count} products`);
2035
- * ```
2036
- */
1574
+ const record = results[0];
1575
+ if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
1576
+ return record;
1577
+ }
1578
+ // Deletes all records matching the SQL condition and returns the affected count
2037
1579
  async deleteMany(where) {
2038
1580
  this.logger.log("Deleting multiple records");
2039
1581
  const result = await this.db.delete(this.table).where(where);
@@ -2041,25 +1583,7 @@ var TenantBaseRepository = class {
2041
1583
  count: result.rowCount ?? 0
2042
1584
  };
2043
1585
  }
2044
- /**
2045
- * Count records
2046
- *
2047
- * @param where - Optional SQL condition to filter records
2048
- * @returns Promise resolving to the count of records
2049
- *
2050
- * @example
2051
- * ```typescript
2052
- * import { eq } from 'drizzle-orm';
2053
- *
2054
- * // Count all products
2055
- * const total = await productRepository.count();
2056
- *
2057
- * // Count active products
2058
- * const activeCount = await productRepository.count(
2059
- * eq(products.status, 'ACTIVE')
2060
- * );
2061
- * ```
2062
- */
1586
+ // Counts records matching the optional SQL condition
2063
1587
  async count(where) {
2064
1588
  this.logger.debug("Counting records");
2065
1589
  let query = this.db.select({
@@ -2071,117 +1595,769 @@ var TenantBaseRepository = class {
2071
1595
  const results = await query;
2072
1596
  return results[0].count;
2073
1597
  }
2074
- /**
2075
- * Check if a record exists
2076
- *
2077
- * @param where - SQL condition to match records
2078
- * @returns Promise resolving to true if at least one record exists, false otherwise
2079
- *
2080
- * @example
2081
- * ```typescript
2082
- * import { eq } from 'drizzle-orm';
2083
- *
2084
- * const skuExists = await productRepository.exists(
2085
- * eq(products.sku, 'WDG-001')
2086
- * );
2087
- * ```
2088
- */
1598
+ // Returns true if at least one record matches the SQL condition
2089
1599
  async exists(where) {
2090
1600
  const count = await this.count(where);
2091
1601
  return count > 0;
2092
1602
  }
1603
+ // Finds records formatted as select dropdown options with optional search, pagination, and grouping
1604
+ async findForSelect(config) {
1605
+ this.logger.debug("Finding records for select dropdown");
1606
+ const parsedValues = typeof config.values === "string" ? config.values.split(",").map((v) => v.trim()).filter(Boolean) : config.values;
1607
+ const parsedExcludeIds = typeof config.excludeIds === "string" ? config.excludeIds.split(",").map((v) => v.trim()).filter(Boolean) : config.excludeIds ?? [];
1608
+ const tableColumns = this.table;
1609
+ const valueCol = tableColumns[config.value];
1610
+ if (!valueCol) throw new Error(`Column '${config.value}' not found in table '${this.tableName}'`);
1611
+ const labelCol = tableColumns[config.label];
1612
+ if (!labelCol) throw new Error(`Column '${config.label}' not found in table '${this.tableName}'`);
1613
+ if (parsedValues && parsedValues.length > 0) {
1614
+ const selectCols = {
1615
+ value: valueCol,
1616
+ label: labelCol
1617
+ };
1618
+ if (config.groupId) {
1619
+ const groupIdCol = tableColumns[config.groupId];
1620
+ if (groupIdCol) selectCols.groupId = groupIdCol;
1621
+ }
1622
+ const rows2 = await this.db.select(selectCols).from(this.table).where(inArray2(valueCol, parsedValues));
1623
+ return {
1624
+ options: rows2.map((row) => ({
1625
+ value: row.value,
1626
+ label: String(row.label),
1627
+ ...config.groupId && row.groupId != null ? {
1628
+ groupId: row.groupId
1629
+ } : {}
1630
+ })),
1631
+ hasMore: false,
1632
+ ...config.groups ? {
1633
+ groups: config.groups
1634
+ } : {}
1635
+ };
1636
+ }
1637
+ const selectFields = {
1638
+ value: valueCol,
1639
+ label: labelCol,
1640
+ totalCount: sql2`count(*) over()`.mapWith(Number)
1641
+ };
1642
+ if (config.groupId) {
1643
+ const groupIdCol = tableColumns[config.groupId];
1644
+ if (groupIdCol) selectFields.groupId = groupIdCol;
1645
+ }
1646
+ const conditions = [];
1647
+ if (config.search) {
1648
+ conditions.push(ilike2(labelCol, `%${config.search}%`));
1649
+ }
1650
+ if (parsedExcludeIds.length > 0) {
1651
+ conditions.push(notInArray2(valueCol, parsedExcludeIds));
1652
+ }
1653
+ if (config.where) {
1654
+ for (const [field, val] of Object.entries(config.where)) {
1655
+ const column = tableColumns[field];
1656
+ if (column) {
1657
+ conditions.push(eq3(column, val));
1658
+ }
1659
+ }
1660
+ }
1661
+ const orderByKey = config.orderBy ? Object.keys(config.orderBy)[0] : void 0;
1662
+ const orderByCol = orderByKey ? tableColumns[orderByKey] ?? labelCol : labelCol;
1663
+ const limit = Number(config.limit) || 20;
1664
+ const offset = Number(config.offset) || 0;
1665
+ let query = this.db.select(selectFields).from(this.table).$dynamic();
1666
+ if (conditions.length > 0) {
1667
+ query = query.where(conditions.length === 1 ? conditions[0] : and2(...conditions));
1668
+ }
1669
+ const orderClauses = [];
1670
+ if (config.groupId) {
1671
+ const groupIdCol = tableColumns[config.groupId];
1672
+ if (groupIdCol) orderClauses.push(asc2(groupIdCol));
1673
+ }
1674
+ orderClauses.push(asc2(orderByCol));
1675
+ query = query.orderBy(...orderClauses).limit(limit).offset(offset);
1676
+ const rows = await query;
1677
+ const totalCount = rows.length > 0 ? rows[0].totalCount : 0;
1678
+ const options = rows.map((row) => ({
1679
+ value: row.value,
1680
+ label: String(row.label),
1681
+ ...config.groupId && row.groupId != null ? {
1682
+ groupId: row.groupId
1683
+ } : {}
1684
+ }));
1685
+ let resolvedGroups = config.groups;
1686
+ if (config.groupTable && config.groupId) {
1687
+ const groupTableColumns = config.groupTable;
1688
+ const groupIdKey = config.groupIdKey ?? "id";
1689
+ const groupNameKey = config.groupLabelKey ?? "name";
1690
+ const groupIdCol = groupTableColumns[groupIdKey];
1691
+ if (!groupIdCol) throw new Error(`Column '${groupIdKey}' not found in group table`);
1692
+ const groupNameCol = groupTableColumns[groupNameKey];
1693
+ if (!groupNameCol) throw new Error(`Column '${groupNameKey}' not found in group table`);
1694
+ const groupRows = await this.db.select({
1695
+ id: groupIdCol,
1696
+ name: groupNameCol
1697
+ }).from(config.groupTable).orderBy(asc2(groupNameCol));
1698
+ resolvedGroups = groupRows.map((r) => ({
1699
+ id: r.id,
1700
+ name: String(r.name)
1701
+ }));
1702
+ }
1703
+ return {
1704
+ options,
1705
+ hasMore: offset + limit < totalCount,
1706
+ totalCount,
1707
+ ...resolvedGroups ? {
1708
+ groups: resolvedGroups
1709
+ } : {}
1710
+ };
1711
+ }
2093
1712
  };
2094
1713
 
2095
- // src/exceptions/bad-gateway.exception.ts
2096
- import { HttpStatus } from "@nestjs/common";
1714
+ // src/email/email.module.ts
1715
+ import { Global as Global4, Module as Module4 } from "@nestjs/common";
1716
+ import { ConfigModule as ConfigModule2 } from "@nestjs/config";
2097
1717
 
2098
- // src/exceptions/base-field.exception.ts
2099
- import { HttpException } from "@nestjs/common";
2100
- var HttpProblemException = class extends HttpException {
1718
+ // src/email/email.service.ts
1719
+ import { BrevoClient, BrevoError, BrevoTimeoutError } from "@getbrevo/brevo";
1720
+ import { Injectable as Injectable7, Logger as Logger8 } from "@nestjs/common";
1721
+ import { ConfigService as ConfigService4 } from "@nestjs/config";
1722
+ function _ts_decorate11(decorators, target, key, desc) {
1723
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1724
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1725
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1726
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1727
+ }
1728
+ __name(_ts_decorate11, "_ts_decorate");
1729
+ function _ts_metadata7(k, v) {
1730
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1731
+ }
1732
+ __name(_ts_metadata7, "_ts_metadata");
1733
+ var EmailService = class _EmailService {
2101
1734
  static {
2102
- __name(this, "HttpProblemException");
2103
- }
2104
- constructor(detailOrOptions, httpStatus) {
2105
- const options = typeof detailOrOptions === "string" ? {
2106
- detail: detailOrOptions
2107
- } : detailOrOptions;
2108
- super({
2109
- type: options.type ?? "about:blank",
2110
- label: options.label,
2111
- detail: options.detail,
2112
- errors: options.errors ?? []
2113
- }, httpStatus);
2114
- }
2115
- };
1735
+ __name(this, "EmailService");
1736
+ }
1737
+ configService;
1738
+ logger = new Logger8(_EmailService.name);
1739
+ brevoClient;
1740
+ senderEmail;
1741
+ senderName;
1742
+ constructor(configService) {
1743
+ this.configService = configService;
1744
+ const apiKey = this.configService.get("BREVO_API_KEY");
1745
+ if (!apiKey) {
1746
+ this.logger.error("BREVO_API_KEY is not configured. Email sending will fail.");
1747
+ throw new Error("Email service configuration error: Missing BREVO_API_KEY");
1748
+ }
1749
+ this.brevoClient = new BrevoClient({
1750
+ apiKey,
1751
+ maxRetries: 3
1752
+ });
1753
+ const senderEmail = this.configService.get("SENDER_EMAIL");
1754
+ const senderName = this.configService.get("SENDER_NAME");
1755
+ if (!senderEmail || !senderName) {
1756
+ this.logger.error("Sender email or name is not configured.");
1757
+ throw new Error("Email service configuration error: Missing SENDER_EMAIL or SENDER_NAME");
1758
+ }
1759
+ this.senderEmail = senderEmail;
1760
+ this.senderName = senderName;
1761
+ this.logger.log("Brevo email service initialized successfully");
1762
+ }
1763
+ // Sends an email verification OTP to the given recipient
1764
+ async sendVerificationEmail(email, otp, expiresAt, displayName) {
1765
+ const name = displayName || "there";
1766
+ const expiryMinutes = Math.ceil((expiresAt.getTime() - Date.now()) / 6e4);
1767
+ const subject = "Verify Your Email - Vritti AI Cloud";
1768
+ const htmlContent = `
1769
+ <!DOCTYPE html>
1770
+ <html>
1771
+ <head>
1772
+ <meta charset="UTF-8">
1773
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1774
+ </head>
1775
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
1776
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
1777
+ <tr>
1778
+ <td style="padding: 40px 20px;">
1779
+ <table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
1780
+ <!-- Header -->
1781
+ <tr>
1782
+ <td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
1783
+ <h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Email Verification</h1>
1784
+ </td>
1785
+ </tr>
2116
1786
 
2117
- // src/exceptions/bad-gateway.exception.ts
2118
- var BadGatewayException = class extends HttpProblemException {
2119
- static {
2120
- __name(this, "BadGatewayException");
2121
- }
2122
- constructor(detailOrOptions) {
2123
- super(detailOrOptions ?? "Bad Gateway", HttpStatus.BAD_GATEWAY);
2124
- }
2125
- };
1787
+ <!-- Content -->
1788
+ <tr>
1789
+ <td style="padding: 40px;">
1790
+ <p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
1791
+ Hello <strong>${name}</strong>,
1792
+ </p>
1793
+ <p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
1794
+ Thank you for signing up with Vritti AI Cloud. Please use the following verification code to complete your registration:
1795
+ </p>
2126
1796
 
2127
- // src/exceptions/bad-request.exception.ts
2128
- import { HttpStatus as HttpStatus2 } from "@nestjs/common";
2129
- var BadRequestException = class extends HttpProblemException {
2130
- static {
2131
- __name(this, "BadRequestException");
2132
- }
2133
- constructor(detailOrOptions) {
2134
- super(detailOrOptions ?? "Bad Request", HttpStatus2.BAD_REQUEST);
2135
- }
2136
- };
1797
+ <!-- OTP Box -->
1798
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 8px; text-align: center; margin: 30px 0;">
1799
+ <div style="color: #ffffff; font-size: 36px; font-weight: bold; letter-spacing: 10px; font-family: 'Courier New', monospace;">
1800
+ ${otp}
1801
+ </div>
1802
+ </div>
2137
1803
 
2138
- // src/exceptions/conflict.exception.ts
2139
- import { HttpStatus as HttpStatus3 } from "@nestjs/common";
2140
- var ConflictException = class extends HttpProblemException {
2141
- static {
2142
- __name(this, "ConflictException");
2143
- }
2144
- constructor(detailOrOptions) {
2145
- super(detailOrOptions ?? "Conflict", HttpStatus3.CONFLICT);
2146
- }
2147
- };
1804
+ <p style="margin: 30px 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
1805
+ <strong>Important:</strong> This code will expire in <strong>${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}</strong>.
1806
+ </p>
1807
+ <p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6;">
1808
+ If you didn't request this verification, please ignore this email.
1809
+ </p>
1810
+ </td>
1811
+ </tr>
2148
1812
 
2149
- // src/exceptions/forbidden.exception.ts
2150
- import { HttpStatus as HttpStatus4 } from "@nestjs/common";
2151
- var ForbiddenException2 = class extends HttpProblemException {
2152
- static {
2153
- __name(this, "ForbiddenException");
2154
- }
2155
- constructor(detailOrOptions) {
2156
- super(detailOrOptions ?? "Forbidden", HttpStatus4.FORBIDDEN);
2157
- }
2158
- };
1813
+ <!-- Footer -->
1814
+ <tr>
1815
+ <td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
1816
+ <p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
1817
+ Vritti AI Cloud - Cloud Management Platform
1818
+ </p>
1819
+ <p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
1820
+ This is an automated message, please do not reply.
1821
+ </p>
1822
+ </td>
1823
+ </tr>
1824
+ </table>
1825
+ </td>
1826
+ </tr>
1827
+ </table>
1828
+ </body>
1829
+ </html>
1830
+ `;
1831
+ const textContent = `
1832
+ Hello ${name},
2159
1833
 
2160
- // src/exceptions/gone.exception.ts
2161
- import { HttpStatus as HttpStatus5 } from "@nestjs/common";
2162
- var GoneException = class extends HttpProblemException {
2163
- static {
2164
- __name(this, "GoneException");
2165
- }
2166
- constructor(detailOrOptions) {
2167
- super(detailOrOptions ?? "Gone", HttpStatus5.GONE);
2168
- }
2169
- };
1834
+ Thank you for signing up with Vritti AI Cloud. Please use the following verification code to complete your registration:
2170
1835
 
2171
- // src/exceptions/internal-server-error.exception.ts
2172
- import { HttpStatus as HttpStatus6 } from "@nestjs/common";
2173
- var InternalServerErrorException3 = class extends HttpProblemException {
2174
- static {
2175
- __name(this, "InternalServerErrorException");
2176
- }
2177
- constructor(detailOrOptions) {
2178
- super(detailOrOptions ?? "Internal Server Error", HttpStatus6.INTERNAL_SERVER_ERROR);
2179
- }
2180
- };
1836
+ Verification Code: ${otp}
2181
1837
 
2182
- // src/exceptions/method-not-allowed.exception.ts
2183
- import { HttpStatus as HttpStatus7 } from "@nestjs/common";
2184
- var MethodNotAllowedException = class extends HttpProblemException {
1838
+ This code will expire in ${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}.
1839
+
1840
+ If you didn't request this verification, please ignore this email.
1841
+
1842
+ ---
1843
+ Vritti AI Cloud - Cloud Management Platform
1844
+ This is an automated message, please do not reply.
1845
+ `.trim();
1846
+ await this.sendEmail({
1847
+ to: [
1848
+ {
1849
+ email,
1850
+ name
1851
+ }
1852
+ ],
1853
+ subject,
1854
+ htmlContent,
1855
+ textContent
1856
+ });
1857
+ this.logger.log(`Verification email sent to ${email}`);
1858
+ }
1859
+ // Sends a password reset OTP to the given recipient
1860
+ async sendPasswordResetEmail(email, otp, expiresAt, displayName) {
1861
+ const name = displayName || "there";
1862
+ const expiryMinutes = Math.ceil((expiresAt.getTime() - Date.now()) / 6e4);
1863
+ const subject = "Reset Your Password - Vritti AI Cloud";
1864
+ const htmlContent = `
1865
+ <!DOCTYPE html>
1866
+ <html>
1867
+ <head>
1868
+ <meta charset="UTF-8">
1869
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1870
+ </head>
1871
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
1872
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
1873
+ <tr>
1874
+ <td style="padding: 40px 20px;">
1875
+ <table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
1876
+ <!-- Header -->
1877
+ <tr>
1878
+ <td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
1879
+ <h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Password Reset</h1>
1880
+ </td>
1881
+ </tr>
1882
+
1883
+ <!-- Content -->
1884
+ <tr>
1885
+ <td style="padding: 40px;">
1886
+ <p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
1887
+ Hello <strong>${name}</strong>,
1888
+ </p>
1889
+ <p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
1890
+ We received a request to reset your password. Use the following code to complete the process:
1891
+ </p>
1892
+
1893
+ <!-- OTP Box -->
1894
+ <div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 30px; border-radius: 8px; text-align: center; margin: 30px 0;">
1895
+ <div style="color: #ffffff; font-size: 36px; font-weight: bold; letter-spacing: 10px; font-family: 'Courier New', monospace;">
1896
+ ${otp}
1897
+ </div>
1898
+ </div>
1899
+
1900
+ <p style="margin: 30px 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
1901
+ <strong>Important:</strong> This code will expire in <strong>${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}</strong>.
1902
+ </p>
1903
+ <p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
1904
+ If you didn't request a password reset, please ignore this email and your password will remain unchanged.
1905
+ </p>
1906
+ <div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-top: 20px; border-radius: 4px;">
1907
+ <p style="margin: 0; color: #856404; font-size: 13px; line-height: 1.5;">
1908
+ <strong>Security Tip:</strong> Never share this code with anyone. Vritti will never ask for your verification code.
1909
+ </p>
1910
+ </div>
1911
+ </td>
1912
+ </tr>
1913
+
1914
+ <!-- Footer -->
1915
+ <tr>
1916
+ <td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
1917
+ <p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
1918
+ Vritti AI Cloud - Cloud Management Platform
1919
+ </p>
1920
+ <p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
1921
+ This is an automated message, please do not reply.
1922
+ </p>
1923
+ </td>
1924
+ </tr>
1925
+ </table>
1926
+ </td>
1927
+ </tr>
1928
+ </table>
1929
+ </body>
1930
+ </html>
1931
+ `;
1932
+ const textContent = `
1933
+ Hello ${name},
1934
+
1935
+ We received a request to reset your password. Use the following code to complete the process:
1936
+
1937
+ Reset Code: ${otp}
1938
+
1939
+ This code will expire in ${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}.
1940
+
1941
+ If you didn't request a password reset, please ignore this email and your password will remain unchanged.
1942
+
1943
+ SECURITY TIP: Never share this code with anyone. Vritti will never ask for your verification code.
1944
+
1945
+ ---
1946
+ Vritti AI Cloud - Cloud Management Platform
1947
+ This is an automated message, please do not reply.
1948
+ `.trim();
1949
+ await this.sendEmail({
1950
+ to: [
1951
+ {
1952
+ email,
1953
+ name
1954
+ }
1955
+ ],
1956
+ subject,
1957
+ htmlContent,
1958
+ textContent
1959
+ });
1960
+ this.logger.log(`Password reset email sent to ${email}`);
1961
+ }
1962
+ // Sends an email change notification to the old address with a revert link
1963
+ async sendEmailChangeNotification(oldEmail, newEmail, revertToken, revertExpiresAt, displayName) {
1964
+ const name = displayName || "there";
1965
+ const subject = "Your Email Address Has Been Changed - Vritti AI Cloud";
1966
+ const hoursUntilExpiry = Math.floor((revertExpiresAt.getTime() - Date.now()) / (1e3 * 60 * 60));
1967
+ const revertLink = `https://local.vrittiai.com:3012/settings/profile/revert-email?token=${revertToken}`;
1968
+ const htmlContent = `
1969
+ <!DOCTYPE html>
1970
+ <html>
1971
+ <head>
1972
+ <meta charset="UTF-8">
1973
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1974
+ </head>
1975
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
1976
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
1977
+ <tr>
1978
+ <td style="padding: 40px 20px;">
1979
+ <table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
1980
+ <!-- Header -->
1981
+ <tr>
1982
+ <td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
1983
+ <h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Email Address Changed</h1>
1984
+ </td>
1985
+ </tr>
1986
+
1987
+ <!-- Content -->
1988
+ <tr>
1989
+ <td style="padding: 40px;">
1990
+ <p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
1991
+ Hello <strong>${name}</strong>,
1992
+ </p>
1993
+ <p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
1994
+ We're writing to inform you that your Vritti AI Cloud email address has been successfully changed.
1995
+ </p>
1996
+
1997
+ <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 30px 0;">
1998
+ <p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
1999
+ <strong>Previous Email:</strong>
2000
+ </p>
2001
+ <p style="margin: 0 0 20px; color: #333333; font-size: 16px; font-family: monospace;">
2002
+ ${oldEmail}
2003
+ </p>
2004
+ <p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
2005
+ <strong>New Email:</strong>
2006
+ </p>
2007
+ <p style="margin: 0; color: #333333; font-size: 16px; font-family: monospace;">
2008
+ ${newEmail}
2009
+ </p>
2010
+ </div>
2011
+
2012
+ <div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 20px; margin: 30px 0; border-radius: 4px;">
2013
+ <p style="margin: 0 0 15px; color: #856404; font-size: 14px; line-height: 1.6;">
2014
+ <strong>Didn't make this change?</strong>
2015
+ </p>
2016
+ <p style="margin: 0 0 20px; color: #856404; font-size: 14px; line-height: 1.6;">
2017
+ If you did not authorize this change, you can revert it within the next <strong>${hoursUntilExpiry} hours</strong> by clicking the button below:
2018
+ </p>
2019
+ <div style="text-align: center;">
2020
+ <a href="${revertLink}" style="display: inline-block; padding: 12px 30px; background-color: #dc3545; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 14px;">
2021
+ Revert Email Change
2022
+ </a>
2023
+ </div>
2024
+ </div>
2025
+
2026
+ <p style="margin: 30px 0 0; color: #666666; font-size: 14px; line-height: 1.6;">
2027
+ If you made this change, you can safely ignore this email.
2028
+ </p>
2029
+ </td>
2030
+ </tr>
2031
+
2032
+ <!-- Footer -->
2033
+ <tr>
2034
+ <td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
2035
+ <p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
2036
+ Vritti AI Cloud - Cloud Management Platform
2037
+ </p>
2038
+ <p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
2039
+ This is an automated message, please do not reply.
2040
+ </p>
2041
+ </td>
2042
+ </tr>
2043
+ </table>
2044
+ </td>
2045
+ </tr>
2046
+ </table>
2047
+ </body>
2048
+ </html>
2049
+ `;
2050
+ const textContent = `
2051
+ Hello ${name},
2052
+
2053
+ We're writing to inform you that your Vritti AI Cloud email address has been successfully changed.
2054
+
2055
+ Previous Email: ${oldEmail}
2056
+ New Email: ${newEmail}
2057
+
2058
+ DIDN'T MAKE THIS CHANGE?
2059
+
2060
+ If you did not authorize this change, you can revert it within the next ${hoursUntilExpiry} hours by visiting:
2061
+ ${revertLink}
2062
+
2063
+ If you made this change, you can safely ignore this email.
2064
+
2065
+ ---
2066
+ Vritti AI Cloud - Cloud Management Platform
2067
+ This is an automated message, please do not reply.
2068
+ `.trim();
2069
+ await this.sendEmail({
2070
+ to: [
2071
+ {
2072
+ email: oldEmail,
2073
+ name
2074
+ }
2075
+ ],
2076
+ subject,
2077
+ htmlContent,
2078
+ textContent
2079
+ });
2080
+ this.logger.log(`Email change notification sent to ${oldEmail}`);
2081
+ }
2082
+ // Sends a confirmation to the restored email address after a revert
2083
+ async sendEmailRevertConfirmation(email, displayName) {
2084
+ const name = displayName || "there";
2085
+ const subject = "Email Address Change Reverted - Vritti AI Cloud";
2086
+ const htmlContent = `
2087
+ <!DOCTYPE html>
2088
+ <html>
2089
+ <head>
2090
+ <meta charset="UTF-8">
2091
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2092
+ </head>
2093
+ <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
2094
+ <table role="presentation" style="width: 100%; border-collapse: collapse;">
2095
+ <tr>
2096
+ <td style="padding: 40px 20px;">
2097
+ <table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
2098
+ <!-- Header -->
2099
+ <tr>
2100
+ <td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
2101
+ <h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Email Change Reverted</h1>
2102
+ </td>
2103
+ </tr>
2104
+
2105
+ <!-- Content -->
2106
+ <tr>
2107
+ <td style="padding: 40px;">
2108
+ <p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
2109
+ Hello <strong>${name}</strong>,
2110
+ </p>
2111
+ <p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
2112
+ Your recent email address change has been successfully reverted. Your email is now:
2113
+ </p>
2114
+
2115
+ <div style="background-color: #d4edda; padding: 20px; border-radius: 8px; margin: 30px 0; text-align: center;">
2116
+ <p style="margin: 0; color: #155724; font-size: 18px; font-weight: 600; font-family: monospace;">
2117
+ ${email}
2118
+ </p>
2119
+ </div>
2120
+
2121
+ <p style="margin: 30px 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
2122
+ If you did not request this revert, please contact our support team immediately.
2123
+ </p>
2124
+ </td>
2125
+ </tr>
2126
+
2127
+ <!-- Footer -->
2128
+ <tr>
2129
+ <td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
2130
+ <p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
2131
+ Vritti AI Cloud - Cloud Management Platform
2132
+ </p>
2133
+ <p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
2134
+ This is an automated message, please do not reply.
2135
+ </p>
2136
+ </td>
2137
+ </tr>
2138
+ </table>
2139
+ </td>
2140
+ </tr>
2141
+ </table>
2142
+ </body>
2143
+ </html>
2144
+ `;
2145
+ const textContent = `
2146
+ Hello ${name},
2147
+
2148
+ Your recent email address change has been successfully reverted. Your email is now:
2149
+
2150
+ ${email}
2151
+
2152
+ If you did not request this revert, please contact our support team immediately.
2153
+
2154
+ ---
2155
+ Vritti AI Cloud - Cloud Management Platform
2156
+ This is an automated message, please do not reply.
2157
+ `.trim();
2158
+ await this.sendEmail({
2159
+ to: [
2160
+ {
2161
+ email,
2162
+ name
2163
+ }
2164
+ ],
2165
+ subject,
2166
+ htmlContent,
2167
+ textContent
2168
+ });
2169
+ this.logger.log(`Email revert confirmation sent to ${email}`);
2170
+ }
2171
+ // Verifies Brevo API connectivity — a 400 response means the API is reachable
2172
+ async verifyConnection() {
2173
+ try {
2174
+ await this.brevoClient.transactionalEmails.sendTransacEmail({
2175
+ sender: {
2176
+ email: this.senderEmail,
2177
+ name: this.senderName
2178
+ },
2179
+ to: [
2180
+ {
2181
+ email: this.senderEmail
2182
+ }
2183
+ ],
2184
+ subject: "Connection Test",
2185
+ htmlContent: "<p>Test</p>"
2186
+ });
2187
+ return true;
2188
+ } catch (err) {
2189
+ if (err instanceof BrevoError && err.statusCode === 400) {
2190
+ return true;
2191
+ }
2192
+ this.logger.error("Brevo connection verification failed:", err);
2193
+ return false;
2194
+ }
2195
+ }
2196
+ // Sends a transactional email via Brevo — retries handled internally by BrevoClient
2197
+ async sendEmail(emailData) {
2198
+ try {
2199
+ const result = await this.brevoClient.transactionalEmails.sendTransacEmail({
2200
+ sender: {
2201
+ email: this.senderEmail,
2202
+ name: this.senderName
2203
+ },
2204
+ to: emailData.to,
2205
+ subject: emailData.subject,
2206
+ htmlContent: emailData.htmlContent,
2207
+ textContent: emailData.textContent
2208
+ });
2209
+ this.logger.debug(`Email sent successfully. Message ID: ${result.messageId}`);
2210
+ } catch (err) {
2211
+ if (err instanceof BrevoTimeoutError) {
2212
+ this.logger.error("Brevo request timed out after retries.");
2213
+ throw new Error("Email sending failed: timeout");
2214
+ }
2215
+ if (err instanceof BrevoError) {
2216
+ if (err.statusCode === 429) {
2217
+ this.logger.error("Brevo rate limit exceeded after retries.");
2218
+ throw new Error("Email sending failed: rate limit exceeded");
2219
+ }
2220
+ if (err.statusCode === 401) {
2221
+ this.logger.error("Brevo authentication failed. Check your API key.");
2222
+ throw new Error("Email service authentication failed");
2223
+ }
2224
+ if (err.statusCode === 400) {
2225
+ this.logger.error("Bad request to Brevo API:", err.message);
2226
+ throw new Error(`Invalid email parameters: ${err.message}`);
2227
+ }
2228
+ this.logger.error(`Brevo API error ${err.statusCode}:`, err.message);
2229
+ throw new Error(`Email sending failed: ${err.message}`);
2230
+ }
2231
+ throw err;
2232
+ }
2233
+ }
2234
+ };
2235
+ EmailService = _ts_decorate11([
2236
+ Injectable7(),
2237
+ _ts_metadata7("design:type", Function),
2238
+ _ts_metadata7("design:paramtypes", [
2239
+ typeof ConfigService4 === "undefined" ? Object : ConfigService4
2240
+ ])
2241
+ ], EmailService);
2242
+
2243
+ // src/email/email.module.ts
2244
+ function _ts_decorate12(decorators, target, key, desc) {
2245
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2246
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2247
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2248
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2249
+ }
2250
+ __name(_ts_decorate12, "_ts_decorate");
2251
+ var EmailModule = class {
2252
+ static {
2253
+ __name(this, "EmailModule");
2254
+ }
2255
+ };
2256
+ EmailModule = _ts_decorate12([
2257
+ Global4(),
2258
+ Module4({
2259
+ imports: [
2260
+ ConfigModule2
2261
+ ],
2262
+ providers: [
2263
+ EmailService
2264
+ ],
2265
+ exports: [
2266
+ EmailService
2267
+ ]
2268
+ })
2269
+ ], EmailModule);
2270
+
2271
+ // src/exceptions/bad-gateway.exception.ts
2272
+ import { HttpStatus } from "@nestjs/common";
2273
+
2274
+ // src/exceptions/base-field.exception.ts
2275
+ import { HttpException } from "@nestjs/common";
2276
+ var HttpProblemException = class extends HttpException {
2277
+ static {
2278
+ __name(this, "HttpProblemException");
2279
+ }
2280
+ constructor(detailOrOptions, httpStatus) {
2281
+ const options = typeof detailOrOptions === "string" ? {
2282
+ detail: detailOrOptions
2283
+ } : detailOrOptions;
2284
+ super({
2285
+ type: options.type ?? "about:blank",
2286
+ label: options.label,
2287
+ detail: options.detail,
2288
+ errors: options.errors ?? []
2289
+ }, httpStatus);
2290
+ }
2291
+ };
2292
+
2293
+ // src/exceptions/bad-gateway.exception.ts
2294
+ var BadGatewayException = class extends HttpProblemException {
2295
+ static {
2296
+ __name(this, "BadGatewayException");
2297
+ }
2298
+ constructor(detailOrOptions) {
2299
+ super(detailOrOptions ?? "Bad Gateway", HttpStatus.BAD_GATEWAY);
2300
+ }
2301
+ };
2302
+
2303
+ // src/exceptions/bad-request.exception.ts
2304
+ import { HttpStatus as HttpStatus2 } from "@nestjs/common";
2305
+ var BadRequestException = class extends HttpProblemException {
2306
+ static {
2307
+ __name(this, "BadRequestException");
2308
+ }
2309
+ constructor(detailOrOptions) {
2310
+ super(detailOrOptions ?? "Bad Request", HttpStatus2.BAD_REQUEST);
2311
+ }
2312
+ };
2313
+
2314
+ // src/exceptions/conflict.exception.ts
2315
+ import { HttpStatus as HttpStatus3 } from "@nestjs/common";
2316
+ var ConflictException = class extends HttpProblemException {
2317
+ static {
2318
+ __name(this, "ConflictException");
2319
+ }
2320
+ constructor(detailOrOptions) {
2321
+ super(detailOrOptions ?? "Conflict", HttpStatus3.CONFLICT);
2322
+ }
2323
+ };
2324
+
2325
+ // src/exceptions/forbidden.exception.ts
2326
+ import { HttpStatus as HttpStatus4 } from "@nestjs/common";
2327
+ var ForbiddenException2 = class extends HttpProblemException {
2328
+ static {
2329
+ __name(this, "ForbiddenException");
2330
+ }
2331
+ constructor(detailOrOptions) {
2332
+ super(detailOrOptions ?? "Forbidden", HttpStatus4.FORBIDDEN);
2333
+ }
2334
+ };
2335
+
2336
+ // src/exceptions/gone.exception.ts
2337
+ import { HttpStatus as HttpStatus5 } from "@nestjs/common";
2338
+ var GoneException = class extends HttpProblemException {
2339
+ static {
2340
+ __name(this, "GoneException");
2341
+ }
2342
+ constructor(detailOrOptions) {
2343
+ super(detailOrOptions ?? "Gone", HttpStatus5.GONE);
2344
+ }
2345
+ };
2346
+
2347
+ // src/exceptions/internal-server-error.exception.ts
2348
+ import { HttpStatus as HttpStatus6 } from "@nestjs/common";
2349
+ var InternalServerErrorException3 = class extends HttpProblemException {
2350
+ static {
2351
+ __name(this, "InternalServerErrorException");
2352
+ }
2353
+ constructor(detailOrOptions) {
2354
+ super(detailOrOptions ?? "Internal Server Error", HttpStatus6.INTERNAL_SERVER_ERROR);
2355
+ }
2356
+ };
2357
+
2358
+ // src/exceptions/method-not-allowed.exception.ts
2359
+ import { HttpStatus as HttpStatus7 } from "@nestjs/common";
2360
+ var MethodNotAllowedException = class extends HttpProblemException {
2185
2361
  static {
2186
2362
  __name(this, "MethodNotAllowedException");
2187
2363
  }
@@ -2269,7 +2445,7 @@ var TooManyRequestsException = class extends HttpProblemException {
2269
2445
 
2270
2446
  // src/exceptions/unauthorized.exception.ts
2271
2447
  import { HttpStatus as HttpStatus15 } from "@nestjs/common";
2272
- var UnauthorizedException5 = class extends HttpProblemException {
2448
+ var UnauthorizedException3 = class extends HttpProblemException {
2273
2449
  static {
2274
2450
  __name(this, "UnauthorizedException");
2275
2451
  }
@@ -2312,14 +2488,14 @@ var ValidationException = class extends HttpProblemException {
2312
2488
  };
2313
2489
 
2314
2490
  // src/filters/http-exception.filter.ts
2315
- import { Catch, HttpException as HttpException2, HttpStatus as HttpStatus19, Logger as Logger10 } from "@nestjs/common";
2316
- function _ts_decorate12(decorators, target, key, desc) {
2491
+ import { Catch, HttpException as HttpException2, HttpStatus as HttpStatus19, Logger as Logger9 } from "@nestjs/common";
2492
+ function _ts_decorate13(decorators, target, key, desc) {
2317
2493
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2318
2494
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2319
2495
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2320
2496
  return c > 3 && r && Object.defineProperty(target, key, r), r;
2321
2497
  }
2322
- __name(_ts_decorate12, "_ts_decorate");
2498
+ __name(_ts_decorate13, "_ts_decorate");
2323
2499
  function getHttpStatusTitle(status) {
2324
2500
  const enumKey = Object.entries(HttpStatus19).find(([key, value]) => value === status && Number.isNaN(Number(key)))?.[0];
2325
2501
  if (!enumKey) {
@@ -2332,7 +2508,7 @@ var HttpExceptionFilter = class _HttpExceptionFilter {
2332
2508
  static {
2333
2509
  __name(this, "HttpExceptionFilter");
2334
2510
  }
2335
- logger = new Logger10(_HttpExceptionFilter.name);
2511
+ logger = new Logger9(_HttpExceptionFilter.name);
2336
2512
  catch(exception, host) {
2337
2513
  const ctx = host.switchToHttp();
2338
2514
  const response = ctx.getResponse();
@@ -2392,226 +2568,16 @@ var HttpExceptionFilter = class _HttpExceptionFilter {
2392
2568
  response.header("Content-Type", "application/problem+json").status(status).send(problemDetails);
2393
2569
  }
2394
2570
  };
2395
- HttpExceptionFilter = _ts_decorate12([
2571
+ HttpExceptionFilter = _ts_decorate13([
2396
2572
  Catch()
2397
2573
  ], HttpExceptionFilter);
2398
2574
 
2399
- // src/utils/phone.utils.ts
2400
- var CALLING_CODE_TO_COUNTRY = {
2401
- // 3-digit codes
2402
- "355": "AL",
2403
- "213": "DZ",
2404
- "376": "AD",
2405
- "244": "AO",
2406
- "672": "AQ",
2407
- "374": "AM",
2408
- "297": "AW",
2409
- "994": "AZ",
2410
- "973": "BH",
2411
- "880": "BD",
2412
- "375": "BY",
2413
- "501": "BZ",
2414
- "229": "BJ",
2415
- "975": "BT",
2416
- "591": "BO",
2417
- "387": "BA",
2418
- "267": "BW",
2419
- "673": "BN",
2420
- "359": "BG",
2421
- "226": "BF",
2422
- "257": "BI",
2423
- "855": "KH",
2424
- "237": "CM",
2425
- "238": "CV",
2426
- "236": "CF",
2427
- "235": "TD",
2428
- "269": "KM",
2429
- "242": "CG",
2430
- "243": "CD",
2431
- "506": "CR",
2432
- "385": "HR",
2433
- "357": "CY",
2434
- "420": "CZ",
2435
- "253": "DJ",
2436
- "593": "EC",
2437
- "503": "SV",
2438
- "240": "GQ",
2439
- "291": "ER",
2440
- "372": "EE",
2441
- "251": "ET",
2442
- "679": "FJ",
2443
- "358": "FI",
2444
- "241": "GA",
2445
- "220": "GM",
2446
- "995": "GE",
2447
- "233": "GH",
2448
- "350": "GI",
2449
- "299": "GL",
2450
- "502": "GT",
2451
- "224": "GN",
2452
- "245": "GW",
2453
- "592": "GY",
2454
- "509": "HT",
2455
- "504": "HN",
2456
- "354": "IS",
2457
- "964": "IQ",
2458
- "353": "IE",
2459
- "972": "IL",
2460
- "225": "CI",
2461
- "962": "JO",
2462
- "254": "KE",
2463
- "686": "KI",
2464
- "965": "KW",
2465
- "996": "KG",
2466
- "856": "LA",
2467
- "371": "LV",
2468
- "961": "LB",
2469
- "266": "LS",
2470
- "231": "LR",
2471
- "218": "LY",
2472
- "423": "LI",
2473
- "370": "LT",
2474
- "352": "LU",
2475
- "389": "MK",
2476
- "261": "MG",
2477
- "265": "MW",
2478
- "960": "MV",
2479
- "223": "ML",
2480
- "356": "MT",
2481
- "692": "MH",
2482
- "222": "MR",
2483
- "230": "MU",
2484
- "262": "YT",
2485
- "691": "FM",
2486
- "373": "MD",
2487
- "377": "MC",
2488
- "976": "MN",
2489
- "382": "ME",
2490
- "258": "MZ",
2491
- "264": "NA",
2492
- "674": "NR",
2493
- "977": "NP",
2494
- "505": "NI",
2495
- "227": "NE",
2496
- "234": "NG",
2497
- "683": "NU",
2498
- "968": "OM",
2499
- "680": "PW",
2500
- "970": "PS",
2501
- "507": "PA",
2502
- "675": "PG",
2503
- "595": "PY",
2504
- "351": "PT",
2505
- "974": "QA",
2506
- "250": "RW",
2507
- "685": "WS",
2508
- "378": "SM",
2509
- "239": "ST",
2510
- "966": "SA",
2511
- "221": "SN",
2512
- "381": "RS",
2513
- "248": "SC",
2514
- "232": "SL",
2515
- "421": "SK",
2516
- "386": "SI",
2517
- "677": "SB",
2518
- "252": "SO",
2519
- "211": "SS",
2520
- "249": "SD",
2521
- "597": "SR",
2522
- "268": "SZ",
2523
- "963": "SY",
2524
- "992": "TJ",
2525
- "255": "TZ",
2526
- "228": "TG",
2527
- "676": "TO",
2528
- "216": "TN",
2529
- "993": "TM",
2530
- "688": "TV",
2531
- "256": "UG",
2532
- "380": "UA",
2533
- "971": "AE",
2534
- "598": "UY",
2535
- "998": "UZ",
2536
- "678": "VU",
2537
- "379": "VA",
2538
- "967": "YE",
2539
- "260": "ZM",
2540
- "263": "ZW",
2541
- // 2-digit codes
2542
- "93": "AF",
2543
- "54": "AR",
2544
- "61": "AU",
2545
- "43": "AT",
2546
- "32": "BE",
2547
- "55": "BR",
2548
- "56": "CL",
2549
- "86": "CN",
2550
- "57": "CO",
2551
- "53": "CU",
2552
- "45": "DK",
2553
- "20": "EG",
2554
- "33": "FR",
2555
- "49": "DE",
2556
- "30": "GR",
2557
- "36": "HU",
2558
- "91": "IN",
2559
- "62": "ID",
2560
- "98": "IR",
2561
- "39": "IT",
2562
- "81": "JP",
2563
- "82": "KR",
2564
- "60": "MY",
2565
- "52": "MX",
2566
- "31": "NL",
2567
- "64": "NZ",
2568
- "47": "NO",
2569
- "92": "PK",
2570
- "51": "PE",
2571
- "63": "PH",
2572
- "48": "PL",
2573
- "40": "RO",
2574
- "65": "SG",
2575
- "27": "ZA",
2576
- "34": "ES",
2577
- "94": "LK",
2578
- "46": "SE",
2579
- "41": "CH",
2580
- "66": "TH",
2581
- "90": "TR",
2582
- "44": "GB",
2583
- "58": "VE",
2584
- "84": "VN",
2585
- // 1-digit codes (shared codes default to most common country)
2586
- "1": "US",
2587
- "7": "RU"
2588
- };
2589
- function extractCountryFromPhone(phone) {
2590
- const digits = phone.startsWith("+") ? phone.slice(1) : phone;
2591
- for (const length of [
2592
- 3,
2593
- 2,
2594
- 1
2595
- ]) {
2596
- const prefix = digits.slice(0, length);
2597
- if (CALLING_CODE_TO_COUNTRY[prefix]) {
2598
- return CALLING_CODE_TO_COUNTRY[prefix];
2599
- }
2600
- }
2601
- return void 0;
2602
- }
2603
- __name(extractCountryFromPhone, "extractCountryFromPhone");
2604
- function normalizePhoneNumber(phone) {
2605
- return phone.startsWith("+") ? phone : `+${phone}`;
2606
- }
2607
- __name(normalizePhoneNumber, "normalizePhoneNumber");
2608
-
2609
2575
  // src/logger/interceptors/http-logger.interceptor.ts
2610
- import { Injectable as Injectable10, Optional as Optional2 } from "@nestjs/common";
2611
- import { catchError, tap as tap2 } from "rxjs/operators";
2576
+ import { Injectable as Injectable9, Optional as Optional2 } from "@nestjs/common";
2577
+ import { catchError, tap } from "rxjs/operators";
2612
2578
 
2613
2579
  // src/logger/services/logger.service.ts
2614
- import { Injectable as Injectable9, Optional } from "@nestjs/common";
2580
+ import { Injectable as Injectable8, Optional } from "@nestjs/common";
2615
2581
  import { createLogger, format, transports } from "winston";
2616
2582
  import DailyRotateFile from "winston-daily-rotate-file";
2617
2583
 
@@ -2649,13 +2615,13 @@ function addCorrelationIdToResponse(reply, correlationId, headerName = DEFAULT_C
2649
2615
  __name(addCorrelationIdToResponse, "addCorrelationIdToResponse");
2650
2616
 
2651
2617
  // src/logger/services/logger.service.ts
2652
- function _ts_decorate13(decorators, target, key, desc) {
2618
+ function _ts_decorate14(decorators, target, key, desc) {
2653
2619
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2654
2620
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2655
2621
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2656
2622
  return c > 3 && r && Object.defineProperty(target, key, r), r;
2657
2623
  }
2658
- __name(_ts_decorate13, "_ts_decorate");
2624
+ __name(_ts_decorate14, "_ts_decorate");
2659
2625
  function _ts_metadata8(k, v) {
2660
2626
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2661
2627
  }
@@ -2687,10 +2653,7 @@ var LoggerService = class _LoggerService {
2687
2653
  this.activeLogger = this.createWinstonLogger(options);
2688
2654
  }
2689
2655
  }
2690
- /**
2691
- * Creates a Winston logger instance with inline configuration.
2692
- * Consolidates winston-config.factory.ts logic.
2693
- */
2656
+ // Creates a Winston logger instance with inline transports and format configuration
2694
2657
  createWinstonLogger(opts) {
2695
2658
  const level = opts.level ?? "debug";
2696
2659
  const logFormat = opts.format ?? "text";
@@ -2781,9 +2744,7 @@ ${trace}`;
2781
2744
  setContext(context) {
2782
2745
  this.context = context;
2783
2746
  }
2784
- /**
2785
- * Unified internal logging method that handles both Winston and NestJS Logger.
2786
- */
2747
+ // Dispatches a log entry to either the Winston or NestJS logger implementation
2787
2748
  _log(level, message, context, trace) {
2788
2749
  const ctx = context ?? this.context;
2789
2750
  if ("format" in this.activeLogger && "transports" in this.activeLogger) {
@@ -2811,9 +2772,7 @@ ${trace}`;
2811
2772
  }
2812
2773
  }
2813
2774
  }
2814
- /**
2815
- * Logs with custom metadata (Winston only).
2816
- */
2775
+ // Logs a message with custom metadata fields (Winston only)
2817
2776
  logWithMetadata(level, message, metadata, context) {
2818
2777
  const ctx = context ?? this.context;
2819
2778
  if ("format" in this.activeLogger && "transports" in this.activeLogger) {
@@ -2840,10 +2799,7 @@ ${trace}`;
2840
2799
  }
2841
2800
  return String(message);
2842
2801
  }
2843
- /**
2844
- * Enriches metadata with correlation context from AsyncLocalStorage.
2845
- * Inline from winston-logger.service.ts
2846
- */
2802
+ // Enriches metadata with correlation context from AsyncLocalStorage
2847
2803
  enrichMetadata(metadata = {}, context, trace) {
2848
2804
  const enriched = {
2849
2805
  ...metadata
@@ -2867,8 +2823,8 @@ ${trace}`;
2867
2823
  return childLogger;
2868
2824
  }
2869
2825
  };
2870
- LoggerService = _ts_decorate13([
2871
- Injectable9(),
2826
+ LoggerService = _ts_decorate14([
2827
+ Injectable8(),
2872
2828
  _ts_param4(0, Optional()),
2873
2829
  _ts_param4(1, Optional()),
2874
2830
  _ts_metadata8("design:type", Function),
@@ -2879,13 +2835,13 @@ LoggerService = _ts_decorate13([
2879
2835
  ], LoggerService);
2880
2836
 
2881
2837
  // src/logger/interceptors/http-logger.interceptor.ts
2882
- function _ts_decorate14(decorators, target, key, desc) {
2838
+ function _ts_decorate15(decorators, target, key, desc) {
2883
2839
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2884
2840
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2885
2841
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2886
2842
  return c > 3 && r && Object.defineProperty(target, key, r), r;
2887
2843
  }
2888
- __name(_ts_decorate14, "_ts_decorate");
2844
+ __name(_ts_decorate15, "_ts_decorate");
2889
2845
  function _ts_metadata9(k, v) {
2890
2846
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
2891
2847
  }
@@ -2921,7 +2877,7 @@ var HttpLoggerInterceptor = class {
2921
2877
  if (this.enableRequestLog) {
2922
2878
  this.logRequest(request);
2923
2879
  }
2924
- return next.handle().pipe(tap2(() => {
2880
+ return next.handle().pipe(tap(() => {
2925
2881
  if (this.enableResponseLog) {
2926
2882
  const duration = Date.now() - startTime;
2927
2883
  this.logResponse(request, response, duration);
@@ -2974,6 +2930,7 @@ var HttpLoggerInterceptor = class {
2974
2930
  try {
2975
2931
  const correlationContext = getCorrelationContext();
2976
2932
  const statusCode = response.statusCode || 500;
2933
+ const err = error;
2977
2934
  const metadata = {
2978
2935
  type: "http_error",
2979
2936
  method: request.method,
@@ -2981,24 +2938,24 @@ var HttpLoggerInterceptor = class {
2981
2938
  statusCode,
2982
2939
  duration,
2983
2940
  correlationId: correlationContext?.correlationId,
2984
- errorName: error?.name || "Error",
2985
- errorMessage: error?.message || "Unknown error"
2941
+ errorName: err.name || "Error",
2942
+ errorMessage: err.message || "Unknown error"
2986
2943
  };
2987
- if (error?.stack) {
2988
- metadata.trace = error.stack;
2944
+ if (err.stack) {
2945
+ metadata.trace = err.stack;
2989
2946
  }
2990
- if (error?.response) {
2991
- metadata.errorDetails = error.response;
2947
+ if (err.response) {
2948
+ metadata.errorDetails = err.response;
2992
2949
  }
2993
- const message = `ERROR ${request.method} ${request.url} ${statusCode} - ${error?.message || "Unknown error"}`;
2950
+ const message = `ERROR ${request.method} ${request.url} ${statusCode} - ${err.message || "Unknown error"}`;
2994
2951
  this.logger.logWithMetadata("error", message, metadata);
2995
2952
  } catch (loggingError) {
2996
2953
  this.logger.error("Failed to log HTTP error", loggingError.stack);
2997
2954
  }
2998
2955
  }
2999
2956
  };
3000
- HttpLoggerInterceptor = _ts_decorate14([
3001
- Injectable10(),
2957
+ HttpLoggerInterceptor = _ts_decorate15([
2958
+ Injectable9(),
3002
2959
  _ts_param5(1, Optional2()),
3003
2960
  _ts_metadata9("design:type", Function),
3004
2961
  _ts_metadata9("design:paramtypes", [
@@ -3008,17 +2965,17 @@ HttpLoggerInterceptor = _ts_decorate14([
3008
2965
  ], HttpLoggerInterceptor);
3009
2966
 
3010
2967
  // src/logger/logger.module.ts
3011
- import { Global as Global4, Logger as Logger11, Module as Module4 } from "@nestjs/common";
2968
+ import { Global as Global5, Logger as Logger10, Module as Module5 } from "@nestjs/common";
3012
2969
 
3013
2970
  // src/logger/middleware/correlation-id.middleware.ts
3014
- import { Injectable as Injectable11 } from "@nestjs/common";
3015
- function _ts_decorate15(decorators, target, key, desc) {
2971
+ import { Injectable as Injectable10 } from "@nestjs/common";
2972
+ function _ts_decorate16(decorators, target, key, desc) {
3016
2973
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3017
2974
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3018
2975
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3019
2976
  return c > 3 && r && Object.defineProperty(target, key, r), r;
3020
2977
  }
3021
- __name(_ts_decorate15, "_ts_decorate");
2978
+ __name(_ts_decorate16, "_ts_decorate");
3022
2979
  function _ts_metadata10(k, v) {
3023
2980
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3024
2981
  }
@@ -3033,9 +2990,7 @@ var CorrelationIdMiddleware = class {
3033
2990
  this.includeInResponse = options.includeInResponse ?? true;
3034
2991
  this.responseHeader = options.responseHeader ?? DEFAULT_CORRELATION_HEADER;
3035
2992
  }
3036
- /**
3037
- * Middleware handler for processing requests.
3038
- */
2993
+ // Generates and stores a correlation ID for the incoming request
3039
2994
  use(_req, reply, next) {
3040
2995
  const correlationId = generateCorrelationId();
3041
2996
  if (this.includeInResponse) {
@@ -3047,11 +3002,7 @@ var CorrelationIdMiddleware = class {
3047
3002
  next();
3048
3003
  });
3049
3004
  }
3050
- /**
3051
- * Fastify hook handler for onRequest.
3052
- * This is an async function that returns a Promise, ensuring the AsyncLocalStorage
3053
- * context persists throughout the entire request lifecycle.
3054
- */
3005
+ // Fastify onRequest hook that initializes correlation context in AsyncLocalStorage
3055
3006
  async onRequest(_req, reply) {
3056
3007
  const correlationId = generateCorrelationId();
3057
3008
  if (this.includeInResponse) {
@@ -3065,8 +3016,8 @@ var CorrelationIdMiddleware = class {
3065
3016
  }
3066
3017
  }
3067
3018
  };
3068
- CorrelationIdMiddleware = _ts_decorate15([
3069
- Injectable11(),
3019
+ CorrelationIdMiddleware = _ts_decorate16([
3020
+ Injectable10(),
3070
3021
  _ts_metadata10("design:type", Function),
3071
3022
  _ts_metadata10("design:paramtypes", [
3072
3023
  typeof CorrelationIdMiddlewareOptions === "undefined" ? Object : CorrelationIdMiddlewareOptions
@@ -3074,13 +3025,13 @@ CorrelationIdMiddleware = _ts_decorate15([
3074
3025
  ], CorrelationIdMiddleware);
3075
3026
 
3076
3027
  // src/logger/logger.module.ts
3077
- function _ts_decorate16(decorators, target, key, desc) {
3028
+ function _ts_decorate17(decorators, target, key, desc) {
3078
3029
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3079
3030
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3080
3031
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3081
3032
  return c > 3 && r && Object.defineProperty(target, key, r), r;
3082
3033
  }
3083
- __name(_ts_decorate16, "_ts_decorate");
3034
+ __name(_ts_decorate17, "_ts_decorate");
3084
3035
  var LOGGER_MODULE_OPTIONS = Symbol("LOGGER_MODULE_OPTIONS");
3085
3036
  var DEFAULT_LOGGER_OPTIONS = {
3086
3037
  provider: "winston",
@@ -3090,9 +3041,6 @@ var DEFAULT_LOGGER_OPTIONS = {
3090
3041
  maxFiles: "14d"
3091
3042
  };
3092
3043
  var ENVIRONMENT_PRESETS = {
3093
- /**
3094
- * Development preset - maximum verbosity for local development
3095
- */
3096
3044
  development: {
3097
3045
  provider: "winston",
3098
3046
  level: "debug",
@@ -3106,9 +3054,6 @@ var ENVIRONMENT_PRESETS = {
3106
3054
  slowRequestThreshold: 1e3
3107
3055
  }
3108
3056
  },
3109
- /**
3110
- * Staging preset - moderate verbosity with file logging
3111
- */
3112
3057
  staging: {
3113
3058
  provider: "winston",
3114
3059
  level: "log",
@@ -3122,9 +3067,6 @@ var ENVIRONMENT_PRESETS = {
3122
3067
  slowRequestThreshold: 3e3
3123
3068
  }
3124
3069
  },
3125
- /**
3126
- * Production preset - minimal verbosity with all safety features enabled
3127
- */
3128
3070
  production: {
3129
3071
  provider: "winston",
3130
3072
  level: "warn",
@@ -3138,9 +3080,6 @@ var ENVIRONMENT_PRESETS = {
3138
3080
  slowRequestThreshold: 5e3
3139
3081
  }
3140
3082
  },
3141
- /**
3142
- * Test preset - errors only, minimal features for faster test execution
3143
- */
3144
3083
  test: {
3145
3084
  provider: "winston",
3146
3085
  level: "error",
@@ -3169,12 +3108,12 @@ function mergeWithDefaults(options = {}) {
3169
3108
  __name(mergeWithDefaults, "mergeWithDefaults");
3170
3109
  function createDefaultLoggerProvider(options) {
3171
3110
  return {
3172
- provide: Logger11,
3111
+ provide: Logger10,
3173
3112
  useFactory: /* @__PURE__ */ __name(() => {
3174
- const logger = new Logger11();
3175
- if (options.level && typeof logger.setLogLevels === "function") {
3113
+ const logger = new Logger10();
3114
+ if (options.level) {
3176
3115
  const levels = getLevelsUpTo(options.level);
3177
- logger.setLogLevels(levels);
3116
+ logger.setLogLevels?.(levels);
3178
3117
  }
3179
3118
  return logger;
3180
3119
  }, "useFactory")
@@ -3201,7 +3140,7 @@ function createLoggerProviders(options = {}) {
3201
3140
  inject: [
3202
3141
  LOGGER_MODULE_OPTIONS,
3203
3142
  {
3204
- token: Logger11,
3143
+ token: Logger10,
3205
3144
  optional: true
3206
3145
  }
3207
3146
  ]
@@ -3256,37 +3195,7 @@ var LoggerModule = class _LoggerModule {
3256
3195
  static {
3257
3196
  __name(this, "LoggerModule");
3258
3197
  }
3259
- /**
3260
- * Configures the logger module with static options.
3261
- *
3262
- * Users must explicitly pass `environment` to select a preset.
3263
- * All preset values can be overridden by passing explicit options.
3264
- *
3265
- * @param options - Logger configuration options
3266
- * @returns Dynamic module configuration
3267
- *
3268
- * @example
3269
- * ```typescript
3270
- * // Production preset with app name
3271
- * LoggerModule.forRoot({
3272
- * environment: 'production',
3273
- * appName: 'my-service'
3274
- * })
3275
- *
3276
- * // Development preset with custom level
3277
- * LoggerModule.forRoot({
3278
- * environment: 'development',
3279
- * level: 'verbose',
3280
- * enableFileLogger: true
3281
- * })
3282
- *
3283
- * // Use default NestJS logger
3284
- * LoggerModule.forRoot({
3285
- * provider: 'default',
3286
- * environment: 'development'
3287
- * })
3288
- * ```
3289
- */
3198
+ // Configures the logger module with static options and environment preset
3290
3199
  static forRoot(options = {}) {
3291
3200
  const providers = createLoggerProviders(options);
3292
3201
  return {
@@ -3300,49 +3209,7 @@ var LoggerModule = class _LoggerModule {
3300
3209
  ]
3301
3210
  };
3302
3211
  }
3303
- /**
3304
- * Configures the logger module with async options.
3305
- *
3306
- * Supports dynamic configuration using:
3307
- * - `useFactory`: Factory function with dependency injection
3308
- * - `useClass`: Class implementing `LoggerOptionsFactory`
3309
- * - `useExisting`: Existing provider implementing `LoggerOptionsFactory`
3310
- *
3311
- * Options from the factory/class are merged with environment preset defaults.
3312
- *
3313
- * @param options - Async configuration options
3314
- * @returns Dynamic module configuration
3315
- *
3316
- * @example
3317
- * ```typescript
3318
- * // Factory with ConfigService
3319
- * LoggerModule.forRootAsync({
3320
- * imports: [ConfigModule],
3321
- * useFactory: (config: ConfigService) => ({
3322
- * environment: config.get('NODE_ENV', 'development'),
3323
- * provider: config.get('LOG_PROVIDER', 'winston'),
3324
- * level: config.get('LOG_LEVEL'),
3325
- * appName: config.get('APP_NAME'),
3326
- * }),
3327
- * inject: [ConfigService]
3328
- * })
3329
- *
3330
- * // Factory class
3331
- * @Injectable()
3332
- * class LoggerConfigService implements LoggerOptionsFactory {
3333
- * createLoggerOptions(): LoggerModuleOptions {
3334
- * return {
3335
- * environment: 'production',
3336
- * appName: 'my-service'
3337
- * };
3338
- * }
3339
- * }
3340
- *
3341
- * LoggerModule.forRootAsync({
3342
- * useClass: LoggerConfigService
3343
- * })
3344
- * ```
3345
- */
3212
+ // Configures the logger module with async options (useFactory, useClass, useExisting)
3346
3213
  static forRootAsync(options) {
3347
3214
  const asyncProviders = _LoggerModule.createAsyncProviders(options);
3348
3215
  return {
@@ -3352,13 +3219,13 @@ var LoggerModule = class _LoggerModule {
3352
3219
  ...asyncProviders,
3353
3220
  // Default logger provider
3354
3221
  {
3355
- provide: Logger11,
3222
+ provide: Logger10,
3356
3223
  useFactory: /* @__PURE__ */ __name((opts) => {
3357
3224
  if (opts.provider === "default") {
3358
- const logger = new Logger11();
3359
- if (opts.level && typeof logger.setLogLevels === "function") {
3225
+ const logger = new Logger10();
3226
+ if (opts.level) {
3360
3227
  const levels = getLevelsUpTo(opts.level);
3361
- logger.setLogLevels(levels);
3228
+ logger.setLogLevels?.(levels);
3362
3229
  }
3363
3230
  return logger;
3364
3231
  }
@@ -3377,7 +3244,7 @@ var LoggerModule = class _LoggerModule {
3377
3244
  inject: [
3378
3245
  LOGGER_MODULE_OPTIONS,
3379
3246
  {
3380
- token: Logger11,
3247
+ token: Logger10,
3381
3248
  optional: true
3382
3249
  }
3383
3250
  ]
@@ -3416,15 +3283,10 @@ var LoggerModule = class _LoggerModule {
3416
3283
  ]
3417
3284
  };
3418
3285
  }
3419
- /**
3420
- * Configures middleware for the module.
3421
- * Middleware is registered globally in main.ts using Fastify hooks.
3422
- */
3286
+ // Middleware registration is handled globally in main.ts via Fastify hooks
3423
3287
  configure(_consumer) {
3424
3288
  }
3425
- /**
3426
- * Creates async providers for dynamic module configuration.
3427
- */
3289
+ // Creates async providers for dynamic module configuration
3428
3290
  static createAsyncProviders(options) {
3429
3291
  if (options.useFactory) {
3430
3292
  return [
@@ -3442,9 +3304,7 @@ var LoggerModule = class _LoggerModule {
3442
3304
  }
3443
3305
  return providers;
3444
3306
  }
3445
- /**
3446
- * Creates the async options provider.
3447
- */
3307
+ // Creates the DI provider that resolves and merges async logger options
3448
3308
  static createAsyncOptionsProvider(options) {
3449
3309
  if (options.useFactory) {
3450
3310
  return {
@@ -3483,11 +3343,412 @@ var LoggerModule = class _LoggerModule {
3483
3343
  throw new Error("LoggerModule.forRootAsync() requires one of: useFactory, useClass, or useExisting");
3484
3344
  }
3485
3345
  };
3486
- LoggerModule = _ts_decorate16([
3487
- Global4(),
3488
- Module4({})
3346
+ LoggerModule = _ts_decorate17([
3347
+ Global5(),
3348
+ Module5({})
3489
3349
  ], LoggerModule);
3350
+
3351
+ // src/root/root.module.ts
3352
+ import { Module as Module6 } from "@nestjs/common";
3353
+
3354
+ // src/root/controllers/app.controller.ts
3355
+ import { Controller, Get } from "@nestjs/common";
3356
+ import { ApiTags } from "@nestjs/swagger";
3357
+
3358
+ // src/root/docs/app.docs.ts
3359
+ import { applyDecorators } from "@nestjs/common";
3360
+ import { ApiOperation, ApiResponse } from "@nestjs/swagger";
3361
+ function ApiHealthCheck() {
3362
+ return applyDecorators(ApiOperation({
3363
+ summary: "Health check endpoint"
3364
+ }), ApiResponse({
3365
+ status: 200,
3366
+ description: "Returns a welcome message indicating the API is running",
3367
+ type: String
3368
+ }));
3369
+ }
3370
+ __name(ApiHealthCheck, "ApiHealthCheck");
3371
+
3372
+ // src/root/services/app.service.ts
3373
+ import { Injectable as Injectable11 } from "@nestjs/common";
3374
+ function _ts_decorate18(decorators, target, key, desc) {
3375
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3376
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3377
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3378
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3379
+ }
3380
+ __name(_ts_decorate18, "_ts_decorate");
3381
+ var AppService = class {
3382
+ static {
3383
+ __name(this, "AppService");
3384
+ }
3385
+ // Returns the API welcome message
3386
+ getHello() {
3387
+ return `Hello World!`;
3388
+ }
3389
+ };
3390
+ AppService = _ts_decorate18([
3391
+ Injectable11()
3392
+ ], AppService);
3393
+
3394
+ // src/root/controllers/app.controller.ts
3395
+ function _ts_decorate19(decorators, target, key, desc) {
3396
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3397
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3398
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3399
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3400
+ }
3401
+ __name(_ts_decorate19, "_ts_decorate");
3402
+ function _ts_metadata11(k, v) {
3403
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3404
+ }
3405
+ __name(_ts_metadata11, "_ts_metadata");
3406
+ var AppController = class {
3407
+ static {
3408
+ __name(this, "AppController");
3409
+ }
3410
+ appService;
3411
+ constructor(appService) {
3412
+ this.appService = appService;
3413
+ }
3414
+ // Returns a welcome message indicating the API is running
3415
+ getHello() {
3416
+ return this.appService.getHello();
3417
+ }
3418
+ };
3419
+ _ts_decorate19([
3420
+ Get(),
3421
+ Public(),
3422
+ ApiHealthCheck(),
3423
+ _ts_metadata11("design:type", Function),
3424
+ _ts_metadata11("design:paramtypes", []),
3425
+ _ts_metadata11("design:returntype", String)
3426
+ ], AppController.prototype, "getHello", null);
3427
+ AppController = _ts_decorate19([
3428
+ ApiTags("Health"),
3429
+ Controller(),
3430
+ _ts_metadata11("design:type", Function),
3431
+ _ts_metadata11("design:paramtypes", [
3432
+ typeof AppService === "undefined" ? Object : AppService
3433
+ ])
3434
+ ], AppController);
3435
+
3436
+ // src/root/controllers/csrf.controller.ts
3437
+ import { Controller as Controller2, Get as Get2, HttpCode, HttpStatus as HttpStatus20, Res } from "@nestjs/common";
3438
+ import { ApiTags as ApiTags2 } from "@nestjs/swagger";
3439
+
3440
+ // src/root/docs/csrf.docs.ts
3441
+ import { applyDecorators as applyDecorators2 } from "@nestjs/common";
3442
+ import { ApiOperation as ApiOperation2, ApiResponse as ApiResponse2 } from "@nestjs/swagger";
3443
+ function ApiGetCsrfToken() {
3444
+ return applyDecorators2(ApiOperation2({
3445
+ summary: "Get CSRF token",
3446
+ description: "Generates and returns a CSRF token that must be included in all state-changing requests (POST, PUT, PATCH, DELETE). The token should be sent in the X-CSRF-Token header."
3447
+ }), ApiResponse2({
3448
+ status: 200,
3449
+ description: "CSRF token generated successfully",
3450
+ schema: {
3451
+ type: "object",
3452
+ properties: {
3453
+ csrfToken: {
3454
+ type: "string",
3455
+ description: "The CSRF token to use in subsequent requests",
3456
+ example: "abc123xyz789"
3457
+ }
3458
+ },
3459
+ required: [
3460
+ "csrfToken"
3461
+ ]
3462
+ }
3463
+ }));
3464
+ }
3465
+ __name(ApiGetCsrfToken, "ApiGetCsrfToken");
3466
+
3467
+ // src/root/controllers/csrf.controller.ts
3468
+ function _ts_decorate20(decorators, target, key, desc) {
3469
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3470
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3471
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3472
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3473
+ }
3474
+ __name(_ts_decorate20, "_ts_decorate");
3475
+ function _ts_metadata12(k, v) {
3476
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
3477
+ }
3478
+ __name(_ts_metadata12, "_ts_metadata");
3479
+ function _ts_param6(paramIndex, decorator) {
3480
+ return function(target, key) {
3481
+ decorator(target, key, paramIndex);
3482
+ };
3483
+ }
3484
+ __name(_ts_param6, "_ts_param");
3485
+ var CsrfController = class {
3486
+ static {
3487
+ __name(this, "CsrfController");
3488
+ }
3489
+ // Generates a CSRF token via Fastify's csrf-protection plugin
3490
+ getToken(reply) {
3491
+ const csrfToken = reply.generateCsrf();
3492
+ return {
3493
+ csrfToken
3494
+ };
3495
+ }
3496
+ };
3497
+ _ts_decorate20([
3498
+ Get2("token"),
3499
+ Public(),
3500
+ HttpCode(HttpStatus20.OK),
3501
+ ApiGetCsrfToken(),
3502
+ _ts_param6(0, Res({
3503
+ passthrough: true
3504
+ })),
3505
+ _ts_metadata12("design:type", Function),
3506
+ _ts_metadata12("design:paramtypes", [
3507
+ typeof FastifyReply === "undefined" ? Object : FastifyReply
3508
+ ]),
3509
+ _ts_metadata12("design:returntype", Object)
3510
+ ], CsrfController.prototype, "getToken", null);
3511
+ CsrfController = _ts_decorate20([
3512
+ ApiTags2("CSRF"),
3513
+ Controller2("csrf")
3514
+ ], CsrfController);
3515
+
3516
+ // src/root/root.module.ts
3517
+ function _ts_decorate21(decorators, target, key, desc) {
3518
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3519
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
3520
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
3521
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
3522
+ }
3523
+ __name(_ts_decorate21, "_ts_decorate");
3524
+ var RootModule = class {
3525
+ static {
3526
+ __name(this, "RootModule");
3527
+ }
3528
+ };
3529
+ RootModule = _ts_decorate21([
3530
+ Module6({
3531
+ controllers: [
3532
+ AppController,
3533
+ CsrfController
3534
+ ],
3535
+ providers: [
3536
+ AppService
3537
+ ]
3538
+ })
3539
+ ], RootModule);
3540
+
3541
+ // src/utils/phone.utils.ts
3542
+ var CALLING_CODE_TO_COUNTRY = {
3543
+ // 3-digit codes
3544
+ "355": "AL",
3545
+ "213": "DZ",
3546
+ "376": "AD",
3547
+ "244": "AO",
3548
+ "672": "AQ",
3549
+ "374": "AM",
3550
+ "297": "AW",
3551
+ "994": "AZ",
3552
+ "973": "BH",
3553
+ "880": "BD",
3554
+ "375": "BY",
3555
+ "501": "BZ",
3556
+ "229": "BJ",
3557
+ "975": "BT",
3558
+ "591": "BO",
3559
+ "387": "BA",
3560
+ "267": "BW",
3561
+ "673": "BN",
3562
+ "359": "BG",
3563
+ "226": "BF",
3564
+ "257": "BI",
3565
+ "855": "KH",
3566
+ "237": "CM",
3567
+ "238": "CV",
3568
+ "236": "CF",
3569
+ "235": "TD",
3570
+ "269": "KM",
3571
+ "242": "CG",
3572
+ "243": "CD",
3573
+ "506": "CR",
3574
+ "385": "HR",
3575
+ "357": "CY",
3576
+ "420": "CZ",
3577
+ "253": "DJ",
3578
+ "593": "EC",
3579
+ "503": "SV",
3580
+ "240": "GQ",
3581
+ "291": "ER",
3582
+ "372": "EE",
3583
+ "251": "ET",
3584
+ "679": "FJ",
3585
+ "358": "FI",
3586
+ "241": "GA",
3587
+ "220": "GM",
3588
+ "995": "GE",
3589
+ "233": "GH",
3590
+ "350": "GI",
3591
+ "299": "GL",
3592
+ "502": "GT",
3593
+ "224": "GN",
3594
+ "245": "GW",
3595
+ "592": "GY",
3596
+ "509": "HT",
3597
+ "504": "HN",
3598
+ "354": "IS",
3599
+ "964": "IQ",
3600
+ "353": "IE",
3601
+ "972": "IL",
3602
+ "225": "CI",
3603
+ "962": "JO",
3604
+ "254": "KE",
3605
+ "686": "KI",
3606
+ "965": "KW",
3607
+ "996": "KG",
3608
+ "856": "LA",
3609
+ "371": "LV",
3610
+ "961": "LB",
3611
+ "266": "LS",
3612
+ "231": "LR",
3613
+ "218": "LY",
3614
+ "423": "LI",
3615
+ "370": "LT",
3616
+ "352": "LU",
3617
+ "389": "MK",
3618
+ "261": "MG",
3619
+ "265": "MW",
3620
+ "960": "MV",
3621
+ "223": "ML",
3622
+ "356": "MT",
3623
+ "692": "MH",
3624
+ "222": "MR",
3625
+ "230": "MU",
3626
+ "262": "YT",
3627
+ "691": "FM",
3628
+ "373": "MD",
3629
+ "377": "MC",
3630
+ "976": "MN",
3631
+ "382": "ME",
3632
+ "258": "MZ",
3633
+ "264": "NA",
3634
+ "674": "NR",
3635
+ "977": "NP",
3636
+ "505": "NI",
3637
+ "227": "NE",
3638
+ "234": "NG",
3639
+ "683": "NU",
3640
+ "968": "OM",
3641
+ "680": "PW",
3642
+ "970": "PS",
3643
+ "507": "PA",
3644
+ "675": "PG",
3645
+ "595": "PY",
3646
+ "351": "PT",
3647
+ "974": "QA",
3648
+ "250": "RW",
3649
+ "685": "WS",
3650
+ "378": "SM",
3651
+ "239": "ST",
3652
+ "966": "SA",
3653
+ "221": "SN",
3654
+ "381": "RS",
3655
+ "248": "SC",
3656
+ "232": "SL",
3657
+ "421": "SK",
3658
+ "386": "SI",
3659
+ "677": "SB",
3660
+ "252": "SO",
3661
+ "211": "SS",
3662
+ "249": "SD",
3663
+ "597": "SR",
3664
+ "268": "SZ",
3665
+ "963": "SY",
3666
+ "992": "TJ",
3667
+ "255": "TZ",
3668
+ "228": "TG",
3669
+ "676": "TO",
3670
+ "216": "TN",
3671
+ "993": "TM",
3672
+ "688": "TV",
3673
+ "256": "UG",
3674
+ "380": "UA",
3675
+ "971": "AE",
3676
+ "598": "UY",
3677
+ "998": "UZ",
3678
+ "678": "VU",
3679
+ "379": "VA",
3680
+ "967": "YE",
3681
+ "260": "ZM",
3682
+ "263": "ZW",
3683
+ // 2-digit codes
3684
+ "93": "AF",
3685
+ "54": "AR",
3686
+ "61": "AU",
3687
+ "43": "AT",
3688
+ "32": "BE",
3689
+ "55": "BR",
3690
+ "56": "CL",
3691
+ "86": "CN",
3692
+ "57": "CO",
3693
+ "53": "CU",
3694
+ "45": "DK",
3695
+ "20": "EG",
3696
+ "33": "FR",
3697
+ "49": "DE",
3698
+ "30": "GR",
3699
+ "36": "HU",
3700
+ "91": "IN",
3701
+ "62": "ID",
3702
+ "98": "IR",
3703
+ "39": "IT",
3704
+ "81": "JP",
3705
+ "82": "KR",
3706
+ "60": "MY",
3707
+ "52": "MX",
3708
+ "31": "NL",
3709
+ "64": "NZ",
3710
+ "47": "NO",
3711
+ "92": "PK",
3712
+ "51": "PE",
3713
+ "63": "PH",
3714
+ "48": "PL",
3715
+ "40": "RO",
3716
+ "65": "SG",
3717
+ "27": "ZA",
3718
+ "34": "ES",
3719
+ "94": "LK",
3720
+ "46": "SE",
3721
+ "41": "CH",
3722
+ "66": "TH",
3723
+ "90": "TR",
3724
+ "44": "GB",
3725
+ "58": "VE",
3726
+ "84": "VN",
3727
+ // 1-digit codes (shared codes default to most common country)
3728
+ "1": "US",
3729
+ "7": "RU"
3730
+ };
3731
+ function extractCountryFromPhone(phone) {
3732
+ const digits = phone.startsWith("+") ? phone.slice(1) : phone;
3733
+ for (const length of [
3734
+ 3,
3735
+ 2,
3736
+ 1
3737
+ ]) {
3738
+ const prefix = digits.slice(0, length);
3739
+ if (CALLING_CODE_TO_COUNTRY[prefix]) {
3740
+ return CALLING_CODE_TO_COUNTRY[prefix];
3741
+ }
3742
+ }
3743
+ return void 0;
3744
+ }
3745
+ __name(extractCountryFromPhone, "extractCountryFromPhone");
3746
+ function normalizePhoneNumber(phone) {
3747
+ return phone.startsWith("+") ? phone : `+${phone}`;
3748
+ }
3749
+ __name(normalizePhoneNumber, "normalizePhoneNumber");
3490
3750
  export {
3751
+ AccessToken,
3491
3752
  AuthConfigModule,
3492
3753
  BadGatewayException,
3493
3754
  BadRequestException,
@@ -3495,12 +3756,15 @@ export {
3495
3756
  CorrelationIdMiddleware,
3496
3757
  DEFAULT_CORRELATION_HEADER,
3497
3758
  DatabaseModule,
3759
+ EmailModule,
3760
+ EmailService,
3498
3761
  ForbiddenException2 as ForbiddenException,
3499
3762
  GoneException,
3500
3763
  HttpExceptionFilter,
3501
3764
  HttpLoggerInterceptor,
3502
3765
  HttpProblemException,
3503
3766
  InternalServerErrorException3 as InternalServerErrorException,
3767
+ JwtAuthService,
3504
3768
  LOGGER_MODULE_OPTIONS,
3505
3769
  LoggerModule,
3506
3770
  LoggerService,
@@ -3513,17 +3777,23 @@ export {
3513
3777
  PrimaryBaseRepository,
3514
3778
  PrimaryDatabaseService,
3515
3779
  Public,
3780
+ RESET_KEY,
3781
+ RefreshTokenCookie,
3516
3782
  RequestTimeoutException,
3783
+ Reset,
3784
+ RootModule,
3517
3785
  SKIP_CSRF_KEY,
3786
+ SelectOptionsQueryDto,
3518
3787
  ServiceUnavailableException,
3788
+ SessionData,
3519
3789
  SkipCsrf,
3520
- SseAuthGuard,
3521
3790
  Tenant,
3522
3791
  TenantBaseRepository,
3523
3792
  TenantContextService,
3524
3793
  TenantDatabaseService,
3794
+ TokenType,
3525
3795
  TooManyRequestsException,
3526
- UnauthorizedException5 as UnauthorizedException,
3796
+ UnauthorizedException3 as UnauthorizedException,
3527
3797
  UnprocessableEntityException,
3528
3798
  UnsupportedMediaTypeException,
3529
3799
  UserId,
@@ -3540,8 +3810,11 @@ export {
3540
3810
  getHttpStatusTitle,
3541
3811
  getJwtExpiry,
3542
3812
  getRefreshCookieOptions,
3813
+ getTokenExpiry,
3543
3814
  hashToken,
3815
+ jwtConfigFactory,
3544
3816
  normalizePhoneNumber,
3817
+ parseExpiryToMs,
3545
3818
  resetConfig,
3546
3819
  runWithCorrelationContext,
3547
3820
  updateCorrelationContext,