@vritti/api-sdk 0.1.0 → 0.1.2

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.cjs CHANGED
@@ -71,18 +71,113 @@ __export(index_exports, {
71
71
  ValidationException: () => ValidationException,
72
72
  VrittiAuthGuard: () => VrittiAuthGuard,
73
73
  addCorrelationIdToResponse: () => addCorrelationIdToResponse,
74
+ configureApiSdk: () => configureApiSdk,
74
75
  correlationStorage: () => correlationStorage,
76
+ defineConfig: () => defineConfig,
75
77
  generateCorrelationId: () => generateCorrelationId,
78
+ getConfig: () => getConfig,
76
79
  getCorrelationContext: () => getCorrelationContext,
77
80
  getHttpStatusTitle: () => getHttpStatusTitle,
81
+ getJwtExpiry: () => getJwtExpiry,
82
+ getRefreshCookieOptions: () => getRefreshCookieOptions,
83
+ hashToken: () => hashToken,
84
+ resetConfig: () => resetConfig,
78
85
  runWithCorrelationContext: () => runWithCorrelationContext,
79
- updateCorrelationContext: () => updateCorrelationContext
86
+ updateCorrelationContext: () => updateCorrelationContext,
87
+ verifyTokenHash: () => verifyTokenHash
80
88
  });
81
89
  module.exports = __toCommonJS(index_exports);
82
90
 
91
+ // src/config/index.ts
92
+ var defaultConfig = {
93
+ cookie: {
94
+ refreshCookieName: "vritti_refresh",
95
+ refreshCookieMaxAge: 30 * 24 * 60 * 60 * 1e3,
96
+ refreshCookiePath: "/",
97
+ refreshCookieSecure: process.env.NODE_ENV === "production",
98
+ refreshCookieSameSite: "strict"
99
+ },
100
+ jwt: {
101
+ accessTokenExpiry: "15m",
102
+ refreshTokenExpiry: "30d",
103
+ onboardingTokenExpiry: "24h",
104
+ validateTokenBinding: true
105
+ },
106
+ guard: {
107
+ tenantHeaderName: "x-tenant-id",
108
+ authHeaderName: "authorization",
109
+ tokenPrefix: "Bearer"
110
+ }
111
+ };
112
+ var currentConfig = {
113
+ ...defaultConfig
114
+ };
115
+ function defineConfig(config) {
116
+ return config;
117
+ }
118
+ __name(defineConfig, "defineConfig");
119
+ function configureApiSdk(userConfig) {
120
+ currentConfig = {
121
+ cookie: {
122
+ ...defaultConfig.cookie,
123
+ ...userConfig.cookie || {}
124
+ },
125
+ jwt: {
126
+ ...defaultConfig.jwt,
127
+ ...userConfig.jwt || {}
128
+ },
129
+ guard: {
130
+ ...defaultConfig.guard,
131
+ ...userConfig.guard || {}
132
+ }
133
+ };
134
+ }
135
+ __name(configureApiSdk, "configureApiSdk");
136
+ function getConfig() {
137
+ return currentConfig;
138
+ }
139
+ __name(getConfig, "getConfig");
140
+ function resetConfig() {
141
+ currentConfig = {
142
+ ...defaultConfig
143
+ };
144
+ }
145
+ __name(resetConfig, "resetConfig");
146
+ function getRefreshCookieOptions() {
147
+ return {
148
+ httpOnly: true,
149
+ secure: currentConfig.cookie.refreshCookieSecure,
150
+ sameSite: currentConfig.cookie.refreshCookieSameSite,
151
+ path: currentConfig.cookie.refreshCookiePath,
152
+ maxAge: currentConfig.cookie.refreshCookieMaxAge
153
+ };
154
+ }
155
+ __name(getRefreshCookieOptions, "getRefreshCookieOptions");
156
+ function getJwtExpiry() {
157
+ return {
158
+ access: currentConfig.jwt.accessTokenExpiry,
159
+ refresh: currentConfig.jwt.refreshTokenExpiry,
160
+ onboarding: currentConfig.jwt.onboardingTokenExpiry
161
+ };
162
+ }
163
+ __name(getJwtExpiry, "getJwtExpiry");
164
+
165
+ // src/auth/utils/token-hash.util.ts
166
+ var crypto = __toESM(require("crypto"), 1);
167
+ function hashToken(token) {
168
+ return crypto.createHash("sha256").update(token).digest("hex");
169
+ }
170
+ __name(hashToken, "hashToken");
171
+ function verifyTokenHash(token, expectedHash) {
172
+ const computedHash = hashToken(token);
173
+ if (computedHash.length !== expectedHash.length) return false;
174
+ return crypto.timingSafeEqual(Buffer.from(computedHash, "hex"), Buffer.from(expectedHash, "hex"));
175
+ }
176
+ __name(verifyTokenHash, "verifyTokenHash");
177
+
83
178
  // src/auth/auth-config.module.ts
84
179
  var import_common5 = require("@nestjs/common");
85
- var import_config2 = require("@nestjs/config");
180
+ var import_config4 = require("@nestjs/config");
86
181
  var import_core3 = require("@nestjs/core");
87
182
  var import_jwt2 = require("@nestjs/jwt");
88
183
 
@@ -143,17 +238,18 @@ var RequestService = class {
143
238
  return type === "Bearer" && token ? token : null;
144
239
  }
145
240
  /**
146
- * Extract refresh token from session-id cookie
147
- * Cookie name: session-id
241
+ * Extract refresh token from httpOnly cookie
242
+ * Cookie name is configurable via api-sdk config
148
243
  * @returns Refresh token or null if not found
149
244
  */
150
245
  getRefreshToken() {
151
246
  try {
152
247
  const cookies = this.request.cookies;
153
248
  if (cookies && typeof cookies === "object") {
154
- const sessionId = cookies["session-id"];
155
- if (sessionId) {
156
- return sessionId;
249
+ const config = getConfig();
250
+ const refreshToken = cookies[config.cookie.refreshCookieName];
251
+ if (refreshToken) {
252
+ return refreshToken;
157
253
  }
158
254
  }
159
255
  return null;
@@ -215,10 +311,9 @@ RequestModule = _ts_decorate2([
215
311
 
216
312
  // src/auth/guards/vritti-auth.guard.ts
217
313
  var import_common4 = require("@nestjs/common");
218
- var import_config = require("@nestjs/config");
314
+ var import_config2 = require("@nestjs/config");
219
315
  var import_core2 = require("@nestjs/core");
220
316
  var import_jwt = require("@nestjs/jwt");
221
- var jwt = __toESM(require("jsonwebtoken"), 1);
222
317
 
223
318
  // src/database/services/primary-database.service.ts
224
319
  var import_common3 = require("@nestjs/common");
@@ -280,10 +375,14 @@ var PrimaryDatabaseService = class _PrimaryDatabaseService {
280
375
  connectionString: databaseUrl,
281
376
  max: this.options.maxConnections || 10
282
377
  });
378
+ this.logger.debug(`Schema keys passed to drizzle: [${Object.keys(this.options.drizzleSchema || {}).join(", ")}]`);
379
+ this.logger.debug(`Relations keys passed to drizzle: [${Object.keys(this.options.drizzleRelations || {}).join(", ")}]`);
283
380
  this.db = (0, import_node_postgres.drizzle)({
284
381
  client: this.pool,
285
- schema: this.options.drizzleSchema
382
+ schema: this.options.drizzleSchema,
383
+ relations: this.options.drizzleRelations
286
384
  });
385
+ this.logger.debug(`Drizzle query keys after init: [${Object.keys(this.db.query || {}).join(", ")}]`);
287
386
  await this.pool.query("SELECT 1");
288
387
  this.logger.log("Connected to primary database (tenant registry)");
289
388
  } catch (error) {
@@ -514,6 +613,7 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
514
613
  }
515
614
  const validatedToken2 = this.validateAccessToken(accessToken);
516
615
  this.logger.debug("Onboarding token validated successfully");
616
+ this.validateRefreshTokenBinding(context, validatedToken2);
517
617
  const userId2 = validatedToken2.userId;
518
618
  request.user = {
519
619
  id: userId2
@@ -526,13 +626,7 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
526
626
  }
527
627
  const validatedToken = this.validateAccessToken(accessToken);
528
628
  this.logger.debug("Access token validated successfully");
529
- const refreshToken = this.requestService.getRefreshToken();
530
- if (!refreshToken) {
531
- this.logger.warn("Refresh token (session-id) not found in cookies");
532
- throw new import_common4.UnauthorizedException("Refresh token not found");
533
- }
534
- this.validateRefreshToken(refreshToken);
535
- this.logger.debug("Refresh token validated successfully");
629
+ this.validateRefreshTokenBinding(context, validatedToken);
536
630
  const userId = validatedToken.userId;
537
631
  request.user = {
538
632
  id: userId
@@ -603,60 +697,36 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
603
697
  }
604
698
  }
605
699
  /**
606
- * Validate refresh token with proper expiry checks
607
- * Throws UnauthorizedException if token is invalid or expired
608
- */
609
- validateRefreshToken(token) {
610
- const jwtSecret = this.configService.get("JWT_REFRESH_SECRET") || this.configService.get("JWT_SECRET");
611
- this.validateRefreshTokenWithSecret(token, jwtSecret);
612
- }
613
- /**
614
- * Helper to validate refresh token with specific secret
700
+ * Validate that the access token is bound to the refresh token in the cookie.
701
+ * This prevents token theft - a stolen access token is useless without the
702
+ * corresponding refresh token cookie.
703
+ *
704
+ * @param context - The execution context containing the request
705
+ * @param validatedToken - The decoded and validated JWT token
706
+ * @throws UnauthorizedException if token binding validation fails
615
707
  */
616
- validateRefreshTokenWithSecret(token, secret) {
617
- if (!secret) {
618
- this.logger.error("JWT secret not configured for refresh token validation");
619
- throw new import_common4.UnauthorizedException("Server configuration error");
708
+ validateRefreshTokenBinding(context, validatedToken) {
709
+ const config = getConfig();
710
+ if (!config.jwt.validateTokenBinding) {
711
+ this.logger.debug("Token binding validation is disabled");
712
+ return;
620
713
  }
621
- try {
622
- const decoded = jwt.verify(token, secret, {
623
- algorithms: [
624
- "HS256",
625
- "HS512",
626
- "RS256"
627
- ]
628
- });
629
- this.logger.debug(`Refresh token decoded for user: ${decoded.userId}`);
630
- if (decoded.exp) {
631
- const expiryTime = decoded.exp * 1e3;
632
- const currentTime = Date.now();
633
- if (currentTime > expiryTime) {
634
- this.logger.warn("Refresh token has expired");
635
- throw new import_common4.UnauthorizedException("Refresh token has expired. Please login again");
636
- }
637
- const timeRemaining = expiryTime - currentTime;
638
- this.logger.debug(`Refresh token valid for ${Math.floor(timeRemaining / 1e3)} more seconds`);
639
- }
640
- } catch (error) {
641
- if (error instanceof import_common4.UnauthorizedException) {
642
- throw error;
643
- }
644
- const jwtError = error;
645
- if (jwtError?.name === "TokenExpiredError") {
646
- this.logger.warn(`Refresh token expired at: ${jwtError?.expiredAt}`);
647
- throw new import_common4.UnauthorizedException("Refresh token has expired. Please login again");
648
- }
649
- if (jwtError?.name === "JsonWebTokenError") {
650
- this.logger.warn(`Refresh token verification failed: ${jwtError?.message}`);
651
- throw new import_common4.UnauthorizedException("Invalid refresh token");
652
- }
653
- if (jwtError?.name === "NotBeforeError") {
654
- this.logger.warn("Refresh token used before valid (nbf claim)");
655
- throw new import_common4.UnauthorizedException("Refresh token not yet valid");
656
- }
657
- this.logger.error("Unexpected error validating refresh token", error);
658
- throw new import_common4.UnauthorizedException("Refresh token validation failed");
714
+ if (!validatedToken.refreshTokenHash) {
715
+ this.logger.debug("Token does not contain refreshTokenHash, skipping binding validation");
716
+ return;
717
+ }
718
+ const request = context.switchToHttp().getRequest();
719
+ const cookies = request.cookies || {};
720
+ const refreshToken = cookies[config.cookie.refreshCookieName];
721
+ if (!refreshToken) {
722
+ this.logger.warn("Session validation failed - refresh token cookie not found");
723
+ throw new import_common4.UnauthorizedException("Session validation failed");
724
+ }
725
+ if (!verifyTokenHash(refreshToken, validatedToken.refreshTokenHash)) {
726
+ this.logger.warn("Session validation failed - token binding mismatch");
727
+ throw new import_common4.UnauthorizedException("Session validation failed");
659
728
  }
729
+ this.logger.debug("Token binding validated successfully");
660
730
  }
661
731
  };
662
732
  VrittiAuthGuard = _ts_decorate4([
@@ -666,7 +736,7 @@ VrittiAuthGuard = _ts_decorate4([
666
736
  _ts_metadata3("design:type", Function),
667
737
  _ts_metadata3("design:paramtypes", [
668
738
  typeof import_core2.Reflector === "undefined" ? Object : import_core2.Reflector,
669
- typeof import_config.ConfigService === "undefined" ? Object : import_config.ConfigService,
739
+ typeof import_config2.ConfigService === "undefined" ? Object : import_config2.ConfigService,
670
740
  typeof import_jwt.JwtService === "undefined" ? Object : import_jwt.JwtService,
671
741
  typeof PrimaryDatabaseService === "undefined" ? Object : PrimaryDatabaseService,
672
742
  typeof RequestService === "undefined" ? Object : RequestService
@@ -699,14 +769,14 @@ var AuthConfigModule = class _AuthConfigModule {
699
769
  return {
700
770
  module: _AuthConfigModule,
701
771
  imports: [
702
- import_config2.ConfigModule,
772
+ import_config4.ConfigModule,
703
773
  RequestModule,
704
774
  import_jwt2.JwtModule.registerAsync({
705
775
  imports: [
706
- import_config2.ConfigModule
776
+ import_config4.ConfigModule
707
777
  ],
708
778
  inject: [
709
- import_config2.ConfigService
779
+ import_config4.ConfigService
710
780
  ],
711
781
  useFactory: /* @__PURE__ */ __name((config) => ({
712
782
  secret: config.get("JWT_SECRET"),
@@ -1305,6 +1375,10 @@ DatabaseModule = _ts_decorate10([
1305
1375
  // src/database/repositories/primary-base.repository.ts
1306
1376
  var import_common11 = require("@nestjs/common");
1307
1377
  var import_drizzle_orm2 = require("drizzle-orm");
1378
+ function snakeToCamel(str) {
1379
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1380
+ }
1381
+ __name(snakeToCamel, "snakeToCamel");
1308
1382
  var PrimaryBaseRepository = class {
1309
1383
  static {
1310
1384
  __name(this, "PrimaryBaseRepository");
@@ -1314,7 +1388,8 @@ var PrimaryBaseRepository = class {
1314
1388
  logger;
1315
1389
  /**
1316
1390
  * The table name extracted from the Drizzle table at runtime.
1317
- * Used to access the query API for this repository's table.
1391
+ * Stored in camelCase to match Drizzle's query object keys.
1392
+ * Example: 'email_verifications' -> 'emailVerifications'
1318
1393
  */
1319
1394
  tableName;
1320
1395
  /**
@@ -1326,21 +1401,28 @@ var PrimaryBaseRepository = class {
1326
1401
  return this.database.drizzleClient;
1327
1402
  }
1328
1403
  /**
1329
- * Model query API for THIS repository's table (Prisma-like syntax)
1404
+ * Model query API for THIS repository's table (Drizzle v2 relational queries)
1330
1405
  * Scoped to only the table this repository manages.
1331
1406
  * Returns a type-safe wrapper around Drizzle's RelationalQueryBuilder.
1332
1407
  *
1333
1408
  * @example
1334
1409
  * ```typescript
1335
- * // Use relational queries with type safety
1410
+ * // Use relational queries with v2 object-based where syntax
1336
1411
  * const user = await this.model.findFirst({
1337
- * where: eq(users.id, id),
1412
+ * where: { id },
1338
1413
  * with: { posts: true, profile: true }
1339
1414
  * });
1340
1415
  * ```
1341
1416
  */
1342
1417
  get model() {
1343
- return this.database.drizzleClient.query[this.tableName];
1418
+ const query = this.database.drizzleClient.query;
1419
+ const queryKeys = Object.keys(query || {});
1420
+ this.logger.debug(`Looking for '${this.tableName}' in query keys: [${queryKeys.join(", ")}]`);
1421
+ const model = query[this.tableName];
1422
+ if (!model) {
1423
+ this.logger.error(`Table '${this.tableName}' not found in query object. Available: [${queryKeys.join(", ")}]`);
1424
+ }
1425
+ return model;
1344
1426
  }
1345
1427
  /**
1346
1428
  * Create a new repository instance
@@ -1360,9 +1442,11 @@ var PrimaryBaseRepository = class {
1360
1442
  constructor(database, table) {
1361
1443
  this.database = database;
1362
1444
  this.table = table;
1363
- this.tableName = (0, import_drizzle_orm2.getTableName)(table);
1445
+ const dbTableName = (0, import_drizzle_orm2.getTableName)(table);
1446
+ this.tableName = snakeToCamel(dbTableName);
1364
1447
  this.logger = new import_common11.Logger(this.constructor.name);
1365
1448
  this.logger.debug(`Initialized ${this.constructor.name}`);
1449
+ this.logger.debug(`Table name: '${dbTableName}' -> query key: '${this.tableName}'`);
1366
1450
  }
1367
1451
  /**
1368
1452
  * Create a new record
@@ -1396,21 +1480,31 @@ var PrimaryBaseRepository = class {
1396
1480
  */
1397
1481
  async findById(id) {
1398
1482
  this.logger.debug(`Finding record by ID: ${id}`);
1399
- const idColumn = this.table.id;
1400
1483
  return this.model.findFirst({
1401
- where: (0, import_drizzle_orm2.eq)(idColumn, id)
1484
+ where: {
1485
+ id
1486
+ }
1402
1487
  });
1403
1488
  }
1404
1489
  /**
1405
- * Find a single record with custom where clause
1490
+ * Find a single record with custom where clause (Drizzle v2 object-based syntax)
1406
1491
  *
1407
- * @param where - SQL condition
1492
+ * @param where - Object-based filter condition
1408
1493
  * @returns Promise resolving to the record or undefined if not found
1409
1494
  *
1410
1495
  * @example
1411
1496
  * ```typescript
1412
- * import { eq } from 'drizzle-orm';
1413
- * const user = await userRepository.findOne(eq(users.email, 'user@example.com'));
1497
+ * // Simple equality
1498
+ * const user = await userRepository.findOne({ email: 'user@example.com' });
1499
+ *
1500
+ * // With operators
1501
+ * const user = await userRepository.findOne({ age: { gte: 18 } });
1502
+ *
1503
+ * // Multiple conditions (AND)
1504
+ * const user = await userRepository.findOne({
1505
+ * email: 'user@example.com',
1506
+ * status: 'ACTIVE'
1507
+ * });
1414
1508
  * ```
1415
1509
  */
1416
1510
  async findOne(where) {
@@ -1420,25 +1514,33 @@ var PrimaryBaseRepository = class {
1420
1514
  });
1421
1515
  }
1422
1516
  /**
1423
- * Find multiple records
1517
+ * Find multiple records (Drizzle v2 object-based syntax)
1424
1518
  *
1425
1519
  * @param options - Query options (where, orderBy, limit, offset)
1426
1520
  * @returns Promise resolving to an array of records
1427
1521
  *
1428
1522
  * @example
1429
1523
  * ```typescript
1430
- * import { eq, desc } from 'drizzle-orm';
1431
- *
1432
1524
  * // Find all users
1433
1525
  * const users = await userRepository.findMany();
1434
1526
  *
1435
- * // Find with filtering and pagination
1527
+ * // Find with filtering and pagination (v2 object syntax)
1436
1528
  * const users = await userRepository.findMany({
1437
- * where: eq(users.accountStatus, 'ACTIVE'),
1438
- * orderBy: desc(users.createdAt),
1529
+ * where: { accountStatus: 'ACTIVE' },
1530
+ * orderBy: { createdAt: 'desc' },
1439
1531
  * limit: 10,
1440
1532
  * offset: 0
1441
1533
  * });
1534
+ *
1535
+ * // Multiple conditions
1536
+ * const users = await userRepository.findMany({
1537
+ * where: {
1538
+ * AND: [
1539
+ * { status: 'ACTIVE' },
1540
+ * { age: { gte: 18 } }
1541
+ * ]
1542
+ * }
1543
+ * });
1442
1544
  * ```
1443
1545
  */
1444
1546
  async findMany(options) {
@@ -3384,11 +3486,19 @@ LoggerModule = _ts_decorate17([
3384
3486
  ValidationException,
3385
3487
  VrittiAuthGuard,
3386
3488
  addCorrelationIdToResponse,
3489
+ configureApiSdk,
3387
3490
  correlationStorage,
3491
+ defineConfig,
3388
3492
  generateCorrelationId,
3493
+ getConfig,
3389
3494
  getCorrelationContext,
3390
3495
  getHttpStatusTitle,
3496
+ getJwtExpiry,
3497
+ getRefreshCookieOptions,
3498
+ hashToken,
3499
+ resetConfig,
3391
3500
  runWithCorrelationContext,
3392
- updateCorrelationContext
3501
+ updateCorrelationContext,
3502
+ verifyTokenHash
3393
3503
  });
3394
3504
  //# sourceMappingURL=index.cjs.map