@vritti/api-sdk 0.1.1 → 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";
@@ -439,6 +526,7 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
439
526
  }
440
527
  const validatedToken2 = this.validateAccessToken(accessToken);
441
528
  this.logger.debug("Onboarding token validated successfully");
529
+ this.validateRefreshTokenBinding(context, validatedToken2);
442
530
  const userId2 = validatedToken2.userId;
443
531
  request.user = {
444
532
  id: userId2
@@ -451,13 +539,7 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
451
539
  }
452
540
  const validatedToken = this.validateAccessToken(accessToken);
453
541
  this.logger.debug("Access token validated successfully");
454
- const refreshToken = this.requestService.getRefreshToken();
455
- if (!refreshToken) {
456
- this.logger.warn("Refresh token (session-id) not found in cookies");
457
- throw new UnauthorizedException("Refresh token not found");
458
- }
459
- this.validateRefreshToken(refreshToken);
460
- this.logger.debug("Refresh token validated successfully");
542
+ this.validateRefreshTokenBinding(context, validatedToken);
461
543
  const userId = validatedToken.userId;
462
544
  request.user = {
463
545
  id: userId
@@ -528,60 +610,36 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
528
610
  }
529
611
  }
530
612
  /**
531
- * Validate refresh token with proper expiry checks
532
- * Throws UnauthorizedException if token is invalid or expired
533
- */
534
- validateRefreshToken(token) {
535
- const jwtSecret = this.configService.get("JWT_REFRESH_SECRET") || this.configService.get("JWT_SECRET");
536
- this.validateRefreshTokenWithSecret(token, jwtSecret);
537
- }
538
- /**
539
- * 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
540
620
  */
541
- validateRefreshTokenWithSecret(token, secret) {
542
- if (!secret) {
543
- this.logger.error("JWT secret not configured for refresh token validation");
544
- 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;
545
626
  }
546
- try {
547
- const decoded = jwt.verify(token, secret, {
548
- algorithms: [
549
- "HS256",
550
- "HS512",
551
- "RS256"
552
- ]
553
- });
554
- this.logger.debug(`Refresh token decoded for user: ${decoded.userId}`);
555
- if (decoded.exp) {
556
- const expiryTime = decoded.exp * 1e3;
557
- const currentTime = Date.now();
558
- if (currentTime > expiryTime) {
559
- this.logger.warn("Refresh token has expired");
560
- throw new UnauthorizedException("Refresh token has expired. Please login again");
561
- }
562
- const timeRemaining = expiryTime - currentTime;
563
- this.logger.debug(`Refresh token valid for ${Math.floor(timeRemaining / 1e3)} more seconds`);
564
- }
565
- } catch (error) {
566
- if (error instanceof UnauthorizedException) {
567
- throw error;
568
- }
569
- const jwtError = error;
570
- if (jwtError?.name === "TokenExpiredError") {
571
- this.logger.warn(`Refresh token expired at: ${jwtError?.expiredAt}`);
572
- throw new UnauthorizedException("Refresh token has expired. Please login again");
573
- }
574
- if (jwtError?.name === "JsonWebTokenError") {
575
- this.logger.warn(`Refresh token verification failed: ${jwtError?.message}`);
576
- throw new UnauthorizedException("Invalid refresh token");
577
- }
578
- if (jwtError?.name === "NotBeforeError") {
579
- this.logger.warn("Refresh token used before valid (nbf claim)");
580
- throw new UnauthorizedException("Refresh token not yet valid");
581
- }
582
- this.logger.error("Unexpected error validating refresh token", error);
583
- 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");
584
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");
585
643
  }
586
644
  };
587
645
  VrittiAuthGuard = _ts_decorate4([
@@ -1230,6 +1288,10 @@ DatabaseModule = _ts_decorate10([
1230
1288
  // src/database/repositories/primary-base.repository.ts
1231
1289
  import { Logger as Logger6 } from "@nestjs/common";
1232
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");
1233
1295
  var PrimaryBaseRepository = class {
1234
1296
  static {
1235
1297
  __name(this, "PrimaryBaseRepository");
@@ -1239,7 +1301,8 @@ var PrimaryBaseRepository = class {
1239
1301
  logger;
1240
1302
  /**
1241
1303
  * The table name extracted from the Drizzle table at runtime.
1242
- * 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'
1243
1306
  */
1244
1307
  tableName;
1245
1308
  /**
@@ -1292,10 +1355,11 @@ var PrimaryBaseRepository = class {
1292
1355
  constructor(database, table) {
1293
1356
  this.database = database;
1294
1357
  this.table = table;
1295
- this.tableName = getTableName(table);
1358
+ const dbTableName = getTableName(table);
1359
+ this.tableName = snakeToCamel(dbTableName);
1296
1360
  this.logger = new Logger6(this.constructor.name);
1297
1361
  this.logger.debug(`Initialized ${this.constructor.name}`);
1298
- this.logger.debug(`Table name from getTableName: '${this.tableName}'`);
1362
+ this.logger.debug(`Table name: '${dbTableName}' -> query key: '${this.tableName}'`);
1299
1363
  }
1300
1364
  /**
1301
1365
  * Create a new record
@@ -3334,11 +3398,19 @@ export {
3334
3398
  ValidationException,
3335
3399
  VrittiAuthGuard,
3336
3400
  addCorrelationIdToResponse,
3401
+ configureApiSdk,
3337
3402
  correlationStorage,
3403
+ defineConfig,
3338
3404
  generateCorrelationId,
3405
+ getConfig,
3339
3406
  getCorrelationContext,
3340
3407
  getHttpStatusTitle,
3408
+ getJwtExpiry,
3409
+ getRefreshCookieOptions,
3410
+ hashToken,
3411
+ resetConfig,
3341
3412
  runWithCorrelationContext,
3342
- updateCorrelationContext
3413
+ updateCorrelationContext,
3414
+ verifyTokenHash
3343
3415
  };
3344
3416
  //# sourceMappingURL=index.js.map