@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.d.cts CHANGED
@@ -10,6 +10,183 @@ import { FastifyRequest, FastifyReply } from 'fastify';
10
10
  import { Observable } from 'rxjs';
11
11
  import { AsyncLocalStorage } from 'node:async_hooks';
12
12
 
13
+ /**
14
+ * api-sdk Configuration System
15
+ *
16
+ * Similar to quantum-ui's config pattern - provides a type-safe configuration system
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // In vritti-api-nexus/src/main.ts
21
+ * import { configureApiSdk } from '@vritti/api-sdk';
22
+ *
23
+ * configureApiSdk({
24
+ * cookie: {
25
+ * refreshCookieName: 'vritti_refresh',
26
+ * refreshCookieMaxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
27
+ * },
28
+ * jwt: {
29
+ * accessTokenExpiry: '15m',
30
+ * refreshTokenExpiry: '30d',
31
+ * validateTokenBinding: true,
32
+ * },
33
+ * guard: {
34
+ * tenantHeaderName: 'x-tenant-id',
35
+ * },
36
+ * });
37
+ * ```
38
+ */
39
+ /**
40
+ * Cookie configuration options
41
+ */
42
+ interface CookieConfig {
43
+ /**
44
+ * The name of the httpOnly cookie containing the refresh token
45
+ * @default 'vritti_refresh'
46
+ */
47
+ refreshCookieName: string;
48
+ /**
49
+ * Max age of the refresh cookie in milliseconds
50
+ * @default 2592000000 (30 days)
51
+ */
52
+ refreshCookieMaxAge: number;
53
+ /**
54
+ * Cookie path
55
+ * @default '/'
56
+ */
57
+ refreshCookiePath: string;
58
+ /**
59
+ * Whether the cookie is secure (HTTPS only)
60
+ * @default true in production
61
+ */
62
+ refreshCookieSecure: boolean;
63
+ /**
64
+ * SameSite attribute for the cookie
65
+ * @default 'strict'
66
+ */
67
+ refreshCookieSameSite: 'strict' | 'lax' | 'none';
68
+ }
69
+ /**
70
+ * JWT token configuration options
71
+ */
72
+ interface JwtConfig {
73
+ /**
74
+ * Access token expiry time
75
+ * @default '15m'
76
+ */
77
+ accessTokenExpiry: string;
78
+ /**
79
+ * Refresh token expiry time
80
+ * @default '30d'
81
+ */
82
+ refreshTokenExpiry: string;
83
+ /**
84
+ * Onboarding token expiry time
85
+ * @default '24h'
86
+ */
87
+ onboardingTokenExpiry: string;
88
+ /**
89
+ * Whether to validate refresh token binding (hash in access token)
90
+ * @default true
91
+ */
92
+ validateTokenBinding: boolean;
93
+ }
94
+ /**
95
+ * Auth guard configuration options
96
+ */
97
+ interface GuardConfig {
98
+ /**
99
+ * Header name for tenant ID
100
+ * @default 'x-tenant-id'
101
+ */
102
+ tenantHeaderName: string;
103
+ /**
104
+ * Header name for authorization
105
+ * @default 'authorization'
106
+ */
107
+ authHeaderName: string;
108
+ /**
109
+ * Token prefix (e.g., 'Bearer')
110
+ * @default 'Bearer'
111
+ */
112
+ tokenPrefix: string;
113
+ }
114
+ /**
115
+ * Complete api-sdk configuration interface
116
+ */
117
+ interface ApiSdkConfig {
118
+ /**
119
+ * Cookie configuration
120
+ */
121
+ cookie?: Partial<CookieConfig>;
122
+ /**
123
+ * JWT token configuration
124
+ */
125
+ jwt?: Partial<JwtConfig>;
126
+ /**
127
+ * Auth guard configuration
128
+ */
129
+ guard?: Partial<GuardConfig>;
130
+ }
131
+ /**
132
+ * Full configuration type with all properties required
133
+ */
134
+ interface FullConfig {
135
+ cookie: CookieConfig;
136
+ jwt: JwtConfig;
137
+ guard: GuardConfig;
138
+ }
139
+ /**
140
+ * Helper function to define configuration with type safety
141
+ * Similar to Tailwind's defineConfig()
142
+ */
143
+ declare function defineConfig(config: ApiSdkConfig): ApiSdkConfig;
144
+ /**
145
+ * Configure api-sdk with user settings
146
+ * This should be called once in the application's bootstrap (main.ts)
147
+ */
148
+ declare function configureApiSdk(userConfig: ApiSdkConfig): void;
149
+ /**
150
+ * Get the current configuration
151
+ */
152
+ declare function getConfig(): FullConfig;
153
+ /**
154
+ * Reset configuration to defaults (for testing)
155
+ */
156
+ declare function resetConfig(): void;
157
+ /**
158
+ * Get refresh cookie options (convenience method)
159
+ */
160
+ declare function getRefreshCookieOptions(): {
161
+ httpOnly: boolean;
162
+ secure: boolean;
163
+ sameSite: "strict" | "lax" | "none";
164
+ path: string;
165
+ maxAge: number;
166
+ };
167
+ /**
168
+ * Get JWT expiry settings (convenience method)
169
+ */
170
+ declare function getJwtExpiry(): {
171
+ access: string;
172
+ refresh: string;
173
+ onboarding: string;
174
+ };
175
+
176
+ /**
177
+ * Hash a token using SHA-256
178
+ * @param token The token to hash
179
+ * @returns The hex-encoded SHA-256 hash
180
+ */
181
+ declare function hashToken(token: string): string;
182
+ /**
183
+ * Verify a token against its expected hash using constant-time comparison
184
+ * @param token The token to verify
185
+ * @param expectedHash The expected SHA-256 hash
186
+ * @returns true if the token matches the hash
187
+ */
188
+ declare function verifyTokenHash(token: string, expectedHash: string): boolean;
189
+
13
190
  /**
14
191
  * Global authentication configuration module
15
192
  *
@@ -151,11 +328,17 @@ interface DatabaseModuleOptions {
151
328
  */
152
329
  primaryDb: PrimaryDbConfig;
153
330
  /**
154
- * Drizzle schema object containing all tables and relations
331
+ * Drizzle schema object containing all tables
155
332
  * Import your schema from db/schema/index.ts and pass it here
156
333
  * @example import * as schema from '@/db/schema'
157
334
  */
158
335
  drizzleSchema: RegisteredSchema;
336
+ /**
337
+ * Drizzle relations object from defineRelations()
338
+ * Required for relational queries (db.query.*.findFirst/findMany)
339
+ * @example import { relations } from '@/db/schema'
340
+ */
341
+ drizzleRelations?: Record<string, any>;
159
342
  /**
160
343
  * Connection cache TTL in milliseconds
161
344
  * Idle connections will be closed after this period
@@ -556,22 +739,46 @@ declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
556
739
  }
557
740
 
558
741
  /**
559
- * Type-safe wrapper for Drizzle's RelationalQueryBuilder.
742
+ * Drizzle ORM v2 object-based where filter type.
743
+ * Supports simple equality, operators, AND/OR/NOT, and RAW SQL.
744
+ *
745
+ * @example
746
+ * ```typescript
747
+ * // Simple equality
748
+ * { email: 'user@example.com' }
749
+ *
750
+ * // With operators
751
+ * { age: { gt: 18, lt: 65 } }
752
+ *
753
+ * // AND/OR combinations
754
+ * { AND: [{ status: 'ACTIVE' }, { age: { gte: 18 } }] }
755
+ *
756
+ * // RAW SQL expression
757
+ * { RAW: (table) => sql`${table.email} ILIKE '%@gmail.com'` }
758
+ * ```
759
+ */
760
+ type RelationsWhereFilter = Record<string, any>;
761
+ /**
762
+ * Type-safe wrapper for Drizzle's RelationalQueryBuilder (v2 API).
560
763
  * This interface matches the method signatures of RelationalQueryBuilder
561
764
  * but properly binds the TSelect generic for type safety.
562
765
  *
563
766
  * We use this instead of RelationalQueryBuilder directly because
564
767
  * TypeScript cannot infer TSelect from the generic base repository context.
768
+ *
769
+ * @remarks
770
+ * Drizzle ORM v2 uses object-based `where` filters instead of SQL expressions.
771
+ * See: https://orm.drizzle.team/docs/relations-v1-v2
565
772
  */
566
773
  interface TypedRelationalQueryBuilder<TSelect> {
567
774
  findFirst(config?: {
568
- where?: SQL;
775
+ where?: RelationsWhereFilter;
569
776
  with?: Record<string, unknown>;
570
777
  columns?: Record<string, boolean>;
571
778
  }): Promise<TSelect | undefined>;
572
779
  findMany(config?: {
573
- where?: SQL;
574
- orderBy?: SQL;
780
+ where?: RelationsWhereFilter;
781
+ orderBy?: Record<string, 'asc' | 'desc'>;
575
782
  limit?: number;
576
783
  offset?: number;
577
784
  with?: Record<string, unknown>;
@@ -610,17 +817,17 @@ interface TypedRelationalQueryBuilder<TSelect> {
610
817
  * super(database, users);
611
818
  * }
612
819
  *
613
- * // Use Prisma-like relational query syntax (recommended)
820
+ * // Use Drizzle v2 object-based where syntax (recommended)
614
821
  * async findByEmail(email: string): Promise<User | undefined> {
615
822
  * return this.model.findFirst({
616
- * where: eq(users.email, email),
823
+ * where: { email },
617
824
  * });
618
825
  * }
619
826
  *
620
- * // Use Prisma-like with relations
827
+ * // With relations
621
828
  * async findWithRelations(id: string): Promise<User | undefined> {
622
829
  * return this.model.findFirst({
623
- * where: eq(users.id, id),
830
+ * where: { id },
624
831
  * with: { posts: true, profile: true }
625
832
  * });
626
833
  * }
@@ -633,7 +840,8 @@ declare abstract class PrimaryBaseRepository<TTable extends PgTable, TInsert = I
633
840
  protected readonly logger: Logger;
634
841
  /**
635
842
  * The table name extracted from the Drizzle table at runtime.
636
- * Used to access the query API for this repository's table.
843
+ * Stored in camelCase to match Drizzle's query object keys.
844
+ * Example: 'email_verifications' -> 'emailVerifications'
637
845
  */
638
846
  private readonly tableName;
639
847
  /**
@@ -643,15 +851,15 @@ declare abstract class PrimaryBaseRepository<TTable extends PgTable, TInsert = I
643
851
  */
644
852
  protected get db(): TypedDrizzleClient;
645
853
  /**
646
- * Model query API for THIS repository's table (Prisma-like syntax)
854
+ * Model query API for THIS repository's table (Drizzle v2 relational queries)
647
855
  * Scoped to only the table this repository manages.
648
856
  * Returns a type-safe wrapper around Drizzle's RelationalQueryBuilder.
649
857
  *
650
858
  * @example
651
859
  * ```typescript
652
- * // Use relational queries with type safety
860
+ * // Use relational queries with v2 object-based where syntax
653
861
  * const user = await this.model.findFirst({
654
- * where: eq(users.id, id),
862
+ * where: { id },
655
863
  * with: { posts: true, profile: true }
656
864
  * });
657
865
  * ```
@@ -701,43 +909,60 @@ declare abstract class PrimaryBaseRepository<TTable extends PgTable, TInsert = I
701
909
  */
702
910
  findById(id: string): Promise<TSelect | undefined>;
703
911
  /**
704
- * Find a single record with custom where clause
912
+ * Find a single record with custom where clause (Drizzle v2 object-based syntax)
705
913
  *
706
- * @param where - SQL condition
914
+ * @param where - Object-based filter condition
707
915
  * @returns Promise resolving to the record or undefined if not found
708
916
  *
709
917
  * @example
710
918
  * ```typescript
711
- * import { eq } from 'drizzle-orm';
712
- * const user = await userRepository.findOne(eq(users.email, 'user@example.com'));
919
+ * // Simple equality
920
+ * const user = await userRepository.findOne({ email: 'user@example.com' });
921
+ *
922
+ * // With operators
923
+ * const user = await userRepository.findOne({ age: { gte: 18 } });
924
+ *
925
+ * // Multiple conditions (AND)
926
+ * const user = await userRepository.findOne({
927
+ * email: 'user@example.com',
928
+ * status: 'ACTIVE'
929
+ * });
713
930
  * ```
714
931
  */
715
- findOne(where: SQL): Promise<TSelect | undefined>;
932
+ findOne(where: RelationsWhereFilter): Promise<TSelect | undefined>;
716
933
  /**
717
- * Find multiple records
934
+ * Find multiple records (Drizzle v2 object-based syntax)
718
935
  *
719
936
  * @param options - Query options (where, orderBy, limit, offset)
720
937
  * @returns Promise resolving to an array of records
721
938
  *
722
939
  * @example
723
940
  * ```typescript
724
- * import { eq, desc } from 'drizzle-orm';
725
- *
726
941
  * // Find all users
727
942
  * const users = await userRepository.findMany();
728
943
  *
729
- * // Find with filtering and pagination
944
+ * // Find with filtering and pagination (v2 object syntax)
730
945
  * const users = await userRepository.findMany({
731
- * where: eq(users.accountStatus, 'ACTIVE'),
732
- * orderBy: desc(users.createdAt),
946
+ * where: { accountStatus: 'ACTIVE' },
947
+ * orderBy: { createdAt: 'desc' },
733
948
  * limit: 10,
734
949
  * offset: 0
735
950
  * });
951
+ *
952
+ * // Multiple conditions
953
+ * const users = await userRepository.findMany({
954
+ * where: {
955
+ * AND: [
956
+ * { status: 'ACTIVE' },
957
+ * { age: { gte: 18 } }
958
+ * ]
959
+ * }
960
+ * });
736
961
  * ```
737
962
  */
738
963
  findMany(options?: {
739
- where?: SQL;
740
- orderBy?: SQL;
964
+ where?: RelationsWhereFilter;
965
+ orderBy?: Record<string, 'asc' | 'desc'>;
741
966
  limit?: number;
742
967
  offset?: number;
743
968
  }): Promise<TSelect[]>;
@@ -1138,8 +1363,8 @@ declare class RequestService {
1138
1363
  */
1139
1364
  getAccessToken(): string | null;
1140
1365
  /**
1141
- * Extract refresh token from session-id cookie
1142
- * Cookie name: session-id
1366
+ * Extract refresh token from httpOnly cookie
1367
+ * Cookie name is configurable via api-sdk config
1143
1368
  * @returns Refresh token or null if not found
1144
1369
  */
1145
1370
  getRefreshToken(): string | null;
@@ -1157,27 +1382,27 @@ declare class RequestService {
1157
1382
  }
1158
1383
 
1159
1384
  /**
1160
- * Vritti Authentication Guard - Validates JWT tokens and tenant context
1385
+ * Vritti Authentication Guard - Validates JWT access tokens and tenant context
1161
1386
  *
1162
- * This guard performs comprehensive validation and attaches user data to request.
1387
+ * This guard performs access token validation and attaches user data to request.
1388
+ * NOTE: Refresh tokens are NOT validated here - they are only validated in
1389
+ * /auth/token and /auth/refresh endpoints (session.service.ts).
1163
1390
  *
1164
1391
  * Validation Flow:
1165
1392
  * 1. Checks if endpoint is marked with @Public() decorator → skip all validation
1166
1393
  * 2. Checks if endpoint is marked with @Onboarding() decorator:
1167
1394
  * - Requires token type='onboarding'
1168
1395
  * - Validates JWT signature and expiry only
1169
- * - Skips tenant and refresh token validation
1396
+ * - Skips tenant validation
1170
1397
  * - Attaches user data to request.user
1171
1398
  * 3. For regular endpoints (no decorator):
1172
1399
  * - Rejects tokens with type='onboarding'
1173
1400
  * - Validates access token (JWT signature, expiry, nbf)
1174
- * - Validates refresh token from session-id cookie
1175
1401
  * - Validates tenant exists and is ACTIVE
1176
1402
  * - Attaches user data to request.user
1177
1403
  *
1178
1404
  * Token Format:
1179
1405
  * - Access Token: "Authorization: Bearer <jwt_token>"
1180
- * - Refresh Token: "session-id" cookie
1181
1406
  *
1182
1407
  * Token Types:
1183
1408
  * - type='onboarding': Limited access during registration flow (@Onboarding endpoints only)
@@ -1185,11 +1410,9 @@ declare class RequestService {
1185
1410
  *
1186
1411
  * Environment Variables Required:
1187
1412
  * - JWT_SECRET: Secret key to verify access tokens (required)
1188
- * - JWT_REFRESH_SECRET: Secret key for refresh tokens (optional, falls back to JWT_SECRET)
1189
1413
  *
1190
1414
  * Error Responses:
1191
1415
  * - 401: Invalid/expired access token
1192
- * - 401: Invalid/expired refresh token
1193
1416
  * - 401: Tenant not found or inactive
1194
1417
  * - 401: Tenant identifier not found
1195
1418
  * - 401: Token type mismatch (onboarding token on regular endpoint or vice versa)
@@ -1237,14 +1460,15 @@ declare class VrittiAuthGuard implements CanActivate {
1237
1460
  */
1238
1461
  private validateAccessToken;
1239
1462
  /**
1240
- * Validate refresh token with proper expiry checks
1241
- * Throws UnauthorizedException if token is invalid or expired
1242
- */
1243
- private validateRefreshToken;
1244
- /**
1245
- * Helper to validate refresh token with specific secret
1463
+ * Validate that the access token is bound to the refresh token in the cookie.
1464
+ * This prevents token theft - a stolen access token is useless without the
1465
+ * corresponding refresh token cookie.
1466
+ *
1467
+ * @param context - The execution context containing the request
1468
+ * @param validatedToken - The decoded and validated JWT token
1469
+ * @throws UnauthorizedException if token binding validation fails
1246
1470
  */
1247
- private validateRefreshTokenWithSecret;
1471
+ private validateRefreshTokenBinding;
1248
1472
  }
1249
1473
 
1250
1474
  /**
@@ -2270,4 +2494,4 @@ declare function generateCorrelationId(): string;
2270
2494
  */
2271
2495
  declare function addCorrelationIdToResponse(reply: FastifyReply, correlationId: string, headerName?: string): void;
2272
2496
 
2273
- export { type ApiErrorResponse, AuthConfigModule, BadGatewayException, BadRequestException, BaseFieldException, ConflictException, type CorrelationContext, CorrelationIdMiddleware, CsrfGuard, DEFAULT_CORRELATION_HEADER, DatabaseModule, type DatabaseModuleOptions, type FieldError, ForbiddenException, GoneException, HttpExceptionFilter, HttpLoggerInterceptor, type HttpLoggerOptions, HttpModule, InternalServerErrorException, LOGGER_MODULE_OPTIONS, type LogFormat, type LogLevel, type LogMetadata, LoggerModule, type LoggerModuleAsyncOptions, type LoggerModuleOptions, type LoggerOptionsFactory, LoggerService, MethodNotAllowedException, NotAcceptableException, NotFoundException, NotImplementedException, Onboarding, PayloadTooLargeException, PrimaryBaseRepository, PrimaryDatabaseService, type PrimaryDbConfig, type ProblemDetails, Public, type RegisteredSchema, RequestTimeoutException, type SchemaRegistry, ServiceUnavailableException, Tenant, TenantBaseRepository, TenantContextService, TenantDatabaseService, type TenantInfo, TooManyRequestsException, type TypedDrizzleClient, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, ValidationException, VrittiAuthGuard, addCorrelationIdToResponse, correlationStorage, generateCorrelationId, getCorrelationContext, getHttpStatusTitle, runWithCorrelationContext, updateCorrelationContext };
2497
+ export { type ApiErrorResponse, type ApiSdkConfig, AuthConfigModule, BadGatewayException, BadRequestException, BaseFieldException, ConflictException, type CookieConfig, type CorrelationContext, CorrelationIdMiddleware, CsrfGuard, DEFAULT_CORRELATION_HEADER, DatabaseModule, type DatabaseModuleOptions, type FieldError, ForbiddenException, GoneException, type GuardConfig, HttpExceptionFilter, HttpLoggerInterceptor, type HttpLoggerOptions, HttpModule, InternalServerErrorException, type JwtConfig, LOGGER_MODULE_OPTIONS, type LogFormat, type LogLevel, type LogMetadata, LoggerModule, type LoggerModuleAsyncOptions, type LoggerModuleOptions, type LoggerOptionsFactory, LoggerService, MethodNotAllowedException, NotAcceptableException, NotFoundException, NotImplementedException, Onboarding, PayloadTooLargeException, PrimaryBaseRepository, PrimaryDatabaseService, type PrimaryDbConfig, type ProblemDetails, Public, type RegisteredSchema, RequestTimeoutException, type SchemaRegistry, ServiceUnavailableException, Tenant, TenantBaseRepository, TenantContextService, TenantDatabaseService, type TenantInfo, TooManyRequestsException, type TypedDrizzleClient, UnauthorizedException, UnprocessableEntityException, UnsupportedMediaTypeException, ValidationException, VrittiAuthGuard, addCorrelationIdToResponse, configureApiSdk, correlationStorage, defineConfig, generateCorrelationId, getConfig, getCorrelationContext, getHttpStatusTitle, getJwtExpiry, getRefreshCookieOptions, hashToken, resetConfig, runWithCorrelationContext, updateCorrelationContext, verifyTokenHash };