@lenne.tech/nest-server 11.10.0 → 11.10.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.
Files changed (34) hide show
  1. package/dist/core/modules/auth/guards/auth.guard.d.ts +2 -2
  2. package/dist/core/modules/auth/guards/auth.guard.js +68 -8
  3. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  4. package/dist/core/modules/auth/guards/roles.guard.d.ts +3 -4
  5. package/dist/core/modules/auth/guards/roles.guard.js +64 -159
  6. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  7. package/dist/core/modules/better-auth/better-auth-token.service.d.ts +21 -0
  8. package/dist/core/modules/better-auth/better-auth-token.service.js +153 -0
  9. package/dist/core/modules/better-auth/better-auth-token.service.js.map +1 -0
  10. package/dist/core/modules/better-auth/better-auth.types.d.ts +13 -0
  11. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -1
  12. package/dist/core/modules/better-auth/core-better-auth.module.d.ts +2 -0
  13. package/dist/core/modules/better-auth/core-better-auth.module.js +33 -4
  14. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  15. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +1 -0
  16. package/dist/core/modules/better-auth/core-better-auth.service.js +4 -0
  17. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  18. package/dist/core/modules/better-auth/index.d.ts +1 -0
  19. package/dist/core/modules/better-auth/index.js +1 -0
  20. package/dist/core/modules/better-auth/index.js.map +1 -1
  21. package/dist/core.module.js +1 -0
  22. package/dist/core.module.js.map +1 -1
  23. package/dist/tsconfig.build.tsbuildinfo +1 -1
  24. package/package.json +3 -3
  25. package/src/core/modules/auth/guards/auth.guard.ts +136 -23
  26. package/src/core/modules/auth/guards/roles.guard.ts +119 -239
  27. package/src/core/modules/better-auth/better-auth-token.service.ts +241 -0
  28. package/src/core/modules/better-auth/better-auth.types.ts +37 -0
  29. package/src/core/modules/better-auth/core-better-auth.controller.ts +1 -1
  30. package/src/core/modules/better-auth/core-better-auth.module.ts +51 -4
  31. package/src/core/modules/better-auth/core-better-auth.resolver.ts +1 -1
  32. package/src/core/modules/better-auth/core-better-auth.service.ts +13 -0
  33. package/src/core/modules/better-auth/index.ts +1 -0
  34. package/src/core.module.ts +3 -0
@@ -0,0 +1,241 @@
1
+ import { Injectable, Logger, Optional } from '@nestjs/common';
2
+ import { InjectConnection } from '@nestjs/mongoose';
3
+ import { Connection, Types } from 'mongoose';
4
+
5
+ import { BetterAuthenticatedUser } from './better-auth.types';
6
+ import { CoreBetterAuthService } from './core-better-auth.service';
7
+
8
+ /**
9
+ * Result of token extraction from a request
10
+ */
11
+ export interface TokenExtractionResult {
12
+ /** Source of the token (header or cookie) */
13
+ source: 'cookie' | 'header' | null;
14
+ /** The extracted token, if found */
15
+ token: null | string;
16
+ }
17
+
18
+ /**
19
+ * BetterAuthTokenService provides centralized token extraction and user loading
20
+ * for BetterAuth authentication.
21
+ *
22
+ * This service consolidates the token verification logic that was previously
23
+ * duplicated in AuthGuard and RolesGuard, providing:
24
+ * - Token extraction from Authorization header or cookies
25
+ * - JWT token verification via BetterAuth
26
+ * - Session token verification via database lookup
27
+ * - User loading from MongoDB with hasRole() capability
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * const token = this.tokenService.extractTokenFromRequest(request);
32
+ * if (token) {
33
+ * const user = await this.tokenService.verifyAndLoadUser(token);
34
+ * if (user) {
35
+ * request.user = user;
36
+ * }
37
+ * }
38
+ * ```
39
+ */
40
+ @Injectable()
41
+ export class BetterAuthTokenService {
42
+ private readonly logger = new Logger(BetterAuthTokenService.name);
43
+
44
+ constructor(
45
+ @Optional() private readonly betterAuthService?: CoreBetterAuthService,
46
+ @Optional() @InjectConnection() private readonly connection?: Connection,
47
+ ) {}
48
+
49
+ /**
50
+ * Extracts a token from the request's Authorization header or cookies.
51
+ *
52
+ * Checks in order:
53
+ * 1. Authorization header (Bearer token)
54
+ * 2. Session cookies (iam.session_token, better-auth.session_token, token)
55
+ *
56
+ * @param request - HTTP request object with headers and cookies
57
+ * @returns Token extraction result with token and source
58
+ */
59
+ extractTokenFromRequest(request: {
60
+ cookies?: Record<string, string>;
61
+ headers?: Record<string, string | string[] | undefined>;
62
+ }): TokenExtractionResult {
63
+ // Try Authorization header first
64
+ const authHeader = request.headers?.authorization || request.headers?.Authorization;
65
+ const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
66
+
67
+ if (headerValue) {
68
+ if (headerValue.startsWith('Bearer ') || headerValue.startsWith('bearer ')) {
69
+ return { source: 'header', token: headerValue.substring(7) };
70
+ }
71
+ }
72
+
73
+ // Try cookies
74
+ if (request.cookies && this.betterAuthService) {
75
+ const cookieName = this.betterAuthService.getSessionCookieName();
76
+ const token =
77
+ request.cookies[cookieName] ||
78
+ request.cookies['better-auth.session_token'] ||
79
+ request.cookies['token'] ||
80
+ undefined;
81
+
82
+ if (token) {
83
+ return { source: 'cookie', token };
84
+ }
85
+ }
86
+
87
+ return { source: null, token: null };
88
+ }
89
+
90
+ /**
91
+ * Verifies a token (JWT or session) and loads the corresponding user from MongoDB.
92
+ *
93
+ * This method tries multiple verification strategies:
94
+ * 1. BetterAuth JWT verification (if JWT plugin is enabled)
95
+ * 2. BetterAuth session token lookup (database lookup)
96
+ *
97
+ * @param token - The token to verify
98
+ * @returns User object with hasRole method, or null if verification fails
99
+ */
100
+ async verifyAndLoadUser(token: string): Promise<BetterAuthenticatedUser | null> {
101
+ if (!this.betterAuthService || !this.connection) {
102
+ return null;
103
+ }
104
+
105
+ // Strategy 1: Try JWT verification (if JWT plugin is enabled)
106
+ if (this.betterAuthService.isJwtEnabled()) {
107
+ try {
108
+ const payload = await this.betterAuthService.verifyJwtToken(token);
109
+ if (payload?.sub) {
110
+ const user = await this.loadUserFromPayload(payload);
111
+ if (user) {
112
+ return user;
113
+ }
114
+ }
115
+ } catch (error) {
116
+ // Check for token expiration
117
+ if (error instanceof Error && error.message.includes('expired')) {
118
+ this.logger.debug('JWT token expired');
119
+ throw error; // Re-throw for proper handling by guards
120
+ }
121
+ // Other JWT verification failures - try session token next
122
+ this.logger.debug(
123
+ `JWT verification failed, trying session: ${error instanceof Error ? error.message : 'Unknown error'}`,
124
+ );
125
+ }
126
+ }
127
+
128
+ // Strategy 2: Try session token lookup (database lookup)
129
+ try {
130
+ const sessionResult = await this.betterAuthService.getSessionByToken(token);
131
+ if (sessionResult?.user) {
132
+ return this.loadUserFromSessionResult(sessionResult.user);
133
+ }
134
+ } catch (error) {
135
+ this.logger.debug(`Session lookup failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Creates a user object with hasRole method from a MongoDB document.
143
+ *
144
+ * @param user - Raw MongoDB user document
145
+ * @returns User object with hasRole method
146
+ */
147
+ createUserWithHasRole(user: Record<string, unknown>): BetterAuthenticatedUser {
148
+ return {
149
+ ...user,
150
+ _authenticatedViaBetterAuth: true,
151
+ hasRole: (roles: string[]): boolean => {
152
+ const userRoles = user.roles;
153
+ if (!userRoles || !Array.isArray(userRoles)) {
154
+ return false;
155
+ }
156
+ return roles.some((role) => userRoles.includes(role));
157
+ },
158
+ id: (user._id as Types.ObjectId)?.toString() || (user.id as string),
159
+ } as BetterAuthenticatedUser;
160
+ }
161
+
162
+ /**
163
+ * Loads a user from JWT payload using direct MongoDB query.
164
+ *
165
+ * @param payload - JWT payload with sub (user ID or iamId)
166
+ * @returns User object with hasRole method, or null if not found
167
+ */
168
+ private async loadUserFromPayload(payload: { [key: string]: unknown; sub: string }): Promise<BetterAuthenticatedUser | null> {
169
+ if (!this.connection) {
170
+ return null;
171
+ }
172
+
173
+ try {
174
+ const usersCollection = this.connection.collection('users');
175
+ let user: null | Record<string, unknown> = null;
176
+
177
+ // Try to find by MongoDB _id first
178
+ if (Types.ObjectId.isValid(payload.sub)) {
179
+ user = await usersCollection.findOne({ _id: new Types.ObjectId(payload.sub) });
180
+ }
181
+
182
+ // If not found, try by iamId
183
+ if (!user) {
184
+ user = await usersCollection.findOne({ iamId: payload.sub });
185
+ }
186
+
187
+ if (!user) {
188
+ return null;
189
+ }
190
+
191
+ return this.createUserWithHasRole(user);
192
+ } catch (error) {
193
+ this.logger.debug(`Failed to load user from payload: ${error instanceof Error ? error.message : 'Unknown error'}`);
194
+ return null;
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Loads a user from session result (from getSessionByToken).
200
+ *
201
+ * @param sessionUser - User object from session lookup
202
+ * @returns User object with hasRole method, or null if not found
203
+ */
204
+ private async loadUserFromSessionResult(sessionUser: {
205
+ email?: string;
206
+ id?: string;
207
+ }): Promise<BetterAuthenticatedUser | null> {
208
+ if (!this.connection || !sessionUser) {
209
+ return null;
210
+ }
211
+
212
+ try {
213
+ const usersCollection = this.connection.collection('users');
214
+ let user: null | Record<string, unknown> = null;
215
+
216
+ // Try to find by email (most reliable)
217
+ if (sessionUser.email) {
218
+ user = await usersCollection.findOne({ email: sessionUser.email });
219
+ }
220
+
221
+ // If not found by email, try by iamId
222
+ if (!user && sessionUser.id) {
223
+ user = await usersCollection.findOne({ iamId: sessionUser.id });
224
+ }
225
+
226
+ // If still not found, try by _id (if the ID looks like a MongoDB ObjectId)
227
+ if (!user && sessionUser.id && Types.ObjectId.isValid(sessionUser.id)) {
228
+ user = await usersCollection.findOne({ _id: new Types.ObjectId(sessionUser.id) });
229
+ }
230
+
231
+ if (!user) {
232
+ return null;
233
+ }
234
+
235
+ return this.createUserWithHasRole(user);
236
+ } catch (error) {
237
+ this.logger.debug(`Failed to load user from session: ${error instanceof Error ? error.message : 'Unknown error'}`);
238
+ return null;
239
+ }
240
+ }
241
+ }
@@ -5,6 +5,8 @@
5
5
  * and reduce the need for `as any` casts throughout the codebase.
6
6
  */
7
7
 
8
+ import { Types } from 'mongoose';
9
+
8
10
  import { BetterAuthSessionUser } from './core-better-auth-user.mapper';
9
11
 
10
12
  /**
@@ -23,6 +25,41 @@ export interface BetterAuth2FAResponse {
23
25
  user?: BetterAuthSessionUser;
24
26
  }
25
27
 
28
+ /**
29
+ * Authenticated user object created from BetterAuth token verification.
30
+ *
31
+ * This interface represents a user that has been authenticated via BetterAuth
32
+ * (either JWT or session token). It includes the `hasRole` method for
33
+ * role-based access control and the `_authenticatedViaBetterAuth` flag
34
+ * to identify BetterAuth-authenticated users.
35
+ */
36
+ export interface BetterAuthenticatedUser {
37
+ /** Allow additional properties from MongoDB document */
38
+ [key: string]: unknown;
39
+ /** Flag indicating this user was authenticated via BetterAuth */
40
+ _authenticatedViaBetterAuth: true;
41
+ /** MongoDB _id field */
42
+ _id?: Types.ObjectId;
43
+ /** User's email address */
44
+ email: string;
45
+ /** Whether user's email is verified */
46
+ emailVerified?: boolean;
47
+ /**
48
+ * Check if user has any of the specified roles
49
+ * @param roles - Array of role names to check
50
+ * @returns true if user has at least one of the roles
51
+ */
52
+ hasRole: (roles: string[]) => boolean;
53
+ /** User ID as string */
54
+ id: string;
55
+ /** User's assigned roles */
56
+ roles?: string[];
57
+ /** Whether the user is verified */
58
+ verified?: boolean;
59
+ /** Timestamp when user was verified */
60
+ verifiedAt?: Date;
61
+ }
62
+
26
63
  /**
27
64
  * Better-Auth session response from getSession API
28
65
  */
@@ -614,7 +614,7 @@ export class CoreBetterAuthController {
614
614
  * @param sessionUser - The user from Better-Auth session
615
615
  * @param _mappedUser - The synced user from legacy system (available for override customization)
616
616
  */
617
- // eslint-disable-next-line unused-imports/no-unused-vars
617
+
618
618
  protected mapUser(sessionUser: BetterAuthSessionUser, _mappedUser: any): CoreBetterAuthUserResponse {
619
619
  return {
620
620
  email: sessionUser.email,
@@ -9,11 +9,14 @@ import {
9
9
  Optional,
10
10
  Type,
11
11
  } from '@nestjs/common';
12
+ import { APP_GUARD } from '@nestjs/core';
12
13
  import { getConnectionToken } from '@nestjs/mongoose';
13
14
  import mongoose, { Connection } from 'mongoose';
14
15
 
15
16
  import { IBetterAuth } from '../../common/interfaces/server-options.interface';
16
17
  import { ConfigService } from '../../common/services/config.service';
18
+ import { RolesGuard } from '../auth/guards/roles.guard';
19
+ import { BetterAuthTokenService } from './better-auth-token.service';
17
20
  import { BetterAuthInstance, createBetterAuthInstance } from './better-auth.config';
18
21
  import { DefaultBetterAuthResolver } from './better-auth.resolver';
19
22
  import { CoreBetterAuthApiMiddleware } from './core-better-auth-api.middleware';
@@ -79,6 +82,19 @@ export interface CoreBetterAuthModuleOptions {
79
82
  */
80
83
  fallbackSecrets?: (string | undefined)[];
81
84
 
85
+ /**
86
+ * Register RolesGuard as a global guard.
87
+ *
88
+ * This should be set to `true` for IAM-only setups (CoreModule.forRoot with 1 parameter)
89
+ * where CoreAuthModule is not imported (which normally registers RolesGuard globally).
90
+ *
91
+ * When `true`, all `@Roles()` decorators will be enforced automatically without
92
+ * needing explicit `@UseGuards(RolesGuard)` on each endpoint.
93
+ *
94
+ * @default false
95
+ */
96
+ registerRolesGuardGlobally?: boolean;
97
+
82
98
  /**
83
99
  * Custom resolver class to use instead of the default DefaultBetterAuthResolver.
84
100
  * The class must extend CoreBetterAuthResolver.
@@ -152,6 +168,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
152
168
  private static currentConfig: IBetterAuth | null = null;
153
169
  private static customController: null | Type<CoreBetterAuthController> = null;
154
170
  private static customResolver: null | Type<CoreBetterAuthResolver> = null;
171
+ private static shouldRegisterRolesGuardGlobally = false;
155
172
 
156
173
  /**
157
174
  * Gets the controller class to use (custom or default)
@@ -251,7 +268,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
251
268
  * @returns Dynamic module configuration
252
269
  */
253
270
  static forRoot(options: CoreBetterAuthModuleOptions): DynamicModule {
254
- const { config: rawConfig, controller, fallbackSecrets, resolver } = options;
271
+ const { config: rawConfig, controller, fallbackSecrets, registerRolesGuardGlobally, resolver } = options;
255
272
 
256
273
  // Normalize config: true → {}, false/undefined → null
257
274
  const config = normalizeBetterAuthConfig(rawConfig);
@@ -262,6 +279,8 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
262
279
  this.customController = controller || null;
263
280
  // Store custom resolver if provided
264
281
  this.customResolver = resolver || null;
282
+ // Store whether to register RolesGuard globally (for IAM-only setups)
283
+ this.shouldRegisterRolesGuardGlobally = registerRolesGuardGlobally ?? false;
265
284
 
266
285
  // If better-auth is disabled (config is null or enabled: false), return minimal module
267
286
  // Note: We don't provide middleware classes when disabled because they depend on CoreBetterAuthService
@@ -270,7 +289,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
270
289
  this.logger.debug('BetterAuth is disabled - skipping initialization');
271
290
  this.betterAuthEnabled = false;
272
291
  return {
273
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter],
292
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
274
293
  module: CoreBetterAuthModule,
275
294
  providers: [
276
295
  {
@@ -282,6 +301,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
282
301
  CoreBetterAuthService,
283
302
  CoreBetterAuthUserMapper,
284
303
  CoreBetterAuthRateLimiter,
304
+ BetterAuthTokenService,
285
305
  ],
286
306
  };
287
307
  }
@@ -306,7 +326,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
306
326
  static forRootAsync(): DynamicModule {
307
327
  return {
308
328
  controllers: [this.getControllerClass()],
309
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter],
329
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
310
330
  imports: [],
311
331
  module: CoreBetterAuthModule,
312
332
  providers: [
@@ -380,6 +400,14 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
380
400
  CoreBetterAuthApiMiddleware,
381
401
  CoreBetterAuthRateLimiter,
382
402
  CoreBetterAuthRateLimitMiddleware,
403
+ // BetterAuthTokenService needs explicit factory to ensure proper dependency injection
404
+ {
405
+ inject: [CoreBetterAuthService, getConnectionToken()],
406
+ provide: BetterAuthTokenService,
407
+ useFactory: (betterAuthService: CoreBetterAuthService, connection: Connection) => {
408
+ return new BetterAuthTokenService(betterAuthService, connection);
409
+ },
410
+ },
383
411
  this.getResolverClass(),
384
412
  ],
385
413
  };
@@ -412,6 +440,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
412
440
  this.currentConfig = null;
413
441
  this.customController = null;
414
442
  this.customResolver = null;
443
+ this.shouldRegisterRolesGuardGlobally = false;
415
444
  }
416
445
 
417
446
  /**
@@ -421,7 +450,7 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
421
450
  private static createDeferredModule(config: IBetterAuth, fallbackSecrets?: (string | undefined)[]): DynamicModule {
422
451
  return {
423
452
  controllers: [this.getControllerClass()],
424
- exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter],
453
+ exports: [BETTER_AUTH_INSTANCE, CoreBetterAuthService, CoreBetterAuthUserMapper, CoreBetterAuthRateLimiter, BetterAuthTokenService],
425
454
  module: CoreBetterAuthModule,
426
455
  providers: [
427
456
  {
@@ -479,7 +508,25 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
479
508
  CoreBetterAuthApiMiddleware,
480
509
  CoreBetterAuthRateLimiter,
481
510
  CoreBetterAuthRateLimitMiddleware,
511
+ // BetterAuthTokenService needs explicit factory to ensure proper dependency injection
512
+ {
513
+ inject: [CoreBetterAuthService, getConnectionToken()],
514
+ provide: BetterAuthTokenService,
515
+ useFactory: (betterAuthService: CoreBetterAuthService, connection: Connection) => {
516
+ return new BetterAuthTokenService(betterAuthService, connection);
517
+ },
518
+ },
482
519
  this.getResolverClass(),
520
+ // Conditionally register RolesGuard globally for IAM-only setups
521
+ // In Legacy mode, RolesGuard is already registered globally via CoreAuthModule
522
+ ...(this.shouldRegisterRolesGuardGlobally
523
+ ? [
524
+ {
525
+ provide: APP_GUARD,
526
+ useClass: RolesGuard,
527
+ },
528
+ ]
529
+ : []),
483
530
  ],
484
531
  };
485
532
  }
@@ -195,7 +195,7 @@ export class CoreBetterAuthResolver {
195
195
  async betterAuthSignIn(
196
196
  @Args('email') email: string,
197
197
  @Args('password') password: string,
198
- // eslint-disable-next-line unused-imports/no-unused-vars -- Reserved for future cookie/session handling
198
+
199
199
  @Context() _ctx: { req: Request; res: Response },
200
200
  ): Promise<CoreBetterAuthAuthModel> {
201
201
  this.ensureEnabled();
@@ -201,6 +201,19 @@ export class CoreBetterAuthService {
201
201
  return this.config.baseUrl || 'http://localhost:3000';
202
202
  }
203
203
 
204
+ /**
205
+ * Gets the session cookie name based on the configured base path.
206
+ *
207
+ * The cookie name follows the pattern: `{basePath}.session_token`
208
+ * For example, with basePath '/iam', the cookie name is 'iam.session_token'
209
+ *
210
+ * @returns The session cookie name
211
+ */
212
+ getSessionCookieName(): string {
213
+ const basePath = this.getBasePath()?.replace(/^\//, '').replace(/\//g, '.') || 'iam';
214
+ return `${basePath}.session_token`;
215
+ }
216
+
204
217
  // ===================================================================================================================
205
218
  // JWT Token Methods
206
219
  // ===================================================================================================================
@@ -22,6 +22,7 @@
22
22
  * - DefaultBetterAuthResolver: Default resolver implementation (use as fallback)
23
23
  */
24
24
 
25
+ export * from './better-auth-token.service';
25
26
  export * from './better-auth.config';
26
27
  export * from './better-auth.resolver';
27
28
  export * from './better-auth.types';
@@ -262,6 +262,9 @@ export class CoreModule implements NestModule {
262
262
  config: betterAuthConfig === true ? {} : betterAuthConfig || {},
263
263
  // Pass JWT secrets for backwards compatibility fallback
264
264
  fallbackSecrets: [config.jwt?.secret, config.jwt?.refresh?.secret],
265
+ // In IAM-only mode, register RolesGuard globally to enforce @Roles() decorators
266
+ // In Legacy mode (autoRegister), RolesGuard is already registered via CoreAuthModule
267
+ registerRolesGuardGlobally: isIamOnlyMode,
265
268
  }),
266
269
  );
267
270
  }