@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 +201 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +267 -43
- package/dist/index.d.ts +267 -43
- package/dist/index.js +186 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
68
|
-
* Cookie name
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
528
|
-
*
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.logger.
|
|
551
|
-
|
|
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
|
-
*
|
|
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 (
|
|
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
|
|
1323
|
+
* // Use relational queries with v2 object-based where syntax
|
|
1257
1324
|
* const user = await this.model.findFirst({
|
|
1258
|
-
* where:
|
|
1325
|
+
* where: { id },
|
|
1259
1326
|
* with: { posts: true, profile: true }
|
|
1260
1327
|
* });
|
|
1261
1328
|
* ```
|
|
1262
1329
|
*/
|
|
1263
1330
|
get model() {
|
|
1264
|
-
|
|
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
|
-
|
|
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:
|
|
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 -
|
|
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
|
-
*
|
|
1334
|
-
* const user = await userRepository.findOne(
|
|
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:
|
|
1359
|
-
* orderBy: desc
|
|
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
|