@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.js CHANGED
@@ -1,6 +1,93 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
+ // src/config/index.ts
5
+ var defaultConfig = {
6
+ cookie: {
7
+ refreshCookieName: "vritti_refresh",
8
+ refreshCookieMaxAge: 30 * 24 * 60 * 60 * 1e3,
9
+ refreshCookiePath: "/",
10
+ refreshCookieSecure: process.env.NODE_ENV === "production",
11
+ refreshCookieSameSite: "strict"
12
+ },
13
+ jwt: {
14
+ accessTokenExpiry: "15m",
15
+ refreshTokenExpiry: "30d",
16
+ onboardingTokenExpiry: "24h",
17
+ validateTokenBinding: true
18
+ },
19
+ guard: {
20
+ tenantHeaderName: "x-tenant-id",
21
+ authHeaderName: "authorization",
22
+ tokenPrefix: "Bearer"
23
+ }
24
+ };
25
+ var currentConfig = {
26
+ ...defaultConfig
27
+ };
28
+ function defineConfig(config) {
29
+ return config;
30
+ }
31
+ __name(defineConfig, "defineConfig");
32
+ function configureApiSdk(userConfig) {
33
+ currentConfig = {
34
+ cookie: {
35
+ ...defaultConfig.cookie,
36
+ ...userConfig.cookie || {}
37
+ },
38
+ jwt: {
39
+ ...defaultConfig.jwt,
40
+ ...userConfig.jwt || {}
41
+ },
42
+ guard: {
43
+ ...defaultConfig.guard,
44
+ ...userConfig.guard || {}
45
+ }
46
+ };
47
+ }
48
+ __name(configureApiSdk, "configureApiSdk");
49
+ function getConfig() {
50
+ return currentConfig;
51
+ }
52
+ __name(getConfig, "getConfig");
53
+ function resetConfig() {
54
+ currentConfig = {
55
+ ...defaultConfig
56
+ };
57
+ }
58
+ __name(resetConfig, "resetConfig");
59
+ function getRefreshCookieOptions() {
60
+ return {
61
+ httpOnly: true,
62
+ secure: currentConfig.cookie.refreshCookieSecure,
63
+ sameSite: currentConfig.cookie.refreshCookieSameSite,
64
+ path: currentConfig.cookie.refreshCookiePath,
65
+ maxAge: currentConfig.cookie.refreshCookieMaxAge
66
+ };
67
+ }
68
+ __name(getRefreshCookieOptions, "getRefreshCookieOptions");
69
+ function getJwtExpiry() {
70
+ return {
71
+ access: currentConfig.jwt.accessTokenExpiry,
72
+ refresh: currentConfig.jwt.refreshTokenExpiry,
73
+ onboarding: currentConfig.jwt.onboardingTokenExpiry
74
+ };
75
+ }
76
+ __name(getJwtExpiry, "getJwtExpiry");
77
+
78
+ // src/auth/utils/token-hash.util.ts
79
+ import * as crypto from "crypto";
80
+ function hashToken(token) {
81
+ return crypto.createHash("sha256").update(token).digest("hex");
82
+ }
83
+ __name(hashToken, "hashToken");
84
+ function verifyTokenHash(token, expectedHash) {
85
+ const computedHash = hashToken(token);
86
+ if (computedHash.length !== expectedHash.length) return false;
87
+ return crypto.timingSafeEqual(Buffer.from(computedHash, "hex"), Buffer.from(expectedHash, "hex"));
88
+ }
89
+ __name(verifyTokenHash, "verifyTokenHash");
90
+
4
91
  // src/auth/auth-config.module.ts
5
92
  import { Global as Global2, Module as Module2 } from "@nestjs/common";
6
93
  import { ConfigModule, ConfigService as ConfigService2 } from "@nestjs/config";
@@ -64,17 +151,18 @@ var RequestService = class {
64
151
  return type === "Bearer" && token ? token : null;
65
152
  }
66
153
  /**
67
- * Extract refresh token from session-id cookie
68
- * Cookie name: session-id
154
+ * Extract refresh token from httpOnly cookie
155
+ * Cookie name is configurable via api-sdk config
69
156
  * @returns Refresh token or null if not found
70
157
  */
71
158
  getRefreshToken() {
72
159
  try {
73
160
  const cookies = this.request.cookies;
74
161
  if (cookies && typeof cookies === "object") {
75
- const sessionId = cookies["session-id"];
76
- if (sessionId) {
77
- return sessionId;
162
+ const config = getConfig();
163
+ const refreshToken = cookies[config.cookie.refreshCookieName];
164
+ if (refreshToken) {
165
+ return refreshToken;
78
166
  }
79
167
  }
80
168
  return null;
@@ -139,7 +227,6 @@ import { Injectable as Injectable3, Logger as Logger2, Scope as Scope2, Unauthor
139
227
  import { ConfigService } from "@nestjs/config";
140
228
  import { Reflector } from "@nestjs/core";
141
229
  import { JwtService } from "@nestjs/jwt";
142
- import * as jwt from "jsonwebtoken";
143
230
 
144
231
  // src/database/services/primary-database.service.ts
145
232
  import { Inject as Inject2, Injectable as Injectable2, InternalServerErrorException, Logger } from "@nestjs/common";
@@ -201,10 +288,14 @@ var PrimaryDatabaseService = class _PrimaryDatabaseService {
201
288
  connectionString: databaseUrl,
202
289
  max: this.options.maxConnections || 10
203
290
  });
291
+ this.logger.debug(`Schema keys passed to drizzle: [${Object.keys(this.options.drizzleSchema || {}).join(", ")}]`);
292
+ this.logger.debug(`Relations keys passed to drizzle: [${Object.keys(this.options.drizzleRelations || {}).join(", ")}]`);
204
293
  this.db = drizzle({
205
294
  client: this.pool,
206
- schema: this.options.drizzleSchema
295
+ schema: this.options.drizzleSchema,
296
+ relations: this.options.drizzleRelations
207
297
  });
298
+ this.logger.debug(`Drizzle query keys after init: [${Object.keys(this.db.query || {}).join(", ")}]`);
208
299
  await this.pool.query("SELECT 1");
209
300
  this.logger.log("Connected to primary database (tenant registry)");
210
301
  } catch (error) {
@@ -435,6 +526,7 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
435
526
  }
436
527
  const validatedToken2 = this.validateAccessToken(accessToken);
437
528
  this.logger.debug("Onboarding token validated successfully");
529
+ this.validateRefreshTokenBinding(context, validatedToken2);
438
530
  const userId2 = validatedToken2.userId;
439
531
  request.user = {
440
532
  id: userId2
@@ -447,13 +539,7 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
447
539
  }
448
540
  const validatedToken = this.validateAccessToken(accessToken);
449
541
  this.logger.debug("Access token validated successfully");
450
- const refreshToken = this.requestService.getRefreshToken();
451
- if (!refreshToken) {
452
- this.logger.warn("Refresh token (session-id) not found in cookies");
453
- throw new UnauthorizedException("Refresh token not found");
454
- }
455
- this.validateRefreshToken(refreshToken);
456
- this.logger.debug("Refresh token validated successfully");
542
+ this.validateRefreshTokenBinding(context, validatedToken);
457
543
  const userId = validatedToken.userId;
458
544
  request.user = {
459
545
  id: userId
@@ -524,60 +610,36 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
524
610
  }
525
611
  }
526
612
  /**
527
- * Validate refresh token with proper expiry checks
528
- * Throws UnauthorizedException if token is invalid or expired
529
- */
530
- validateRefreshToken(token) {
531
- const jwtSecret = this.configService.get("JWT_REFRESH_SECRET") || this.configService.get("JWT_SECRET");
532
- this.validateRefreshTokenWithSecret(token, jwtSecret);
533
- }
534
- /**
535
- * Helper to validate refresh token with specific secret
613
+ * Validate that the access token is bound to the refresh token in the cookie.
614
+ * This prevents token theft - a stolen access token is useless without the
615
+ * corresponding refresh token cookie.
616
+ *
617
+ * @param context - The execution context containing the request
618
+ * @param validatedToken - The decoded and validated JWT token
619
+ * @throws UnauthorizedException if token binding validation fails
536
620
  */
537
- validateRefreshTokenWithSecret(token, secret) {
538
- if (!secret) {
539
- this.logger.error("JWT secret not configured for refresh token validation");
540
- throw new UnauthorizedException("Server configuration error");
621
+ validateRefreshTokenBinding(context, validatedToken) {
622
+ const config = getConfig();
623
+ if (!config.jwt.validateTokenBinding) {
624
+ this.logger.debug("Token binding validation is disabled");
625
+ return;
541
626
  }
542
- try {
543
- const decoded = jwt.verify(token, secret, {
544
- algorithms: [
545
- "HS256",
546
- "HS512",
547
- "RS256"
548
- ]
549
- });
550
- this.logger.debug(`Refresh token decoded for user: ${decoded.userId}`);
551
- if (decoded.exp) {
552
- const expiryTime = decoded.exp * 1e3;
553
- const currentTime = Date.now();
554
- if (currentTime > expiryTime) {
555
- this.logger.warn("Refresh token has expired");
556
- throw new UnauthorizedException("Refresh token has expired. Please login again");
557
- }
558
- const timeRemaining = expiryTime - currentTime;
559
- this.logger.debug(`Refresh token valid for ${Math.floor(timeRemaining / 1e3)} more seconds`);
560
- }
561
- } catch (error) {
562
- if (error instanceof UnauthorizedException) {
563
- throw error;
564
- }
565
- const jwtError = error;
566
- if (jwtError?.name === "TokenExpiredError") {
567
- this.logger.warn(`Refresh token expired at: ${jwtError?.expiredAt}`);
568
- throw new UnauthorizedException("Refresh token has expired. Please login again");
569
- }
570
- if (jwtError?.name === "JsonWebTokenError") {
571
- this.logger.warn(`Refresh token verification failed: ${jwtError?.message}`);
572
- throw new UnauthorizedException("Invalid refresh token");
573
- }
574
- if (jwtError?.name === "NotBeforeError") {
575
- this.logger.warn("Refresh token used before valid (nbf claim)");
576
- throw new UnauthorizedException("Refresh token not yet valid");
577
- }
578
- this.logger.error("Unexpected error validating refresh token", error);
579
- throw new UnauthorizedException("Refresh token validation failed");
627
+ if (!validatedToken.refreshTokenHash) {
628
+ this.logger.debug("Token does not contain refreshTokenHash, skipping binding validation");
629
+ return;
630
+ }
631
+ const request = context.switchToHttp().getRequest();
632
+ const cookies = request.cookies || {};
633
+ const refreshToken = cookies[config.cookie.refreshCookieName];
634
+ if (!refreshToken) {
635
+ this.logger.warn("Session validation failed - refresh token cookie not found");
636
+ throw new UnauthorizedException("Session validation failed");
580
637
  }
638
+ if (!verifyTokenHash(refreshToken, validatedToken.refreshTokenHash)) {
639
+ this.logger.warn("Session validation failed - token binding mismatch");
640
+ throw new UnauthorizedException("Session validation failed");
641
+ }
642
+ this.logger.debug("Token binding validated successfully");
581
643
  }
582
644
  };
583
645
  VrittiAuthGuard = _ts_decorate4([
@@ -1226,6 +1288,10 @@ DatabaseModule = _ts_decorate10([
1226
1288
  // src/database/repositories/primary-base.repository.ts
1227
1289
  import { Logger as Logger6 } from "@nestjs/common";
1228
1290
  import { eq as eq2, sql, getTableName } from "drizzle-orm";
1291
+ function snakeToCamel(str) {
1292
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
1293
+ }
1294
+ __name(snakeToCamel, "snakeToCamel");
1229
1295
  var PrimaryBaseRepository = class {
1230
1296
  static {
1231
1297
  __name(this, "PrimaryBaseRepository");
@@ -1235,7 +1301,8 @@ var PrimaryBaseRepository = class {
1235
1301
  logger;
1236
1302
  /**
1237
1303
  * The table name extracted from the Drizzle table at runtime.
1238
- * Used to access the query API for this repository's table.
1304
+ * Stored in camelCase to match Drizzle's query object keys.
1305
+ * Example: 'email_verifications' -> 'emailVerifications'
1239
1306
  */
1240
1307
  tableName;
1241
1308
  /**
@@ -1247,21 +1314,28 @@ var PrimaryBaseRepository = class {
1247
1314
  return this.database.drizzleClient;
1248
1315
  }
1249
1316
  /**
1250
- * Model query API for THIS repository's table (Prisma-like syntax)
1317
+ * Model query API for THIS repository's table (Drizzle v2 relational queries)
1251
1318
  * Scoped to only the table this repository manages.
1252
1319
  * Returns a type-safe wrapper around Drizzle's RelationalQueryBuilder.
1253
1320
  *
1254
1321
  * @example
1255
1322
  * ```typescript
1256
- * // Use relational queries with type safety
1323
+ * // Use relational queries with v2 object-based where syntax
1257
1324
  * const user = await this.model.findFirst({
1258
- * where: eq(users.id, id),
1325
+ * where: { id },
1259
1326
  * with: { posts: true, profile: true }
1260
1327
  * });
1261
1328
  * ```
1262
1329
  */
1263
1330
  get model() {
1264
- return this.database.drizzleClient.query[this.tableName];
1331
+ const query = this.database.drizzleClient.query;
1332
+ const queryKeys = Object.keys(query || {});
1333
+ this.logger.debug(`Looking for '${this.tableName}' in query keys: [${queryKeys.join(", ")}]`);
1334
+ const model = query[this.tableName];
1335
+ if (!model) {
1336
+ this.logger.error(`Table '${this.tableName}' not found in query object. Available: [${queryKeys.join(", ")}]`);
1337
+ }
1338
+ return model;
1265
1339
  }
1266
1340
  /**
1267
1341
  * Create a new repository instance
@@ -1281,9 +1355,11 @@ var PrimaryBaseRepository = class {
1281
1355
  constructor(database, table) {
1282
1356
  this.database = database;
1283
1357
  this.table = table;
1284
- this.tableName = getTableName(table);
1358
+ const dbTableName = getTableName(table);
1359
+ this.tableName = snakeToCamel(dbTableName);
1285
1360
  this.logger = new Logger6(this.constructor.name);
1286
1361
  this.logger.debug(`Initialized ${this.constructor.name}`);
1362
+ this.logger.debug(`Table name: '${dbTableName}' -> query key: '${this.tableName}'`);
1287
1363
  }
1288
1364
  /**
1289
1365
  * Create a new record
@@ -1317,21 +1393,31 @@ var PrimaryBaseRepository = class {
1317
1393
  */
1318
1394
  async findById(id) {
1319
1395
  this.logger.debug(`Finding record by ID: ${id}`);
1320
- const idColumn = this.table.id;
1321
1396
  return this.model.findFirst({
1322
- where: eq2(idColumn, id)
1397
+ where: {
1398
+ id
1399
+ }
1323
1400
  });
1324
1401
  }
1325
1402
  /**
1326
- * Find a single record with custom where clause
1403
+ * Find a single record with custom where clause (Drizzle v2 object-based syntax)
1327
1404
  *
1328
- * @param where - SQL condition
1405
+ * @param where - Object-based filter condition
1329
1406
  * @returns Promise resolving to the record or undefined if not found
1330
1407
  *
1331
1408
  * @example
1332
1409
  * ```typescript
1333
- * import { eq } from 'drizzle-orm';
1334
- * const user = await userRepository.findOne(eq(users.email, 'user@example.com'));
1410
+ * // Simple equality
1411
+ * const user = await userRepository.findOne({ email: 'user@example.com' });
1412
+ *
1413
+ * // With operators
1414
+ * const user = await userRepository.findOne({ age: { gte: 18 } });
1415
+ *
1416
+ * // Multiple conditions (AND)
1417
+ * const user = await userRepository.findOne({
1418
+ * email: 'user@example.com',
1419
+ * status: 'ACTIVE'
1420
+ * });
1335
1421
  * ```
1336
1422
  */
1337
1423
  async findOne(where) {
@@ -1341,25 +1427,33 @@ var PrimaryBaseRepository = class {
1341
1427
  });
1342
1428
  }
1343
1429
  /**
1344
- * Find multiple records
1430
+ * Find multiple records (Drizzle v2 object-based syntax)
1345
1431
  *
1346
1432
  * @param options - Query options (where, orderBy, limit, offset)
1347
1433
  * @returns Promise resolving to an array of records
1348
1434
  *
1349
1435
  * @example
1350
1436
  * ```typescript
1351
- * import { eq, desc } from 'drizzle-orm';
1352
- *
1353
1437
  * // Find all users
1354
1438
  * const users = await userRepository.findMany();
1355
1439
  *
1356
- * // Find with filtering and pagination
1440
+ * // Find with filtering and pagination (v2 object syntax)
1357
1441
  * const users = await userRepository.findMany({
1358
- * where: eq(users.accountStatus, 'ACTIVE'),
1359
- * orderBy: desc(users.createdAt),
1442
+ * where: { accountStatus: 'ACTIVE' },
1443
+ * orderBy: { createdAt: 'desc' },
1360
1444
  * limit: 10,
1361
1445
  * offset: 0
1362
1446
  * });
1447
+ *
1448
+ * // Multiple conditions
1449
+ * const users = await userRepository.findMany({
1450
+ * where: {
1451
+ * AND: [
1452
+ * { status: 'ACTIVE' },
1453
+ * { age: { gte: 18 } }
1454
+ * ]
1455
+ * }
1456
+ * });
1363
1457
  * ```
1364
1458
  */
1365
1459
  async findMany(options) {
@@ -3304,11 +3398,19 @@ export {
3304
3398
  ValidationException,
3305
3399
  VrittiAuthGuard,
3306
3400
  addCorrelationIdToResponse,
3401
+ configureApiSdk,
3307
3402
  correlationStorage,
3403
+ defineConfig,
3308
3404
  generateCorrelationId,
3405
+ getConfig,
3309
3406
  getCorrelationContext,
3310
3407
  getHttpStatusTitle,
3408
+ getJwtExpiry,
3409
+ getRefreshCookieOptions,
3410
+ hashToken,
3411
+ resetConfig,
3311
3412
  runWithCorrelationContext,
3312
- updateCorrelationContext
3413
+ updateCorrelationContext,
3414
+ verifyTokenHash
3313
3415
  };
3314
3416
  //# sourceMappingURL=index.js.map