@lenne.tech/nest-server 11.16.1 → 11.18.0

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 (113) hide show
  1. package/dist/config.env.js +8 -2
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/decorators/response-model.decorator.d.ts +3 -0
  4. package/dist/core/common/decorators/response-model.decorator.js +8 -0
  5. package/dist/core/common/decorators/response-model.decorator.js.map +1 -0
  6. package/dist/core/common/helpers/db.helper.js +2 -2
  7. package/dist/core/common/helpers/db.helper.js.map +1 -1
  8. package/dist/core/common/helpers/filter.helper.js +3 -3
  9. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  10. package/dist/core/common/helpers/input.helper.js +2 -2
  11. package/dist/core/common/helpers/input.helper.js.map +1 -1
  12. package/dist/core/common/helpers/interceptor.helper.d.ts +3 -0
  13. package/dist/core/common/helpers/interceptor.helper.js +84 -0
  14. package/dist/core/common/helpers/interceptor.helper.js.map +1 -0
  15. package/dist/core/common/helpers/service.helper.d.ts +1 -0
  16. package/dist/core/common/helpers/service.helper.js +1 -0
  17. package/dist/core/common/helpers/service.helper.js.map +1 -1
  18. package/dist/core/common/interceptors/check-security.interceptor.d.ts +2 -0
  19. package/dist/core/common/interceptors/check-security.interceptor.js +43 -1
  20. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  21. package/dist/core/common/interceptors/response-model.interceptor.d.ts +13 -0
  22. package/dist/core/common/interceptors/response-model.interceptor.js +107 -0
  23. package/dist/core/common/interceptors/response-model.interceptor.js.map +1 -0
  24. package/dist/core/common/interceptors/translate-response.interceptor.d.ts +8 -0
  25. package/dist/core/common/interceptors/translate-response.interceptor.js +85 -0
  26. package/dist/core/common/interceptors/translate-response.interceptor.js.map +1 -0
  27. package/dist/core/common/interfaces/server-options.interface.d.ts +16 -0
  28. package/dist/core/common/middleware/request-context.middleware.d.ts +5 -0
  29. package/dist/core/common/middleware/request-context.middleware.js +29 -0
  30. package/dist/core/common/middleware/request-context.middleware.js.map +1 -0
  31. package/dist/core/common/pipes/map-and-validate.pipe.js +2 -2
  32. package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
  33. package/dist/core/common/plugins/complexity.plugin.d.ts +2 -2
  34. package/dist/core/common/plugins/mongoose-audit-fields.plugin.d.ts +1 -0
  35. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js +51 -0
  36. package/dist/core/common/plugins/mongoose-audit-fields.plugin.js.map +1 -0
  37. package/dist/core/common/plugins/mongoose-password.plugin.d.ts +4 -0
  38. package/dist/core/common/plugins/mongoose-password.plugin.js +69 -0
  39. package/dist/core/common/plugins/mongoose-password.plugin.js.map +1 -0
  40. package/dist/core/common/plugins/mongoose-role-guard.plugin.d.ts +1 -0
  41. package/dist/core/common/plugins/mongoose-role-guard.plugin.js +80 -0
  42. package/dist/core/common/plugins/mongoose-role-guard.plugin.js.map +1 -0
  43. package/dist/core/common/services/config.service.js +2 -2
  44. package/dist/core/common/services/config.service.js.map +1 -1
  45. package/dist/core/common/services/model-registry.service.d.ts +8 -0
  46. package/dist/core/common/services/model-registry.service.js +20 -0
  47. package/dist/core/common/services/model-registry.service.js.map +1 -0
  48. package/dist/core/common/services/module.service.d.ts +2 -0
  49. package/dist/core/common/services/module.service.js +36 -1
  50. package/dist/core/common/services/module.service.js.map +1 -1
  51. package/dist/core/common/services/request-context.service.d.ts +18 -0
  52. package/dist/core/common/services/request-context.service.js +32 -0
  53. package/dist/core/common/services/request-context.service.js.map +1 -0
  54. package/dist/core/modules/auth/guards/auth.guard.js +2 -2
  55. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  56. package/dist/core/modules/better-auth/core-better-auth.resolver.js +2 -2
  57. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
  58. package/dist/core/modules/permissions/core-permissions.controller.d.ts +13 -0
  59. package/dist/core/modules/permissions/core-permissions.controller.js +71 -0
  60. package/dist/core/modules/permissions/core-permissions.controller.js.map +1 -0
  61. package/dist/core/modules/permissions/core-permissions.module.d.ts +5 -0
  62. package/dist/core/modules/permissions/core-permissions.module.js +36 -0
  63. package/dist/core/modules/permissions/core-permissions.module.js.map +1 -0
  64. package/dist/core/modules/permissions/core-permissions.service.d.ts +34 -0
  65. package/dist/core/modules/permissions/core-permissions.service.js +610 -0
  66. package/dist/core/modules/permissions/core-permissions.service.js.map +1 -0
  67. package/dist/core/modules/permissions/interfaces/permissions.interface.d.ts +93 -0
  68. package/dist/core/modules/permissions/interfaces/permissions.interface.js +3 -0
  69. package/dist/core/modules/permissions/interfaces/permissions.interface.js.map +1 -0
  70. package/dist/core/modules/permissions/permissions-scanner.d.ts +25 -0
  71. package/dist/core/modules/permissions/permissions-scanner.js +817 -0
  72. package/dist/core/modules/permissions/permissions-scanner.js.map +1 -0
  73. package/dist/core.module.js +41 -0
  74. package/dist/core.module.js.map +1 -1
  75. package/dist/index.d.ts +15 -0
  76. package/dist/index.js +15 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/server/modules/file/file-info.model.d.ts +12 -12
  79. package/dist/server/modules/user/user.model.d.ts +33 -33
  80. package/dist/tsconfig.build.tsbuildinfo +1 -1
  81. package/package.json +35 -30
  82. package/src/config.env.ts +8 -2
  83. package/src/core/common/decorators/response-model.decorator.ts +31 -0
  84. package/src/core/common/helpers/db.helper.ts +2 -2
  85. package/src/core/common/helpers/filter.helper.ts +3 -3
  86. package/src/core/common/helpers/input.helper.ts +2 -2
  87. package/src/core/common/helpers/interceptor.helper.ts +132 -0
  88. package/src/core/common/helpers/service.helper.ts +1 -1
  89. package/src/core/common/interceptors/check-security.interceptor.ts +44 -1
  90. package/src/core/common/interceptors/response-model.interceptor.ts +135 -0
  91. package/src/core/common/interceptors/translate-response.interceptor.ts +104 -0
  92. package/src/core/common/interfaces/server-options.interface.ts +186 -0
  93. package/src/core/common/middleware/request-context.middleware.ts +25 -0
  94. package/src/core/common/pipes/map-and-validate.pipe.ts +2 -2
  95. package/src/core/common/plugins/complexity.plugin.ts +2 -2
  96. package/src/core/common/plugins/mongoose-audit-fields.plugin.ts +74 -0
  97. package/src/core/common/plugins/mongoose-password.plugin.ts +100 -0
  98. package/src/core/common/plugins/mongoose-role-guard.plugin.ts +150 -0
  99. package/src/core/common/services/config.service.ts +2 -2
  100. package/src/core/common/services/model-registry.service.ts +25 -0
  101. package/src/core/common/services/module.service.ts +91 -1
  102. package/src/core/common/services/request-context.service.ts +69 -0
  103. package/src/core/modules/auth/guards/auth.guard.ts +2 -2
  104. package/src/core/modules/better-auth/core-better-auth.resolver.ts +2 -2
  105. package/src/core/modules/permissions/INTEGRATION-CHECKLIST.md +56 -0
  106. package/src/core/modules/permissions/README.md +102 -0
  107. package/src/core/modules/permissions/core-permissions.controller.ts +34 -0
  108. package/src/core/modules/permissions/core-permissions.module.ts +36 -0
  109. package/src/core/modules/permissions/core-permissions.service.ts +627 -0
  110. package/src/core/modules/permissions/interfaces/permissions.interface.ts +125 -0
  111. package/src/core/modules/permissions/permissions-scanner.ts +1011 -0
  112. package/src/core.module.ts +62 -4
  113. package/src/index.ts +20 -0
@@ -0,0 +1,1011 @@
1
+ /**
2
+ * Pure permissions scanning functions with no NestJS dependencies.
3
+ *
4
+ * This module is the single source of truth for AST-based permissions scanning.
5
+ * It is used by:
6
+ * - CorePermissionsService (nest-server runtime, adds caching/watcher/HTTP)
7
+ * - lt CLI `lt server permissions` command (dynamic import from node_modules)
8
+ *
9
+ * All functions are standalone and framework-agnostic.
10
+ */
11
+ import { existsSync, readdirSync, statSync } from 'fs';
12
+ import { join, relative } from 'path';
13
+ import { Project, SyntaxKind } from 'ts-morph';
14
+
15
+ import type {
16
+ EffectiveEndpoint,
17
+ EffectiveMatrixEntry,
18
+ EndpointPermissions,
19
+ FieldPermission,
20
+ FilePermissions,
21
+ MethodPermission,
22
+ ModulePermissions,
23
+ PermissionsReport,
24
+ ReportStats,
25
+ RoleEnumInfo,
26
+ SecurityCheckInfo,
27
+ SecurityWarning,
28
+ WarningsByType,
29
+ } from './interfaces/permissions.interface';
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Public API
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Calculate coverage and warning statistics from scan results.
37
+ */
38
+ export function calculateStats(
39
+ modules: ModulePermissions[],
40
+ objects: FilePermissions[],
41
+ warnings: SecurityWarning[],
42
+ ): ReportStats {
43
+ const warningsByType: WarningsByType = {
44
+ NO_RESTRICTION: 0,
45
+ NO_ROLES: 0,
46
+ NO_SECURITY_CHECK: 0,
47
+ UNRESTRICTED_FIELD: 0,
48
+ UNRESTRICTED_METHOD: 0,
49
+ };
50
+ for (const w of warnings) {
51
+ if (w.type in warningsByType) {
52
+ warningsByType[w.type as keyof WarningsByType]++;
53
+ }
54
+ }
55
+
56
+ const totalModels = modules.reduce((s, m) => s + m.models.length, 0);
57
+ let totalMethods = 0;
58
+ let methodsWithRoles = 0;
59
+ for (const mod of modules) {
60
+ for (const ctrl of mod.controllers) {
61
+ for (const m of ctrl.methods) {
62
+ totalMethods++;
63
+ if (m.roles.length > 0 || ctrl.classRoles.length > 0) methodsWithRoles++;
64
+ }
65
+ }
66
+ for (const res of mod.resolvers) {
67
+ for (const m of res.methods) {
68
+ totalMethods++;
69
+ if (m.roles.length > 0 || res.classRoles.length > 0) methodsWithRoles++;
70
+ }
71
+ }
72
+ }
73
+
74
+ let modelsWithBothChecks = 0;
75
+ for (const mod of modules) {
76
+ for (const model of mod.models) {
77
+ if (model.classRestriction.length > 0 && model.securityCheck) {
78
+ modelsWithBothChecks++;
79
+ }
80
+ }
81
+ }
82
+
83
+ const endpointCoverage = totalMethods > 0 ? Math.round((methodsWithRoles / totalMethods) * 100) : 100;
84
+ const securityCoverage = totalModels > 0 ? Math.round((modelsWithBothChecks / totalModels) * 100) : 100;
85
+
86
+ return {
87
+ endpointCoverage,
88
+ securityCoverage,
89
+ totalEndpoints: totalMethods,
90
+ totalModels,
91
+ totalModules: modules.length,
92
+ totalSubObjects: objects.length,
93
+ totalWarnings: warnings.length,
94
+ warningsByType,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Build an effective permissions matrix for a module, grouping endpoints by role.
100
+ */
101
+ export function buildEffectiveMatrix(mod: ModulePermissions): EffectiveMatrixEntry[] {
102
+ const allRoles = new Set<string>();
103
+
104
+ for (const ctrl of mod.controllers) {
105
+ for (const r of ctrl.classRoles) allRoles.add(r);
106
+ for (const m of ctrl.methods) {
107
+ for (const r of m.roles) allRoles.add(r);
108
+ }
109
+ }
110
+
111
+ for (const res of mod.resolvers) {
112
+ for (const r of res.classRoles) allRoles.add(r);
113
+ for (const m of res.methods) {
114
+ for (const r of m.roles) allRoles.add(r);
115
+ }
116
+ }
117
+
118
+ const result: EffectiveMatrixEntry[] = [];
119
+
120
+ for (const role of [...allRoles].sort()) {
121
+ const endpoints: EffectiveEndpoint[] = [];
122
+
123
+ for (const ctrl of mod.controllers) {
124
+ for (const m of ctrl.methods) {
125
+ const effective = m.roles.length > 0 ? m.roles : ctrl.classRoles;
126
+ if (effective.includes(role)) {
127
+ endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Controller' });
128
+ }
129
+ }
130
+ }
131
+
132
+ for (const res of mod.resolvers) {
133
+ for (const m of res.methods) {
134
+ const effective = m.roles.length > 0 ? m.roles : res.classRoles;
135
+ if (effective.includes(role)) {
136
+ endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Resolver' });
137
+ }
138
+ }
139
+ }
140
+
141
+ result.push({ endpoints, role });
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Collect all role enums from the project's common/enums and modules directories.
149
+ */
150
+ export function collectRoleEnums(project: Project, projectPath: string): RoleEnumInfo[] {
151
+ const enums: RoleEnumInfo[] = [];
152
+ const enumPatterns = [
153
+ join(projectPath, 'src', 'server', 'common', 'enums'),
154
+ join(projectPath, 'src', 'server', 'modules'),
155
+ ];
156
+
157
+ for (const dir of enumPatterns) {
158
+ try {
159
+ const enumFiles = project.addSourceFilesAtPaths(join(dir, '**', '*.enum.ts'));
160
+ for (const sf of enumFiles) {
161
+ for (const enumDecl of sf.getEnums()) {
162
+ const enumName = enumDecl.getName();
163
+ if (enumName.toLowerCase().includes('role')) {
164
+ const values = enumDecl.getMembers().map((m) => ({
165
+ key: m.getName(),
166
+ value: m.getValue()?.toString() || m.getName(),
167
+ }));
168
+ enums.push({
169
+ file: relative(projectPath, sf.getFilePath()),
170
+ name: enumName,
171
+ values,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ } catch {
177
+ // Directory may not exist
178
+ }
179
+ }
180
+
181
+ return enums;
182
+ }
183
+
184
+ /**
185
+ * Detect security gaps in modules and objects and return warnings.
186
+ */
187
+ export function detectSecurityGaps(modules: ModulePermissions[], objects: FilePermissions[]): SecurityWarning[] {
188
+ const warnings: SecurityWarning[] = [];
189
+
190
+ for (const mod of modules) {
191
+ for (const model of mod.models) {
192
+ if (model.classRestriction.length === 0) {
193
+ warnings.push({
194
+ details: `Model ${model.className} has no @Restricted class-level restriction`,
195
+ file: model.filePath,
196
+ module: mod.name,
197
+ type: 'NO_RESTRICTION',
198
+ });
199
+ }
200
+ if (!model.securityCheck) {
201
+ warnings.push({
202
+ details: `Model ${model.className} has no securityCheck override`,
203
+ file: model.filePath,
204
+ module: mod.name,
205
+ type: 'NO_SECURITY_CHECK',
206
+ });
207
+ }
208
+ for (const field of model.fields) {
209
+ if (field.roles === '*(none)*') {
210
+ warnings.push({
211
+ details: `Field '${field.name}' has no role restriction`,
212
+ file: model.filePath,
213
+ module: mod.name,
214
+ type: 'UNRESTRICTED_FIELD',
215
+ });
216
+ }
217
+ }
218
+ }
219
+ for (const input of mod.inputs) {
220
+ for (const field of input.fields) {
221
+ if (field.roles === '*(none)*') {
222
+ warnings.push({
223
+ details: `Field '${field.name}' has no role restriction`,
224
+ file: input.filePath,
225
+ module: mod.name,
226
+ type: 'UNRESTRICTED_FIELD',
227
+ });
228
+ }
229
+ }
230
+ }
231
+ for (const ctrl of mod.controllers) {
232
+ if (ctrl.classRoles.length === 0) {
233
+ warnings.push({
234
+ details: `Controller ${ctrl.className} has no @Roles class-level restriction`,
235
+ file: ctrl.filePath,
236
+ module: mod.name,
237
+ type: 'NO_ROLES',
238
+ });
239
+ }
240
+ for (const method of ctrl.methods) {
241
+ if (method.roles.length === 0 && ctrl.classRoles.length === 0) {
242
+ warnings.push({
243
+ details: `Method '${method.name}' has no @Roles and class has no @Roles`,
244
+ file: ctrl.filePath,
245
+ module: mod.name,
246
+ type: 'UNRESTRICTED_METHOD',
247
+ });
248
+ }
249
+ }
250
+ }
251
+ for (const res of mod.resolvers) {
252
+ if (res.classRoles.length === 0) {
253
+ warnings.push({
254
+ details: `Resolver ${res.className} has no @Roles class-level restriction`,
255
+ file: res.filePath,
256
+ module: mod.name,
257
+ type: 'NO_ROLES',
258
+ });
259
+ }
260
+ for (const method of res.methods) {
261
+ if (method.roles.length === 0 && res.classRoles.length === 0) {
262
+ warnings.push({
263
+ details: `Method '${method.name}' has no @Roles and class has no @Roles`,
264
+ file: res.filePath,
265
+ module: mod.name,
266
+ type: 'UNRESTRICTED_METHOD',
267
+ });
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ for (const obj of objects) {
274
+ for (const field of obj.fields) {
275
+ if (field.roles === '*(none)*') {
276
+ warnings.push({
277
+ details: `Field '${field.name}' has no role restriction`,
278
+ file: obj.filePath,
279
+ module: 'objects',
280
+ type: 'UNRESTRICTED_FIELD',
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ return warnings;
287
+ }
288
+
289
+ /**
290
+ * Discover module directories under the given modules directory.
291
+ */
292
+ export function discoverModules(modulesDir: string): string[] {
293
+ if (!existsSync(modulesDir)) return [];
294
+ return readdirSync(modulesDir)
295
+ .filter((item) => statSync(join(modulesDir, item)).isDirectory())
296
+ .sort();
297
+ }
298
+
299
+ /**
300
+ * Find the project root containing src/server/modules/.
301
+ * Walks up from startPath (default: process.cwd()) up to 10 levels.
302
+ */
303
+ export function findProjectRoot(startPath?: string): string | null {
304
+ let current = startPath || process.cwd();
305
+ for (let i = 0; i < 10; i++) {
306
+ if (existsSync(join(current, 'src', 'server', 'modules'))) {
307
+ return current;
308
+ }
309
+ const parent = join(current, '..');
310
+ if (parent === current) break;
311
+ current = parent;
312
+ }
313
+ return null;
314
+ }
315
+
316
+ /**
317
+ * Scan a NestJS project and return a full PermissionsReport.
318
+ *
319
+ * This is the main entry point for external consumers (e.g. lt CLI).
320
+ *
321
+ * @param projectPath - Absolute path to the project root (must contain src/server/modules/)
322
+ * @param logger - Optional logging callbacks for progress reporting
323
+ */
324
+ export function scanPermissions(
325
+ projectPath: string,
326
+ logger?: { log?: (msg: string) => void; warn?: (msg: string) => void },
327
+ ): PermissionsReport {
328
+ const log = logger?.log || (() => {});
329
+ const warn = logger?.warn || (() => {});
330
+
331
+ log('Scanning permissions...');
332
+
333
+ const project = new Project({ compilerOptions: { allowJs: true }, skipAddingFilesFromTsConfig: true });
334
+ const modulesDir = join(projectPath, 'src', 'server', 'modules');
335
+ const objectsDir = join(projectPath, 'src', 'server', 'common', 'objects');
336
+
337
+ // Preload nest-server base classes once (used by resolveInheritedFields for all models)
338
+ preloadBaseClasses(project, projectPath);
339
+
340
+ const roleEnums = collectRoleEnums(project, projectPath);
341
+ const moduleNames = discoverModules(modulesDir);
342
+ const modules: ModulePermissions[] = [];
343
+
344
+ for (const name of moduleNames) {
345
+ try {
346
+ modules.push(scanModule(project, modulesDir, name, projectPath));
347
+ } catch (error) {
348
+ warn(`Failed to scan module '${name}': ${error}`);
349
+ }
350
+ }
351
+
352
+ const objects = scanObjects(project, objectsDir, projectPath);
353
+ const warnings = detectSecurityGaps(modules, objects);
354
+ const stats = calculateStats(modules, objects, warnings);
355
+
356
+ log(`Scan complete: ${modules.length} modules, ${objects.length} objects, ${warnings.length} warnings`);
357
+
358
+ return {
359
+ generated: new Date().toISOString(),
360
+ modules,
361
+ objects,
362
+ roleEnums,
363
+ stats,
364
+ warnings,
365
+ };
366
+ }
367
+
368
+ /**
369
+ * Scan a single module directory for models, inputs, outputs, controllers, and resolvers.
370
+ */
371
+ export function scanModule(
372
+ project: Project,
373
+ modulesDir: string,
374
+ moduleName: string,
375
+ projectPath: string,
376
+ ): ModulePermissions {
377
+ const moduleDir = join(modulesDir, moduleName);
378
+ const result: ModulePermissions = {
379
+ controllers: [],
380
+ inputs: [],
381
+ models: [],
382
+ name: moduleName,
383
+ outputs: [],
384
+ resolvers: [],
385
+ };
386
+
387
+ // Models
388
+ for (const file of listDir(moduleDir).filter((f) => f.endsWith('.model.ts'))) {
389
+ try {
390
+ const sf = project.addSourceFileAtPath(join(moduleDir, file));
391
+ const perms = parseFilePermissions(sf, relative(projectPath, join(moduleDir, file)), true);
392
+ if (perms) {
393
+ const classDecl = sf.getClasses()[0];
394
+ if (classDecl) {
395
+ const inheritedFields = resolveInheritedFields(project, classDecl);
396
+ const localNames = new Set(perms.fields.map((f) => f.name));
397
+ for (const iField of inheritedFields) {
398
+ if (!localNames.has(iField.name)) perms.fields.push(iField);
399
+ }
400
+ }
401
+ result.models.push(perms);
402
+ }
403
+ } catch {
404
+ /* skip */
405
+ }
406
+ }
407
+
408
+ // Inputs
409
+ const inputDir = join(moduleDir, 'inputs');
410
+ for (const file of listDir(inputDir).filter((f) => f.endsWith('.input.ts'))) {
411
+ try {
412
+ const sf = project.addSourceFileAtPath(join(inputDir, file));
413
+ const perms = parseFilePermissions(sf, relative(projectPath, join(inputDir, file)), false);
414
+ if (perms) result.inputs.push(perms);
415
+ } catch {
416
+ /* skip */
417
+ }
418
+ }
419
+
420
+ // Outputs
421
+ const outputDir = join(moduleDir, 'outputs');
422
+ for (const file of listDir(outputDir).filter((f) => f.endsWith('.output.ts'))) {
423
+ try {
424
+ const sf = project.addSourceFileAtPath(join(outputDir, file));
425
+ const perms = parseFilePermissions(sf, relative(projectPath, join(outputDir, file)), false);
426
+ if (perms) result.outputs.push(perms);
427
+ } catch {
428
+ /* skip */
429
+ }
430
+ }
431
+
432
+ // Controllers
433
+ for (const file of listDir(moduleDir).filter((f) => f.endsWith('.controller.ts'))) {
434
+ try {
435
+ const sf = project.addSourceFileAtPath(join(moduleDir, file));
436
+ const perms = parseEndpointPermissions(sf, relative(projectPath, join(moduleDir, file)));
437
+ if (perms) result.controllers.push(perms);
438
+ } catch {
439
+ /* skip */
440
+ }
441
+ }
442
+
443
+ // Resolvers
444
+ for (const file of listDir(moduleDir).filter((f) => f.endsWith('.resolver.ts'))) {
445
+ try {
446
+ const sf = project.addSourceFileAtPath(join(moduleDir, file));
447
+ const perms = parseEndpointPermissions(sf, relative(projectPath, join(moduleDir, file)));
448
+ if (perms) result.resolvers.push(perms);
449
+ } catch {
450
+ /* skip */
451
+ }
452
+ }
453
+
454
+ return result;
455
+ }
456
+
457
+ /**
458
+ * Scan SubObjects directory for .object.ts files.
459
+ */
460
+ export function scanObjects(project: Project, objectsDir: string, projectPath: string): FilePermissions[] {
461
+ const objects: FilePermissions[] = [];
462
+ if (!existsSync(objectsDir)) return objects;
463
+
464
+ for (const dir of listDir(objectsDir)) {
465
+ const dirPath = join(objectsDir, dir);
466
+ try {
467
+ if (!statSync(dirPath).isDirectory()) continue;
468
+ } catch {
469
+ continue;
470
+ }
471
+ for (const file of listDir(dirPath).filter((f) => f.endsWith('.object.ts'))) {
472
+ try {
473
+ const sf = project.addSourceFileAtPath(join(dirPath, file));
474
+ const perms = parseFilePermissions(sf, relative(projectPath, join(dirPath, file)), false);
475
+ if (perms) objects.push(perms);
476
+ } catch {
477
+ /* skip */
478
+ }
479
+ }
480
+ }
481
+
482
+ return objects;
483
+ }
484
+
485
+ // ─────────────────────────────────────────────────────────────────────────────
486
+ // Parsing helpers (exported for advanced usage, but normally not needed directly)
487
+ // ─────────────────────────────────────────────────────────────────────────────
488
+
489
+ /**
490
+ * Extract roles from decorator arguments, handling array syntax and enum prefixes.
491
+ */
492
+ export function extractDecoratorRoles(decoratorArgs: string[]): string[] {
493
+ const roles: string[] = [];
494
+ for (const arg of decoratorArgs) {
495
+ const cleaned = arg.trim();
496
+ if (cleaned.startsWith('[')) {
497
+ const inner = cleaned.slice(1, -1);
498
+ for (const item of inner.split(',')) {
499
+ roles.push(formatRole(item.trim()));
500
+ }
501
+ } else {
502
+ roles.push(formatRole(cleaned));
503
+ }
504
+ }
505
+ return roles;
506
+ }
507
+
508
+ /**
509
+ * Analyze a securityCheck() method body using regex patterns to detect:
510
+ * - `delete obj.field` statements (direct field removal)
511
+ * - Named arrays like `fieldsToRemove = [...]` (batch field removal)
512
+ * - Helper calls like `removeKeys(obj, [...])` (utility-based removal)
513
+ * - `return undefined/null` (full object suppression)
514
+ */
515
+ export function extractSecurityCheckInfo(classDecl: any): SecurityCheckInfo | undefined {
516
+ const method = classDecl.getMethod('securityCheck');
517
+ if (!method) return undefined;
518
+
519
+ const body = method.getBodyText() || '';
520
+ const fieldsStripped: string[] = [];
521
+
522
+ const deleteMatches = body.matchAll(/delete\s+\w+\.(\w+)/g);
523
+ for (const m of deleteMatches) fieldsStripped.push(m[1]);
524
+
525
+ const arrayMatches = body.matchAll(/(?:fieldsToRemove|removeFields|stripFields|fieldsToStrip)\s*=\s*\[([^\]]+)\]/g);
526
+ for (const m of arrayMatches) {
527
+ fieldsStripped.push(...m[1].split(',').map((f: string) => f.trim().replace(/['"]/g, '')));
528
+ }
529
+
530
+ const graphqlFieldMatches = body.matchAll(/(?:removeKeys|filterKeys)\s*\([^,]*,\s*\[([^\]]+)\]/g);
531
+ for (const m of graphqlFieldMatches) {
532
+ fieldsStripped.push(...m[1].split(',').map((f: string) => f.trim().replace(/['"]/g, '')));
533
+ }
534
+
535
+ const returnsUndefined = body.includes('return undefined') || body.includes('return null');
536
+
537
+ const summaryParts: string[] = ['Present'];
538
+ if (fieldsStripped.length > 0) summaryParts.push(`Strips fields: ${fieldsStripped.join(', ')}`);
539
+ if (returnsUndefined) summaryParts.push('May return undefined');
540
+
541
+ return { fieldsStripped: [...new Set(fieldsStripped)], returnsUndefined, summary: summaryParts.join('. ') };
542
+ }
543
+
544
+ /** Strip enum prefix (e.g. 'RoleEnum.ADMIN' -> 'ADMIN') */
545
+ export function formatRole(role: string): string {
546
+ if (!role) return '';
547
+ const dotIndex = role.lastIndexOf('.');
548
+ return dotIndex >= 0 ? role.substring(dotIndex + 1) : role;
549
+ }
550
+
551
+ /** Format roles array for display in Markdown output. */
552
+ export function formatRolesDisplay(roles: string[]): string {
553
+ if (roles.length === 0) return '*(none)*';
554
+ if (roles.length === 1) return `\`${roles[0]}\``;
555
+ return roles.map((r) => `\`${r}\``).join(', ');
556
+ }
557
+
558
+ /**
559
+ * Generate a Markdown report from a PermissionsReport.
560
+ *
561
+ * This is the single source of truth for Markdown output, used by both
562
+ * the nest-server runtime endpoint and the CLI command.
563
+ */
564
+ export function generateMarkdownReport(report: PermissionsReport, projectPath?: string): string {
565
+ const lines: string[] = [];
566
+
567
+ // Header
568
+ lines.push('# Permissions Report');
569
+ lines.push('');
570
+ lines.push(`> Generated: ${report.generated}`);
571
+ if (projectPath) lines.push(`> Project: ${projectPath}`);
572
+ lines.push(
573
+ `> Modules: ${report.stats.totalModules} | Models: ${report.stats.totalModels} | Endpoints: ${report.stats.totalEndpoints} | SubObjects: ${report.stats.totalSubObjects}`,
574
+ );
575
+ lines.push(
576
+ `> Warnings: ${report.stats.totalWarnings} | Endpoint Coverage: ${report.stats.endpointCoverage}% | Security Coverage: ${report.stats.securityCoverage}%`,
577
+ );
578
+ lines.push('');
579
+
580
+ // Table of contents
581
+ lines.push('## Table of Contents');
582
+ lines.push('');
583
+ lines.push('- [Role Index](#role-index)');
584
+ lines.push('- [Summary](#summary)');
585
+ lines.push('- [Warnings](#warnings)');
586
+ for (const mod of report.modules) {
587
+ const anchor = mod.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
588
+ lines.push(`- [Module: ${mod.name}](#module-${anchor})`);
589
+ }
590
+ if (report.objects.length > 0) {
591
+ lines.push('- [SubObjects](#subobjects)');
592
+ }
593
+ lines.push('');
594
+
595
+ // Role index
596
+ lines.push('## Role Index');
597
+ lines.push('');
598
+ if (report.roleEnums.length > 0) {
599
+ lines.push('| Enum | Value | Type |');
600
+ lines.push('|------|-------|------|');
601
+ for (const enumInfo of report.roleEnums) {
602
+ for (const v of enumInfo.values) {
603
+ const isSystem = v.key.startsWith('S_');
604
+ lines.push(
605
+ `| ${enumInfo.name}.${v.key} | ${isSystem ? '*(system)*' : `\`${v.value}\``} | ${isSystem ? 'System' : 'Real'} |`,
606
+ );
607
+ }
608
+ }
609
+ } else {
610
+ lines.push('*No role enums found.*');
611
+ }
612
+ lines.push('');
613
+
614
+ // Summary table
615
+ lines.push('## Summary');
616
+ lines.push('');
617
+ lines.push('| Module | Models | Inputs | Outputs | Controllers | Resolvers | Warnings |');
618
+ lines.push('|--------|--------|--------|---------|-------------|-----------|----------|');
619
+ for (const mod of report.modules) {
620
+ const modWarnings = report.warnings.filter((w) => w.module === mod.name).length;
621
+ lines.push(
622
+ `| ${mod.name} | ${mod.models.length} | ${mod.inputs.length} | ${mod.outputs.length} | ${mod.controllers.length} | ${mod.resolvers.length} | ${modWarnings} |`,
623
+ );
624
+ }
625
+ lines.push('');
626
+
627
+ if (report.stats.totalWarnings > 0) {
628
+ lines.push('### Warnings by Type');
629
+ lines.push('');
630
+ lines.push('| Type | Count |');
631
+ lines.push('|------|-------|');
632
+ for (const [type, count] of Object.entries(report.stats.warningsByType)) {
633
+ if (count > 0) lines.push(`| ${type} | ${count} |`);
634
+ }
635
+ lines.push('');
636
+ }
637
+
638
+ // Warnings
639
+ lines.push('## Warnings');
640
+ lines.push('');
641
+ if (report.warnings.length > 0) {
642
+ lines.push('| # | Module | File | Type | Details |');
643
+ lines.push('|---|--------|------|------|---------|');
644
+ report.warnings.forEach((w, i) => {
645
+ const fileName = w.file.split('/').pop() || w.file;
646
+ lines.push(`| ${i + 1} | ${w.module} | ${fileName} | ${w.type} | ${w.details} |`);
647
+ });
648
+ } else {
649
+ lines.push('*No warnings found.*');
650
+ }
651
+ lines.push('');
652
+
653
+ // Module details
654
+ for (const mod of report.modules) {
655
+ lines.push('---');
656
+ lines.push('');
657
+ lines.push(`## Module: ${mod.name}`);
658
+ lines.push('');
659
+
660
+ // Models
661
+ for (const model of mod.models) {
662
+ lines.push(`### Model: ${model.className}`);
663
+ lines.push(`- **File:** \`${model.filePath}\``);
664
+ if (model.extendsClass) lines.push(`- **Extends:** \`${model.extendsClass}\``);
665
+ lines.push(
666
+ `- **Class Restriction:** ${model.classRestriction.length > 0 ? model.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`,
667
+ );
668
+ if (model.securityCheck) {
669
+ lines.push(`- **securityCheck:** ${model.securityCheck.summary}`);
670
+ } else {
671
+ lines.push('- **securityCheck:** Not present');
672
+ }
673
+ lines.push('');
674
+
675
+ if (model.fields.length > 0) {
676
+ lines.push('| Field | Roles | Source |');
677
+ lines.push('|-------|-------|--------|');
678
+ for (const field of model.fields) {
679
+ const source = field.inherited ? 'inherited' : 'local';
680
+ lines.push(`| ${field.name} | ${field.roles} | ${source} |`);
681
+ }
682
+ }
683
+ lines.push('');
684
+ }
685
+
686
+ // Inputs
687
+ for (const input of mod.inputs) {
688
+ lines.push(`### Input: ${input.className}`);
689
+ lines.push(`- **File:** \`${input.filePath}\``);
690
+ if (input.extendsClass) lines.push(`- **Extends:** \`${input.extendsClass}\``);
691
+ lines.push(
692
+ `- **Class Restriction:** ${input.classRestriction.length > 0 ? input.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`,
693
+ );
694
+ lines.push('');
695
+
696
+ if (input.fields.length > 0) {
697
+ lines.push('| Field | Roles |');
698
+ lines.push('|-------|-------|');
699
+ for (const field of input.fields) {
700
+ lines.push(`| ${field.name} | ${field.roles} |`);
701
+ }
702
+ }
703
+ lines.push('');
704
+ }
705
+
706
+ // Outputs
707
+ for (const output of mod.outputs) {
708
+ lines.push(`### Output: ${output.className}`);
709
+ lines.push(`- **File:** \`${output.filePath}\``);
710
+ if (output.extendsClass) lines.push(`- **Extends:** \`${output.extendsClass}\``);
711
+ lines.push('');
712
+
713
+ if (output.fields.length > 0) {
714
+ lines.push('| Field | Roles |');
715
+ lines.push('|-------|-------|');
716
+ for (const field of output.fields) {
717
+ lines.push(`| ${field.name} | ${field.roles} |`);
718
+ }
719
+ }
720
+ lines.push('');
721
+ }
722
+
723
+ // Controllers
724
+ for (const ctrl of mod.controllers) {
725
+ lines.push(`### Controller: ${ctrl.className}`);
726
+ lines.push(`- **File:** \`${ctrl.filePath}\``);
727
+ lines.push(
728
+ `- **Class Roles:** ${ctrl.classRoles.length > 0 ? ctrl.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`,
729
+ );
730
+ lines.push('');
731
+
732
+ if (ctrl.methods.length > 0) {
733
+ lines.push('| Method | HTTP | Route | Roles | Effective |');
734
+ lines.push('|--------|------|-------|-------|-----------|');
735
+ for (const m of ctrl.methods) {
736
+ const effective =
737
+ m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(ctrl.classRoles)} (class)`;
738
+ lines.push(
739
+ `| ${m.name} | ${m.httpMethod} | ${m.route || '/'} | ${formatRolesDisplay(m.roles)} | ${effective} |`,
740
+ );
741
+ }
742
+ }
743
+ lines.push('');
744
+ }
745
+
746
+ // Resolvers
747
+ for (const res of mod.resolvers) {
748
+ lines.push(`### Resolver: ${res.className}`);
749
+ lines.push(`- **File:** \`${res.filePath}\``);
750
+ lines.push(
751
+ `- **Class Roles:** ${res.classRoles.length > 0 ? res.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`,
752
+ );
753
+ lines.push('');
754
+
755
+ if (res.methods.length > 0) {
756
+ lines.push('| Method | Type | Roles | Effective |');
757
+ lines.push('|--------|------|-------|-----------|');
758
+ for (const m of res.methods) {
759
+ const effective =
760
+ m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(res.classRoles)} (class)`;
761
+ lines.push(`| ${m.name} | ${m.httpMethod} | ${formatRolesDisplay(m.roles)} | ${effective} |`);
762
+ }
763
+ }
764
+ lines.push('');
765
+ }
766
+
767
+ // Effective matrix
768
+ const matrix = buildEffectiveMatrix(mod);
769
+ if (matrix.length > 0) {
770
+ lines.push(`### Effective Permissions: ${mod.name}`);
771
+ lines.push('');
772
+ lines.push('| Role | Endpoint Access |');
773
+ lines.push('|------|-----------------|');
774
+ for (const entry of matrix) {
775
+ const endpointList = entry.endpoints.map((e) => `${e.method} ${e.name}`).join(', ');
776
+ lines.push(`| \`${entry.role}\` | ${endpointList || '*(none)*'} |`);
777
+ }
778
+ lines.push('');
779
+ }
780
+ }
781
+
782
+ // SubObjects
783
+ if (report.objects.length > 0) {
784
+ lines.push('---');
785
+ lines.push('');
786
+ lines.push('## SubObjects');
787
+ lines.push('');
788
+
789
+ for (const obj of report.objects) {
790
+ lines.push(`### ${obj.className}`);
791
+ lines.push(`- **File:** \`${obj.filePath}\``);
792
+ if (obj.extendsClass) lines.push(`- **Extends:** \`${obj.extendsClass}\``);
793
+ lines.push(
794
+ `- **Class Restriction:** ${obj.classRestriction.length > 0 ? obj.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`,
795
+ );
796
+ lines.push('');
797
+
798
+ if (obj.fields.length > 0) {
799
+ lines.push('| Field | Roles | Source |');
800
+ lines.push('|-------|-------|--------|');
801
+ for (const field of obj.fields) {
802
+ const source = field.inherited ? 'inherited' : 'local';
803
+ lines.push(`| ${field.name} | ${field.roles} | ${source} |`);
804
+ }
805
+ }
806
+ lines.push('');
807
+ }
808
+ }
809
+
810
+ return lines.join('\n');
811
+ }
812
+
813
+ /**
814
+ * Parse endpoint permissions (controller/resolver) from a source file.
815
+ */
816
+ export function parseEndpointPermissions(sourceFile: any, filePath: string): EndpointPermissions | undefined {
817
+ const classes = sourceFile.getClasses();
818
+ if (classes.length === 0) return undefined;
819
+
820
+ const classDecl = classes[0];
821
+ const className = classDecl.getName() || 'Unknown';
822
+ const classRoles = parseRolesDecorator(classDecl);
823
+
824
+ let controllerPrefix = '';
825
+ const controllerDeco = classDecl.getDecorator('Controller');
826
+ if (controllerDeco) {
827
+ const args = controllerDeco.getArguments();
828
+ if (args.length > 0) controllerPrefix = args[0].getText().replace(/['"]/g, '');
829
+ }
830
+
831
+ const methods: MethodPermission[] = [];
832
+ for (const method of classDecl.getMethods()) {
833
+ const methodName = method.getName();
834
+ if (methodName.startsWith('_') || ['onModuleDestroy', 'onModuleInit'].includes(methodName)) continue;
835
+
836
+ const methodRoles = parseRolesDecorator(method);
837
+
838
+ for (const httpDeco of ['Delete', 'Get', 'Patch', 'Post', 'Put']) {
839
+ const deco = method.getDecorator(httpDeco);
840
+ if (deco) {
841
+ const args = deco.getArguments();
842
+ const route = args.length > 0 ? args[0].getText().replace(/['"]/g, '') : '/';
843
+ const fullRoute = controllerPrefix
844
+ ? `/${controllerPrefix}/${route}`.replace(/\/+/g, '/')
845
+ : `/${route}`.replace(/\/+/g, '/');
846
+ methods.push({ httpMethod: httpDeco.toUpperCase(), name: methodName, roles: methodRoles, route: fullRoute });
847
+ }
848
+ }
849
+
850
+ for (const gqlDeco of ['Mutation', 'Query', 'Subscription']) {
851
+ const deco = method.getDecorator(gqlDeco);
852
+ if (deco) {
853
+ methods.push({ httpMethod: gqlDeco, name: methodName, roles: methodRoles });
854
+ }
855
+ }
856
+ }
857
+
858
+ return { className, classRoles, filePath, methods };
859
+ }
860
+
861
+ /**
862
+ * Parse field permissions from a class property, checking @UnifiedField and @Restricted decorators.
863
+ */
864
+ export function parseFieldPermission(prop: any): FieldPermission {
865
+ const fieldName = prop.getName();
866
+ let roles = '*(none)*';
867
+ let description: string | undefined;
868
+
869
+ const unifiedField = prop.getDecorator('UnifiedField');
870
+ if (unifiedField) {
871
+ const args = unifiedField.getArguments();
872
+ if (args.length > 0) {
873
+ const optionsArg = args[0];
874
+ if (optionsArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
875
+ const objLit = optionsArg.asKind(SyntaxKind.ObjectLiteralExpression);
876
+
877
+ const rolesProp = objLit?.getProperty('roles');
878
+ if (rolesProp) {
879
+ const init = rolesProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
880
+ if (init) {
881
+ const rolesText = init.getText();
882
+ if (rolesText.startsWith('[')) {
883
+ const inner = rolesText.slice(1, -1);
884
+ const roleList = inner.split(',').map((r: string) => formatRole(r.trim()));
885
+ roles = roleList.map((r: string) => `\`${r}\``).join(', ');
886
+ } else {
887
+ roles = `\`${formatRole(rolesText)}\``;
888
+ }
889
+ }
890
+ }
891
+
892
+ const descProp = objLit?.getProperty('description');
893
+ if (descProp) {
894
+ const init = descProp.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
895
+ if (init) description = init.getText().replace(/^['"]|['"]$/g, '');
896
+ }
897
+ }
898
+ }
899
+ }
900
+
901
+ if (roles === '*(none)*') {
902
+ const restricted = prop.getDecorator('Restricted');
903
+ if (restricted) {
904
+ const args = restricted.getArguments().map((a: any) => a.getText());
905
+ if (args.length > 0) {
906
+ const roleList = extractDecoratorRoles(args);
907
+ roles = roleList.map((r: string) => `\`${r}\``).join(', ');
908
+ }
909
+ }
910
+ }
911
+
912
+ return { description, name: fieldName, roles };
913
+ }
914
+
915
+ /**
916
+ * Parse file permissions (model/input/output) from a source file.
917
+ */
918
+ export function parseFilePermissions(sourceFile: any, filePath: string, isModel: boolean): FilePermissions | undefined {
919
+ const classes = sourceFile.getClasses();
920
+ if (classes.length === 0) return undefined;
921
+
922
+ const classDecl = classes[0];
923
+ const className = classDecl.getName() || 'Unknown';
924
+ const extendsExpr = classDecl.getExtends();
925
+ const extendsClass = extendsExpr?.getText()?.replace(/<.*>/, '') || undefined;
926
+ const classRestriction = parseRestrictedDecorator(classDecl);
927
+ const securityCheck = isModel ? extractSecurityCheckInfo(classDecl) : undefined;
928
+
929
+ const fields: FieldPermission[] = [];
930
+ for (const prop of classDecl.getProperties()) {
931
+ fields.push(parseFieldPermission(prop));
932
+ }
933
+
934
+ return { className, classRestriction, extendsClass, fields, filePath, securityCheck };
935
+ }
936
+
937
+ /** Parse @Restricted decorator roles from a class or property. */
938
+ export function parseRestrictedDecorator(node: any): string[] {
939
+ const restricted = node.getDecorator('Restricted');
940
+ if (!restricted) return [];
941
+ const args = restricted.getArguments().map((a: any) => a.getText());
942
+ return extractDecoratorRoles(args);
943
+ }
944
+
945
+ /** Parse @Roles decorator roles from a class or method. */
946
+ export function parseRolesDecorator(node: any): string[] {
947
+ const roles = node.getDecorator('Roles');
948
+ if (!roles) return [];
949
+ const args = roles.getArguments().map((a: any) => a.getText());
950
+ return extractDecoratorRoles(args);
951
+ }
952
+
953
+ /**
954
+ * Resolve inherited fields from base classes in node_modules/@lenne.tech/nest-server.
955
+ * Recursively walks the inheritance chain.
956
+ *
957
+ * Requires base classes to be preloaded via preloadBaseClasses() (called once in scanPermissions).
958
+ */
959
+ export function resolveInheritedFields(project: Project, classDecl: any): FieldPermission[] {
960
+ const inherited: FieldPermission[] = [];
961
+ const extendsExpr = classDecl.getExtends();
962
+ if (!extendsExpr) return inherited;
963
+
964
+ const baseClassName = extendsExpr.getText().replace(/<.*>/, '');
965
+
966
+ // Search already-loaded source files (preloaded in scanPermissions)
967
+ for (const sf of project.getSourceFiles()) {
968
+ for (const cls of sf.getClasses()) {
969
+ if (cls.getName() === baseClassName) {
970
+ for (const prop of cls.getProperties()) {
971
+ const field = parseFieldPermission(prop);
972
+ field.inherited = true;
973
+ inherited.push(field);
974
+ }
975
+ const parentFields = resolveInheritedFields(project, cls);
976
+ inherited.push(...parentFields);
977
+ return inherited;
978
+ }
979
+ }
980
+ }
981
+
982
+ return inherited;
983
+ }
984
+
985
+ // ─────────────────────────────────────────────────────────────────────────────
986
+ // Internal helpers
987
+ // ─────────────────────────────────────────────────────────────────────────────
988
+
989
+ function preloadBaseClasses(project: Project, projectPath: string): void {
990
+ const nestServerPaths = [
991
+ join(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'src'),
992
+ join(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist'),
993
+ ];
994
+ for (const basePath of nestServerPaths) {
995
+ try {
996
+ if (existsSync(basePath)) {
997
+ project.addSourceFilesAtPaths(join(basePath, '**', '*.ts'));
998
+ }
999
+ } catch {
1000
+ // Base path not available
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ function listDir(dir: string): string[] {
1006
+ try {
1007
+ return readdirSync(dir);
1008
+ } catch {
1009
+ return [];
1010
+ }
1011
+ }