@lenne.tech/nest-server 11.20.1 → 11.21.1

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 (84) hide show
  1. package/README.md +444 -100
  2. package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
  3. package/dist/core/common/decorators/restricted.decorator.js +4 -1
  4. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  5. package/dist/core/common/helpers/input.helper.js +11 -8
  6. package/dist/core/common/helpers/input.helper.js.map +1 -1
  7. package/dist/core/common/interceptors/check-security.interceptor.js +10 -8
  8. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  9. package/dist/core/common/interfaces/server-options.interface.d.ts +5 -1
  10. package/dist/core/common/middleware/request-context.middleware.js +10 -6
  11. package/dist/core/common/middleware/request-context.middleware.js.map +1 -1
  12. package/dist/core/common/plugins/mongoose-tenant.plugin.js +40 -24
  13. package/dist/core/common/plugins/mongoose-tenant.plugin.js.map +1 -1
  14. package/dist/core/common/services/email.service.d.ts +5 -1
  15. package/dist/core/common/services/email.service.js +16 -2
  16. package/dist/core/common/services/email.service.js.map +1 -1
  17. package/dist/core/common/services/request-context.service.d.ts +3 -0
  18. package/dist/core/common/services/request-context.service.js +6 -0
  19. package/dist/core/common/services/request-context.service.js.map +1 -1
  20. package/dist/core/modules/auth/guards/roles.guard.js +6 -10
  21. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  22. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  23. package/dist/core/modules/better-auth/better-auth-roles.guard.js +5 -6
  24. package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
  25. package/dist/core/modules/better-auth/core-better-auth-user.mapper.d.ts +6 -0
  26. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +52 -17
  27. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  28. package/dist/core/modules/better-auth/core-better-auth.service.d.ts +3 -1
  29. package/dist/core/modules/better-auth/core-better-auth.service.js +14 -0
  30. package/dist/core/modules/better-auth/core-better-auth.service.js.map +1 -1
  31. package/dist/core/modules/tenant/core-tenant-member.model.d.ts +11 -0
  32. package/dist/core/modules/tenant/core-tenant-member.model.js +106 -0
  33. package/dist/core/modules/tenant/core-tenant-member.model.js.map +1 -0
  34. package/dist/core/modules/tenant/core-tenant.decorators.d.ts +3 -0
  35. package/dist/core/modules/tenant/core-tenant.decorators.js +12 -0
  36. package/dist/core/modules/tenant/core-tenant.decorators.js.map +1 -0
  37. package/dist/core/modules/tenant/core-tenant.enums.d.ts +13 -0
  38. package/dist/core/modules/tenant/core-tenant.enums.js +25 -0
  39. package/dist/core/modules/tenant/core-tenant.enums.js.map +1 -0
  40. package/dist/core/modules/tenant/core-tenant.guard.d.ts +25 -0
  41. package/dist/core/modules/tenant/core-tenant.guard.js +271 -0
  42. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -0
  43. package/dist/core/modules/tenant/core-tenant.helpers.d.ts +7 -0
  44. package/dist/core/modules/tenant/core-tenant.helpers.js +60 -0
  45. package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -0
  46. package/dist/core/modules/tenant/core-tenant.module.d.ts +12 -0
  47. package/dist/core/modules/tenant/core-tenant.module.js +58 -0
  48. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -0
  49. package/dist/core/modules/tenant/core-tenant.service.d.ts +19 -0
  50. package/dist/core/modules/tenant/core-tenant.service.js +170 -0
  51. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -0
  52. package/dist/core/modules/user/core-user.service.js +12 -1
  53. package/dist/core/modules/user/core-user.service.js.map +1 -1
  54. package/dist/core.module.js +11 -0
  55. package/dist/core.module.js.map +1 -1
  56. package/dist/index.d.ts +7 -0
  57. package/dist/index.js +7 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/tsconfig.build.tsbuildinfo +1 -1
  60. package/package.json +35 -24
  61. package/src/core/common/decorators/restricted.decorator.ts +12 -2
  62. package/src/core/common/helpers/input.helper.ts +24 -9
  63. package/src/core/common/interceptors/check-security.interceptor.ts +19 -13
  64. package/src/core/common/interfaces/server-options.interface.ts +80 -28
  65. package/src/core/common/middleware/request-context.middleware.ts +12 -5
  66. package/src/core/common/plugins/mongoose-tenant.plugin.ts +78 -45
  67. package/src/core/common/services/email.service.ts +26 -5
  68. package/src/core/common/services/request-context.service.ts +15 -1
  69. package/src/core/modules/auth/guards/roles.guard.ts +10 -10
  70. package/src/core/modules/better-auth/better-auth-roles.guard.ts +9 -6
  71. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +86 -21
  72. package/src/core/modules/better-auth/core-better-auth.service.ts +27 -2
  73. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +165 -0
  74. package/src/core/modules/tenant/README.md +268 -0
  75. package/src/core/modules/tenant/core-tenant-member.model.ts +121 -0
  76. package/src/core/modules/tenant/core-tenant.decorators.ts +46 -0
  77. package/src/core/modules/tenant/core-tenant.enums.ts +77 -0
  78. package/src/core/modules/tenant/core-tenant.guard.ts +441 -0
  79. package/src/core/modules/tenant/core-tenant.helpers.ts +103 -0
  80. package/src/core/modules/tenant/core-tenant.module.ts +102 -0
  81. package/src/core/modules/tenant/core-tenant.service.ts +244 -0
  82. package/src/core/modules/user/core-user.service.ts +17 -1
  83. package/src/core.module.ts +15 -0
  84. package/src/index.ts +12 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.20.1",
3
+ "version": "11.21.1",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -73,26 +73,26 @@
73
73
  "node": ">= 20"
74
74
  },
75
75
  "dependencies": {
76
- "@apollo/server": "5.4.0",
76
+ "@apollo/server": "5.5.0",
77
77
  "@as-integrations/express5": "1.1.2",
78
- "@better-auth/passkey": "1.5.4",
78
+ "@better-auth/passkey": "1.5.5",
79
79
  "@getbrevo/brevo": "3.0.1",
80
80
  "@nestjs/apollo": "13.2.4",
81
- "@nestjs/common": "11.1.16",
82
- "@nestjs/core": "11.1.16",
81
+ "@nestjs/common": "11.1.17",
82
+ "@nestjs/core": "11.1.17",
83
83
  "@nestjs/graphql": "13.2.4",
84
84
  "@nestjs/jwt": "11.0.2",
85
85
  "@nestjs/mongoose": "11.0.4",
86
86
  "@nestjs/passport": "11.0.5",
87
- "@nestjs/platform-express": "11.1.16",
87
+ "@nestjs/platform-express": "11.1.17",
88
88
  "@nestjs/schedule": "6.1.1",
89
89
  "@nestjs/swagger": "11.2.6",
90
90
  "@nestjs/terminus": "11.1.1",
91
- "@nestjs/websockets": "11.1.16",
91
+ "@nestjs/websockets": "11.1.17",
92
92
  "@tus/file-store": "2.0.0",
93
93
  "@tus/server": "2.3.0",
94
94
  "bcrypt": "6.0.0",
95
- "better-auth": "1.5.4",
95
+ "better-auth": "1.5.5",
96
96
  "class-transformer": "0.5.1",
97
97
  "class-validator": "0.15.1",
98
98
  "compression": "1.8.1",
@@ -100,18 +100,18 @@
100
100
  "dotenv": "17.3.1",
101
101
  "ejs": "5.0.1",
102
102
  "express": "5.2.1",
103
- "graphql": "16.13.1",
103
+ "graphql": "16.13.2",
104
104
  "graphql-query-complexity": "1.1.0",
105
105
  "graphql-subscriptions": "3.0.0",
106
106
  "graphql-upload": "15.0.2",
107
107
  "js-sha256": "0.11.1",
108
108
  "json-to-graphql-query": "2.3.0",
109
109
  "lodash": "4.17.23",
110
- "mongodb": "7.1.0",
111
- "mongoose": "9.3.0",
110
+ "mongodb": "7.1.1",
111
+ "mongoose": "9.3.3",
112
112
  "multer": "2.1.1",
113
113
  "node-mailjet": "6.0.11",
114
- "nodemailer": "8.0.2",
114
+ "nodemailer": "8.0.4",
115
115
  "passport": "0.7.0",
116
116
  "passport-jwt": "4.0.1",
117
117
  "reflect-metadata": "0.2.2",
@@ -122,31 +122,31 @@
122
122
  },
123
123
  "devDependencies": {
124
124
  "@compodoc/compodoc": "1.2.1",
125
- "@nestjs/cli": "11.0.16",
126
- "@nestjs/schematics": "11.0.9",
127
- "@nestjs/testing": "11.1.16",
125
+ "@nestjs/cli": "11.0.17",
126
+ "@nestjs/schematics": "11.0.10",
127
+ "@nestjs/testing": "11.1.17",
128
128
  "@swc/cli": "0.8.0",
129
- "@swc/core": "1.15.18",
129
+ "@swc/core": "1.15.21",
130
130
  "@types/compression": "1.8.1",
131
131
  "@types/cookie-parser": "1.4.10",
132
132
  "@types/ejs": "3.1.5",
133
133
  "@types/express": "5.0.6",
134
134
  "@types/lodash": "4.17.24",
135
135
  "@types/multer": "2.1.0",
136
- "@types/node": "25.4.0",
136
+ "@types/node": "25.5.0",
137
137
  "@types/nodemailer": "7.0.11",
138
138
  "@types/passport": "1.0.17",
139
139
  "@types/supertest": "7.2.0",
140
- "@vitest/coverage-v8": "4.0.18",
141
- "@vitest/ui": "4.0.18",
140
+ "@vitest/coverage-v8": "4.1.2",
141
+ "@vitest/ui": "4.1.2",
142
142
  "ansi-colors": "4.1.3",
143
143
  "find-file-up": "2.0.1",
144
144
  "husky": "9.1.7",
145
145
  "nodemon": "3.1.14",
146
146
  "npm-watch": "0.13.0",
147
147
  "otpauth": "9.5.0",
148
- "oxfmt": "0.38.0",
149
- "oxlint": "1.53.0",
148
+ "oxfmt": "0.43.0",
149
+ "oxlint": "1.58.0",
150
150
  "rimraf": "6.1.3",
151
151
  "supertest": "7.2.2",
152
152
  "ts-node": "10.9.2",
@@ -157,7 +157,7 @@
157
157
  "vite": "7.3.1",
158
158
  "vite-plugin-node": "7.0.0",
159
159
  "vite-tsconfig-paths": "6.1.1",
160
- "vitest": "4.0.18"
160
+ "vitest": "4.1.2"
161
161
  },
162
162
  "main": "dist/index.js",
163
163
  "types": "dist/index.d.ts",
@@ -179,10 +179,21 @@
179
179
  "minimatch@<3.1.4": "3.1.4",
180
180
  "minimatch@>=9.0.0 <9.0.7": "9.0.7",
181
181
  "minimatch@>=10.0.0 <10.2.3": "10.2.4",
182
- "rollup@>=4.0.0 <4.59.0": "4.59.0",
182
+ "rollup@>=4.0.0 <4.60.1": "4.60.1",
183
183
  "ajv@<6.14.0": "6.14.0",
184
184
  "ajv@>=7.0.0-alpha.0 <8.18.0": "8.18.0",
185
- "file-type@>=13.0.0 <21.3.1": "21.3.1"
185
+ "file-type@>=13.0.0 <21.3.2": "21.3.2",
186
+ "undici@>=7.0.0 <7.24.0": "7.24.3",
187
+ "yauzl@<3.2.1": "3.2.1",
188
+ "flatted@<=3.4.1": "3.4.2",
189
+ "srvx@<0.11.13": "0.11.13",
190
+ "handlebars@>=4.0.0 <4.7.9": "4.7.9",
191
+ "brace-expansion@<1.1.13": "1.1.13",
192
+ "brace-expansion@>=4.0.0 <5.0.5": "5.0.5",
193
+ "picomatch@<2.3.2": "2.3.2",
194
+ "picomatch@>=4.0.0 <4.0.4": "4.0.4",
195
+ "path-to-regexp@>=8.0.0 <8.4.0": "8.4.1",
196
+ "kysely@>=0.26.0 <0.28.15": "0.28.15"
186
197
  },
187
198
  "onlyBuiltDependencies": [
188
199
  "bcrypt",
@@ -5,7 +5,9 @@ import _ = require('lodash');
5
5
  import { ProcessType } from '../enums/process-type.enum';
6
6
  import { RoleEnum } from '../enums/role.enum';
7
7
  import { equalIds, getIncludedIds } from '../helpers/db.helper';
8
+ import { RequestContext } from '../services/request-context.service';
8
9
  import { RequireAtLeastOne } from '../types/required-at-least-one.type';
10
+ import { checkRoleAccess } from '../../modules/tenant/core-tenant.helpers';
9
11
 
10
12
  /**
11
13
  * Restricted meta key
@@ -61,7 +63,14 @@ export const getRestricted = (object: unknown, propertyKey?: string): Restricted
61
63
  */
62
64
  export const checkRestricted = (
63
65
  data: any,
64
- user: { emailVerified?: any; hasRole: (roles: string[]) => boolean; id: any; verified?: any; verifiedAt?: any },
66
+ user: {
67
+ emailVerified?: any;
68
+ hasRole: (roles: string[]) => boolean;
69
+ id: any;
70
+ roles?: string[];
71
+ verified?: any;
72
+ verifiedAt?: any;
73
+ },
65
74
  options: {
66
75
  allowCreatorOfParent?: boolean;
67
76
  checkObjectItself?: boolean;
@@ -163,7 +172,8 @@ export const checkRestricted = (
163
172
  (roles.includes(RoleEnum.S_CREATOR) &&
164
173
  (('createdBy' in data && equalIds(data.createdBy, user)) ||
165
174
  (config.allowCreatorOfParent && !('createdBy' in data) && config.isCreatorOfParent))) ||
166
- (roles.includes(RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt || user?.emailVerified))
175
+ (roles.includes(RoleEnum.S_VERIFIED) && (user?.verified || user?.verifiedAt || user?.emailVerified)) ||
176
+ (user?.id && checkRoleAccess(roles, user?.roles, RequestContext.get()?.tenantRole))
167
177
  ) {
168
178
  valid = true;
169
179
  }
@@ -783,15 +783,30 @@ export function processDeep(
783
783
  specialProperties?: string[];
784
784
  },
785
785
  ): any {
786
- // Set options
787
- const { processedObjects, specialClasses, specialFunctions, specialProperties } = {
788
- processedObjects: new WeakMap(),
789
- specialClasses: [],
790
- specialFunctions: [],
791
- specialProperties: [],
792
- ...options,
786
+ // Set options once and reuse for all recursive calls (avoids creating new objects per property)
787
+ const resolvedOptions = {
788
+ processedObjects: options?.processedObjects ?? new WeakMap(),
789
+ specialClasses: options?.specialClasses ?? [],
790
+ specialFunctions: options?.specialFunctions ?? [],
791
+ specialProperties: options?.specialProperties ?? [],
793
792
  };
794
793
 
794
+ return processDeepInternal(data, func, resolvedOptions);
795
+ }
796
+
797
+ /** Internal recursive implementation that reuses the resolved options object */
798
+ function processDeepInternal(
799
+ data: any,
800
+ func: (data: any) => any,
801
+ options: {
802
+ processedObjects: WeakMap<any, boolean>;
803
+ specialClasses: ((new (args: any[]) => any) | string)[];
804
+ specialFunctions: string[];
805
+ specialProperties: string[];
806
+ },
807
+ ): any {
808
+ const { processedObjects, specialClasses, specialFunctions, specialProperties } = options;
809
+
795
810
  // Check for falsifiable values
796
811
  if (!data) {
797
812
  return func(data);
@@ -806,7 +821,7 @@ export function processDeep(
806
821
 
807
822
  // Process array
808
823
  if (Array.isArray(data)) {
809
- return func(data.map((item) => processDeep(item, func, { processedObjects, specialClasses })));
824
+ return func(data.map((item) => processDeepInternal(item, func, options)));
810
825
  }
811
826
 
812
827
  // Process object
@@ -826,7 +841,7 @@ export function processDeep(
826
841
  }
827
842
  }
828
843
  for (const [key, value] of Object.entries(data)) {
829
- data[key] = processDeep(value, func, { processedObjects, specialClasses });
844
+ data[key] = processDeepInternal(value, func, options);
830
845
  }
831
846
  return func(data);
832
847
  }
@@ -62,18 +62,17 @@ export class CheckSecurityInterceptor implements NestInterceptor {
62
62
 
63
63
  // Check data
64
64
  if (data && typeof data === 'object' && typeof data.securityCheck === 'function') {
65
- const dataJson = JSON.stringify(data);
65
+ // Only capture pre-check state when debug is active (JSON.stringify is expensive)
66
+ const dataJson = this.config.debug ? JSON.stringify(data) : undefined;
66
67
  const response = data.securityCheck(user, force);
67
- new Promise(() => {
68
- if (this.config.debug && dataJson !== JSON.stringify(response)) {
69
- const id = getStringIds(data);
70
- console.debug(
71
- 'CheckSecurityInterceptor: securityCheck changed data of type',
72
- data.constructor.name,
73
- id && !Array.isArray(id) ? `with ID: ${id}` : '',
74
- );
75
- }
76
- });
68
+ if (this.config.debug && dataJson !== JSON.stringify(response)) {
69
+ const id = getStringIds(data);
70
+ console.debug(
71
+ 'CheckSecurityInterceptor: securityCheck changed data of type',
72
+ data.constructor.name,
73
+ id && !Array.isArray(id) ? `with ID: ${id}` : '',
74
+ );
75
+ }
77
76
  if (response && !data._doNotCheckSecurityDeep) {
78
77
  for (const key of Object.keys(response)) {
79
78
  response[key] = check(response[key]);
@@ -109,12 +108,19 @@ export class CheckSecurityInterceptor implements NestInterceptor {
109
108
  // Fallback: Remove known secret fields regardless of model type (recursive into plain objects)
110
109
  const isPlainLike = (val: any): boolean => {
111
110
  if (!val || typeof val !== 'object' || Array.isArray(val)) return false;
112
- // Skip Streams, Buffers, Dates, RegExps and other special objects
111
+ // Skip Streams, Buffers, Dates, RegExps, Maps, Sets
113
112
  if (typeof val.pipe === 'function') return false;
114
113
  if (Buffer.isBuffer(val)) return false;
115
114
  if (val instanceof Date || val instanceof RegExp) return false;
115
+ if (val instanceof Map || val instanceof Set) return false;
116
+ // Skip Mongoose documents and BSON types (they have circular internal references)
117
+ if (val.$__ !== undefined || val._bsontype !== undefined) return false;
116
118
  const proto = Object.getPrototypeOf(val);
117
- return proto === null || proto === Object.prototype || typeof val.constructor === 'function';
119
+ // Only recurse into actual plain objects (created via {} or Object.create(null)).
120
+ // Previously used `typeof val.constructor === 'function'` which was too broad and
121
+ // caused infinite recursion on Mongoose Schema.Types.Mixed fields whose internal
122
+ // objects (Schema, SchemaType) have circular references.
123
+ return proto === null || proto === Object.prototype;
118
124
  };
119
125
  const visited = new WeakSet();
120
126
  const removeSecrets = (data: any) => {
@@ -817,15 +817,12 @@ export interface IJwt {
817
817
  }
818
818
 
819
819
  /**
820
- * Multi-tenancy configuration
820
+ * Multi-tenancy configuration for automatic tenant-based data isolation.
821
821
  *
822
822
  * Follows the "presence implies enabled" pattern:
823
823
  * - `undefined`: Feature disabled (no overhead)
824
824
  * - `{}`: Feature enabled with defaults
825
825
  * - `{ enabled: false }`: Pre-configured but disabled
826
- */
827
- /**
828
- * Multi-tenancy configuration for automatic tenant-based data isolation.
829
826
  *
830
827
  * @since 11.20.0
831
828
  */
@@ -838,26 +835,85 @@ export interface IMultiTenancy {
838
835
  enabled?: boolean;
839
836
 
840
837
  /**
841
- * Field name on `req.user` that contains the tenant identifier.
838
+ * Model names (NOT collection names) to exclude from tenant filtering.
839
+ * These schemas will not have tenant isolation applied.
840
+ * The TenantMember model is always excluded automatically.
841
+ * @example ['User', 'Session']
842
+ */
843
+ excludeSchemas?: string[];
844
+
845
+ /**
846
+ * Header name for tenant selection.
847
+ * The header value contains the tenant ID for the current request.
848
+ * @default 'x-tenant-id'
849
+ * @since 11.21.0
850
+ */
851
+ headerName?: string;
852
+
853
+ /**
854
+ * Mongoose model name for the membership collection.
855
+ * Must be registered via MongooseModule.forFeature().
856
+ * @default 'TenantMember'
857
+ * @since 11.21.0
858
+ */
859
+ membershipModel?: string;
860
+
861
+ /**
862
+ * Whether system admins (RoleEnum.ADMIN) bypass the membership check.
863
+ * When true, admins can access any tenant without being a member.
864
+ * @default true
865
+ * @since 11.21.0
866
+ */
867
+ adminBypass?: boolean;
868
+
869
+ /**
870
+ * Custom role hierarchy for tenant membership roles.
871
+ * Keys = role names (stored in membership documents), values = numeric levels.
872
+ * Higher value = more privileges. Multiple roles can share the same level.
873
+ *
874
+ * Hierarchy roles use level comparison: a higher level includes all lower levels.
875
+ * Roles NOT in this config are treated as "normal roles" with exact match semantics.
876
+ *
877
+ * Use `createHierarchyRoles(hierarchy)` to generate type-safe constants for decorators.
842
878
  *
843
- * **Important:** This only controls which property on `req.user` is read.
844
- * The document schema field name is always `tenantId` and is not configurable.
845
- * Example: `userField: 'organizationId'` reads `req.user.organizationId` but
846
- * filters/sets the `tenantId` field on documents.
879
+ * @default { member: 1, manager: 2, owner: 3 }
880
+ * @since 11.21.0
881
+ *
882
+ * @example
883
+ * ```typescript
884
+ * // config.env.ts
885
+ * multiTenancy: {
886
+ * roleHierarchy: { viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 }
887
+ * }
847
888
  *
848
- * **Falsy values:** If the resolved value is falsy (undefined, null, or empty string ''),
849
- * the user is treated as having no tenant — they will only see documents with `tenantId: null`.
889
+ * // roles.ts
890
+ * import { createHierarchyRoles } from '@lenne.tech/nest-server';
891
+ * export const HR = createHierarchyRoles({ viewer: 1, editor: 2, manager: 2, admin: 3, owner: 4 });
850
892
  *
851
- * @default 'tenantId'
893
+ * // resolver.ts
894
+ * @Roles(HR.EDITOR) // requires level >= 2 (editor, manager, admin, owner)
895
+ * ```
852
896
  */
853
- userField?: string;
897
+ roleHierarchy?: Record<string, number>;
854
898
 
855
899
  /**
856
- * Model names (NOT collection names) to exclude from tenant filtering.
857
- * These schemas will not have tenant isolation applied.
858
- * @example ['User', 'Session']
900
+ * TTL in milliseconds for the tenant guard's in-memory membership cache.
901
+ * The cache avoids repeated DB lookups when the same user accesses the same tenant.
902
+ * Set to 0 to disable caching (useful for testing or security-critical deployments).
903
+ *
904
+ * **Important:** This cache is process-local. In horizontally scaled deployments
905
+ * (multiple server instances), membership changes on one instance are not reflected
906
+ * on other instances until the TTL expires. For security-sensitive deployments,
907
+ * reduce the TTL or set to 0 to disable.
908
+ *
909
+ * Note: `CoreBetterAuthUserMapper` has an independent 15-second user cache for
910
+ * roles and verified status. Both caches affect revocation latency. To control both,
911
+ * set this to 0 and override `USER_CACHE_TTL_MS` in a custom mapper.
912
+ *
913
+ * @default 30000 (30 seconds)
914
+ * @since 11.21.1
859
915
  */
860
- excludeSchemas?: string[];
916
+ cacheTtlMs?: number;
861
917
  }
862
918
 
863
919
  /**
@@ -1365,19 +1421,19 @@ export interface IServerOptions {
1365
1421
  /**
1366
1422
  * Multi-tenancy configuration for tenant-based data isolation.
1367
1423
  *
1368
- * When enabled, a global Mongoose plugin automatically filters all queries
1369
- * by `tenantId`, ensuring tenant isolation in a shared database.
1424
+ * When enabled, provides header-based tenant selection with membership validation.
1425
+ * The active tenant is determined by the X-Tenant-Id header on each request.
1426
+ * A global Mongoose plugin automatically filters all queries by `tenantId`.
1370
1427
  *
1371
1428
  * Follows the "presence implies enabled" pattern:
1372
1429
  * - `undefined`: Disabled (no overhead, backward compatible)
1373
- * - `{}`: Enabled with defaults
1374
- * - `{ userField: 'organizationId' }`: Enabled with custom user field
1430
+ * - `{}`: Enabled with defaults (X-Tenant-Id header, admin bypass)
1375
1431
  * - `{ enabled: false }`: Pre-configured but disabled
1376
1432
  *
1377
1433
  * The plugin activates automatically on any schema that has a `tenantId` field.
1378
1434
  * Schemas without `tenantId` are not affected.
1379
1435
  *
1380
- * @since 11.20.0
1436
+ * @since 11.21.0
1381
1437
  * @default undefined (disabled)
1382
1438
  *
1383
1439
  * @example
@@ -1385,14 +1441,10 @@ export interface IServerOptions {
1385
1441
  * // Enable with defaults
1386
1442
  * multiTenancy: {},
1387
1443
  *
1388
- * // Enable with excluded schemas
1444
+ * // Enable with excluded schemas and custom header
1389
1445
  * multiTenancy: {
1390
1446
  * excludeSchemas: ['User', 'Session'],
1391
- * },
1392
- *
1393
- * // Custom user field
1394
- * multiTenancy: {
1395
- * userField: 'organizationId',
1447
+ * headerName: 'x-tenant-id',
1396
1448
  * },
1397
1449
  * ```
1398
1450
  */
@@ -1,7 +1,6 @@
1
1
  import { Injectable, NestMiddleware } from '@nestjs/common';
2
2
  import { NextFunction, Request, Response } from 'express';
3
3
 
4
- import { ConfigService } from '../services/config.service';
5
4
  import { IRequestContext, RequestContext } from '../services/request-context.service';
6
5
 
7
6
  /**
@@ -21,10 +20,18 @@ export class RequestContextMiddleware implements NestMiddleware {
21
20
  return req.headers?.['accept-language'] || undefined;
22
21
  },
23
22
  get tenantId() {
24
- const config = ConfigService.configFastButReadOnly?.multiTenancy;
25
- if (!config || config.enabled === false) return undefined;
26
- const field = config.userField ?? 'tenantId';
27
- return (req as any).user?.[field] ?? undefined;
23
+ // Only return tenant ID set by CoreTenantGuard (after membership validation).
24
+ // The raw header is NEVER used for plugin filtering.
25
+ return (req as any).tenantId ?? undefined;
26
+ },
27
+ get tenantIds() {
28
+ return (req as any).tenantIds ?? undefined;
29
+ },
30
+ get tenantRole() {
31
+ return (req as any).tenantRole ?? undefined;
32
+ },
33
+ get isAdminBypass() {
34
+ return (req as any).isAdminBypass ?? false;
28
35
  },
29
36
  };
30
37
  RequestContext.run(context, () => next());